MediaWiki
REL1_22
|
00001 <?php 00030 class UploadFromChunks extends UploadFromFile { 00031 protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath; 00032 00040 public function __construct( $user = null, $stash = false, $repo = false ) { 00041 // user object. sometimes this won't exist, as when running from cron. 00042 $this->user = $user; 00043 00044 if ( $repo ) { 00045 $this->repo = $repo; 00046 } else { 00047 $this->repo = RepoGroup::singleton()->getLocalRepo(); 00048 } 00049 00050 if ( $stash ) { 00051 $this->stash = $stash; 00052 } else { 00053 if ( $user ) { 00054 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" ); 00055 } else { 00056 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" ); 00057 } 00058 $this->stash = new UploadStash( $this->repo, $this->user ); 00059 } 00060 00061 return true; 00062 } 00063 00069 public function stashFile( User $user = null ) { 00070 // Stash file is the called on creating a new chunk session: 00071 $this->mChunkIndex = 0; 00072 $this->mOffset = 0; 00073 00074 $this->verifyChunk(); 00075 // Create a local stash target 00076 $this->mLocalFile = parent::stashFile(); 00077 // Update the initial file offset (based on file size) 00078 $this->mOffset = $this->mLocalFile->getSize(); 00079 $this->mFileKey = $this->mLocalFile->getFileKey(); 00080 00081 // Output a copy of this first to chunk 0 location: 00082 $this->outputChunk( $this->mLocalFile->getPath() ); 00083 00084 // Update db table to reflect initial "chunk" state 00085 $this->updateChunkStatus(); 00086 return $this->mLocalFile; 00087 } 00088 00092 public function continueChunks( $name, $key, $webRequestUpload ) { 00093 $this->mFileKey = $key; 00094 $this->mUpload = $webRequestUpload; 00095 // Get the chunk status form the db: 00096 $this->getChunkStatus(); 00097 00098 $metadata = $this->stash->getMetadata( $key ); 00099 $this->initializePathInfo( $name, 00100 $this->getRealPath( $metadata['us_path'] ), 00101 $metadata['us_size'], 00102 false 00103 ); 00104 } 00105 00110 public function concatenateChunks() { 00111 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . 00112 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); 00113 00114 // Concatenate all the chunks to mVirtualTempPath 00115 $fileList = Array(); 00116 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1" 00117 for ( $i = 0; $i <= $this->getChunkIndex(); $i++ ) { 00118 $fileList[] = $this->getVirtualChunkLocation( $i ); 00119 } 00120 00121 // Get the file extension from the last chunk 00122 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); 00123 // Get a 0-byte temp file to perform the concatenation at 00124 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext ); 00125 $tmpPath = $tmpFile 00126 ? $tmpFile->bind( $this )->getPath() // keep alive with $this 00127 : false; // fail in concatenate() 00128 // Concatenate the chunks at the temp file 00129 $tStart = microtime( true ); 00130 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); 00131 $tAmount = microtime( true ) - $tStart; 00132 if ( !$status->isOk() ) { 00133 return $status; 00134 } 00135 wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds.\n" ); 00136 00137 $this->mTempPath = $tmpPath; // file system path 00138 $this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously 00139 $ret = $this->verifyUpload(); 00140 if ( $ret['status'] !== UploadBase::OK ) { 00141 wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); 00142 $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); 00143 return $status; 00144 } 00145 00146 // Update the mTempPath and mLocalFile 00147 // (for FileUpload or normal Stash to take over) 00148 $tStart = microtime( true ); 00149 $this->mLocalFile = parent::stashFile( $this->user ); 00150 $tAmount = microtime( true ) - $tStart; 00151 $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo()) 00152 wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds.\n" ); 00153 00154 return $status; 00155 } 00156 00165 public function performUpload( $comment, $pageText, $watch, $user ) { 00166 $rv = parent::performUpload( $comment, $pageText, $watch, $user ); 00167 return $rv; 00168 } 00169 00175 function getVirtualChunkLocation( $index ) { 00176 return $this->repo->getVirtualUrl( 'temp' ) . 00177 '/' . 00178 $this->repo->getHashPath( 00179 $this->getChunkFileKey( $index ) 00180 ) . 00181 $this->getChunkFileKey( $index ); 00182 } 00183 00192 public function addChunk( $chunkPath, $chunkSize, $offset ) { 00193 // Get the offset before we add the chunk to the file system 00194 $preAppendOffset = $this->getOffset(); 00195 00196 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) { 00197 $status = Status::newFatal( 'file-too-large' ); 00198 } else { 00199 // Make sure the client is uploading the correct chunk with a matching offset. 00200 if ( $preAppendOffset == $offset ) { 00201 // Update local chunk index for the current chunk 00202 $this->mChunkIndex++; 00203 try { 00204 # For some reason mTempPath is set to first part 00205 $oldTemp = $this->mTempPath; 00206 $this->mTempPath = $chunkPath; 00207 $this->verifyChunk(); 00208 $this->mTempPath = $oldTemp; 00209 } catch ( UploadChunkVerificationException $e ) { 00210 return Status::newFatal( $e->getMessage() ); 00211 } 00212 $status = $this->outputChunk( $chunkPath ); 00213 if ( $status->isGood() ) { 00214 // Update local offset: 00215 $this->mOffset = $preAppendOffset + $chunkSize; 00216 // Update chunk table status db 00217 $this->updateChunkStatus(); 00218 } 00219 } else { 00220 $status = Status::newFatal( 'invalid-chunk-offset' ); 00221 } 00222 } 00223 return $status; 00224 } 00225 00229 private function updateChunkStatus() { 00230 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . 00231 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); 00232 00233 $dbw = $this->repo->getMasterDb(); 00234 // Use a quick transaction since we will upload the full temp file into shared 00235 // storage, which takes time for large files. We don't want to hold locks then. 00236 $dbw->begin( __METHOD__ ); 00237 $dbw->update( 00238 'uploadstash', 00239 array( 00240 'us_status' => 'chunks', 00241 'us_chunk_inx' => $this->getChunkIndex(), 00242 'us_size' => $this->getOffset() 00243 ), 00244 array( 'us_key' => $this->mFileKey ), 00245 __METHOD__ 00246 ); 00247 $dbw->commit( __METHOD__ ); 00248 } 00249 00253 private function getChunkStatus() { 00254 // get Master db to avoid race conditions. 00255 // Otherwise, if chunk upload time < replag there will be spurious errors 00256 $dbw = $this->repo->getMasterDb(); 00257 $row = $dbw->selectRow( 00258 'uploadstash', 00259 array( 00260 'us_chunk_inx', 00261 'us_size', 00262 'us_path', 00263 ), 00264 array( 'us_key' => $this->mFileKey ), 00265 __METHOD__ 00266 ); 00267 // Handle result: 00268 if ( $row ) { 00269 $this->mChunkIndex = $row->us_chunk_inx; 00270 $this->mOffset = $row->us_size; 00271 $this->mVirtualTempPath = $row->us_path; 00272 } 00273 } 00274 00279 private function getChunkIndex() { 00280 if ( $this->mChunkIndex !== null ) { 00281 return $this->mChunkIndex; 00282 } 00283 return 0; 00284 } 00285 00290 private function getOffset() { 00291 if ( $this->mOffset !== null ) { 00292 return $this->mOffset; 00293 } 00294 return 0; 00295 } 00296 00304 private function outputChunk( $chunkPath ) { 00305 // Key is fileKey + chunk index 00306 $fileKey = $this->getChunkFileKey(); 00307 00308 // Store the chunk per its indexed fileKey: 00309 $hashPath = $this->repo->getHashPath( $fileKey ); 00310 $storeStatus = $this->repo->quickImport( $chunkPath, 00311 $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); 00312 00313 // Check for error in stashing the chunk: 00314 if ( ! $storeStatus->isOK() ) { 00315 $error = $storeStatus->getErrorsArray(); 00316 $error = reset( $error ); 00317 if ( ! count( $error ) ) { 00318 $error = $storeStatus->getWarningsArray(); 00319 $error = reset( $error ); 00320 if ( ! count( $error ) ) { 00321 $error = array( 'unknown', 'no error recorded' ); 00322 } 00323 } 00324 throw new UploadChunkFileException( "error storing file in '$chunkPath': " . implode( '; ', $error ) ); 00325 } 00326 return $storeStatus; 00327 } 00328 00329 private function getChunkFileKey( $index = null ) { 00330 if ( $index === null ) { 00331 $index = $this->getChunkIndex(); 00332 } 00333 return $this->mFileKey . '.' . $index; 00334 } 00335 00341 private function verifyChunk() { 00342 // Rest mDesiredDestName here so we verify the name as if it were mFileKey 00343 $oldDesiredDestName = $this->mDesiredDestName; 00344 $this->mDesiredDestName = $this->mFileKey; 00345 $this->mTitle = false; 00346 $res = $this->verifyPartialFile(); 00347 $this->mDesiredDestName = $oldDesiredDestName; 00348 $this->mTitle = false; 00349 if ( is_array( $res ) ) { 00350 throw new UploadChunkVerificationException( $res[0] ); 00351 } 00352 } 00353 } 00354 00355 class UploadChunkZeroLengthFileException extends MWException {}; 00356 class UploadChunkFileException extends MWException {}; 00357 class UploadChunkVerificationException extends MWException {};