MediaWiki  REL1_22
UploadFromChunks.php
Go to the documentation of this file.
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 {};