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