MediaWiki
REL1_24
|
00001 <?php 00030 class UploadFromChunks extends UploadFromFile { 00031 protected $mOffset; 00032 protected $mChunkIndex; 00033 protected $mFileKey; 00034 protected $mVirtualTempPath; 00036 private $repo; 00037 00045 public function __construct( $user = null, $stash = false, $repo = false ) { 00046 // user object. sometimes this won't exist, as when running from cron. 00047 $this->user = $user; 00048 00049 if ( $repo ) { 00050 $this->repo = $repo; 00051 } else { 00052 $this->repo = RepoGroup::singleton()->getLocalRepo(); 00053 } 00054 00055 if ( $stash ) { 00056 $this->stash = $stash; 00057 } else { 00058 if ( $user ) { 00059 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" ); 00060 } else { 00061 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" ); 00062 } 00063 $this->stash = new UploadStash( $this->repo, $this->user ); 00064 } 00065 } 00066 00073 public function stashFile( User $user = null ) { 00074 // Stash file is the called on creating a new chunk session: 00075 $this->mChunkIndex = 0; 00076 $this->mOffset = 0; 00077 00078 $this->verifyChunk(); 00079 // Create a local stash target 00080 $this->mLocalFile = parent::stashFile(); 00081 // Update the initial file offset (based on file size) 00082 $this->mOffset = $this->mLocalFile->getSize(); 00083 $this->mFileKey = $this->mLocalFile->getFileKey(); 00084 00085 // Output a copy of this first to chunk 0 location: 00086 $this->outputChunk( $this->mLocalFile->getPath() ); 00087 00088 // Update db table to reflect initial "chunk" state 00089 $this->updateChunkStatus(); 00090 00091 return $this->mLocalFile; 00092 } 00093 00101 public function continueChunks( $name, $key, $webRequestUpload ) { 00102 $this->mFileKey = $key; 00103 $this->mUpload = $webRequestUpload; 00104 // Get the chunk status form the db: 00105 $this->getChunkStatus(); 00106 00107 $metadata = $this->stash->getMetadata( $key ); 00108 $this->initializePathInfo( $name, 00109 $this->getRealPath( $metadata['us_path'] ), 00110 $metadata['us_size'], 00111 false 00112 ); 00113 } 00114 00119 public function concatenateChunks() { 00120 $chunkIndex = $this->getChunkIndex(); 00121 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . 00122 $this->getOffset() . ' inx:' . $chunkIndex . "\n" ); 00123 00124 // Concatenate all the chunks to mVirtualTempPath 00125 $fileList = array(); 00126 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1" 00127 for ( $i = 0; $i <= $chunkIndex; $i++ ) { 00128 $fileList[] = $this->getVirtualChunkLocation( $i ); 00129 } 00130 00131 // Get the file extension from the last chunk 00132 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); 00133 // Get a 0-byte temp file to perform the concatenation at 00134 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext ); 00135 $tmpPath = false; // fail in concatenate() 00136 if ( $tmpFile ) { 00137 // keep alive with $this 00138 $tmpPath = $tmpFile->bind( $this )->getPath(); 00139 } 00140 00141 // Concatenate the chunks at the temp file 00142 $tStart = microtime( true ); 00143 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); 00144 $tAmount = microtime( true ) - $tStart; 00145 if ( !$status->isOk() ) { 00146 return $status; 00147 } 00148 wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds." ); 00149 00150 // File system path 00151 $this->mTempPath = $tmpPath; 00152 // Since this was set for the last chunk previously 00153 $this->mFileSize = filesize( $this->mTempPath ); 00154 $ret = $this->verifyUpload(); 00155 if ( $ret['status'] !== UploadBase::OK ) { 00156 wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); 00157 $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); 00158 00159 return $status; 00160 } 00161 00162 // Update the mTempPath and mLocalFile 00163 // (for FileUpload or normal Stash to take over) 00164 $tStart = microtime( true ); 00165 $this->mLocalFile = parent::stashFile( $this->user ); 00166 $tAmount = microtime( true ) - $tStart; 00167 $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo()) 00168 wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." ); 00169 00170 return $status; 00171 } 00172 00181 public function performUpload( $comment, $pageText, $watch, $user ) { 00182 $rv = parent::performUpload( $comment, $pageText, $watch, $user ); 00183 00184 return $rv; 00185 } 00186 00192 function getVirtualChunkLocation( $index ) { 00193 return $this->repo->getVirtualUrl( 'temp' ) . 00194 '/' . 00195 $this->repo->getHashPath( 00196 $this->getChunkFileKey( $index ) 00197 ) . 00198 $this->getChunkFileKey( $index ); 00199 } 00200 00209 public function addChunk( $chunkPath, $chunkSize, $offset ) { 00210 // Get the offset before we add the chunk to the file system 00211 $preAppendOffset = $this->getOffset(); 00212 00213 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) { 00214 $status = Status::newFatal( 'file-too-large' ); 00215 } else { 00216 // Make sure the client is uploading the correct chunk with a matching offset. 00217 if ( $preAppendOffset == $offset ) { 00218 // Update local chunk index for the current chunk 00219 $this->mChunkIndex++; 00220 try { 00221 # For some reason mTempPath is set to first part 00222 $oldTemp = $this->mTempPath; 00223 $this->mTempPath = $chunkPath; 00224 $this->verifyChunk(); 00225 $this->mTempPath = $oldTemp; 00226 } catch ( UploadChunkVerificationException $e ) { 00227 return Status::newFatal( $e->getMessage() ); 00228 } 00229 $status = $this->outputChunk( $chunkPath ); 00230 if ( $status->isGood() ) { 00231 // Update local offset: 00232 $this->mOffset = $preAppendOffset + $chunkSize; 00233 // Update chunk table status db 00234 $this->updateChunkStatus(); 00235 } 00236 } else { 00237 $status = Status::newFatal( 'invalid-chunk-offset' ); 00238 } 00239 } 00240 00241 return $status; 00242 } 00243 00247 private function updateChunkStatus() { 00248 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . 00249 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); 00250 00251 $dbw = $this->repo->getMasterDb(); 00252 // Use a quick transaction since we will upload the full temp file into shared 00253 // storage, which takes time for large files. We don't want to hold locks then. 00254 $dbw->begin( __METHOD__ ); 00255 $dbw->update( 00256 'uploadstash', 00257 array( 00258 'us_status' => 'chunks', 00259 'us_chunk_inx' => $this->getChunkIndex(), 00260 'us_size' => $this->getOffset() 00261 ), 00262 array( 'us_key' => $this->mFileKey ), 00263 __METHOD__ 00264 ); 00265 $dbw->commit( __METHOD__ ); 00266 } 00267 00271 private function getChunkStatus() { 00272 // get Master db to avoid race conditions. 00273 // Otherwise, if chunk upload time < replag there will be spurious errors 00274 $dbw = $this->repo->getMasterDb(); 00275 $row = $dbw->selectRow( 00276 'uploadstash', 00277 array( 00278 'us_chunk_inx', 00279 'us_size', 00280 'us_path', 00281 ), 00282 array( 'us_key' => $this->mFileKey ), 00283 __METHOD__ 00284 ); 00285 // Handle result: 00286 if ( $row ) { 00287 $this->mChunkIndex = $row->us_chunk_inx; 00288 $this->mOffset = $row->us_size; 00289 $this->mVirtualTempPath = $row->us_path; 00290 } 00291 } 00292 00297 private function getChunkIndex() { 00298 if ( $this->mChunkIndex !== null ) { 00299 return $this->mChunkIndex; 00300 } 00301 00302 return 0; 00303 } 00304 00309 private function getOffset() { 00310 if ( $this->mOffset !== null ) { 00311 return $this->mOffset; 00312 } 00313 00314 return 0; 00315 } 00316 00324 private function outputChunk( $chunkPath ) { 00325 // Key is fileKey + chunk index 00326 $fileKey = $this->getChunkFileKey(); 00327 00328 // Store the chunk per its indexed fileKey: 00329 $hashPath = $this->repo->getHashPath( $fileKey ); 00330 $storeStatus = $this->repo->quickImport( $chunkPath, 00331 $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); 00332 00333 // Check for error in stashing the chunk: 00334 if ( !$storeStatus->isOK() ) { 00335 $error = $storeStatus->getErrorsArray(); 00336 $error = reset( $error ); 00337 if ( !count( $error ) ) { 00338 $error = $storeStatus->getWarningsArray(); 00339 $error = reset( $error ); 00340 if ( !count( $error ) ) { 00341 $error = array( 'unknown', 'no error recorded' ); 00342 } 00343 } 00344 throw new UploadChunkFileException( "Error storing file in '$chunkPath': " . 00345 implode( '; ', $error ) ); 00346 } 00347 00348 return $storeStatus; 00349 } 00350 00351 private function getChunkFileKey( $index = null ) { 00352 if ( $index === null ) { 00353 $index = $this->getChunkIndex(); 00354 } 00355 00356 return $this->mFileKey . '.' . $index; 00357 } 00358 00364 private function verifyChunk() { 00365 // Rest mDesiredDestName here so we verify the name as if it were mFileKey 00366 $oldDesiredDestName = $this->mDesiredDestName; 00367 $this->mDesiredDestName = $this->mFileKey; 00368 $this->mTitle = false; 00369 $res = $this->verifyPartialFile(); 00370 $this->mDesiredDestName = $oldDesiredDestName; 00371 $this->mTitle = false; 00372 if ( is_array( $res ) ) { 00373 throw new UploadChunkVerificationException( $res[0] ); 00374 } 00375 } 00376 } 00377 00378 class UploadChunkZeroLengthFileException extends MWException { 00379 } 00380 00381 class UploadChunkFileException extends MWException { 00382 } 00383 00384 class UploadChunkVerificationException extends MWException { 00385 }