MediaWiki  REL1_24
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;
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 }