MediaWiki  REL1_21
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 {};