MediaWiki  master
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 = false, $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         }
00068         public function stashFile() {
00069                 // Stash file is the called on creating a new chunk session:
00070                 $this->mChunkIndex = 0;
00071                 $this->mOffset = 0;
00072                 // Create a local stash target
00073                 $this->mLocalFile = parent::stashFile();
00074                 // Update the initial file offset ( based on file size )
00075                 $this->mOffset = $this->mLocalFile->getSize();
00076                 $this->mFileKey = $this->mLocalFile->getFileKey();
00077 
00078                 // Output a copy of this first to chunk 0 location:
00079                 $this->outputChunk( $this->mLocalFile->getPath() );
00080 
00081                 // Update db table to reflect initial "chunk" state
00082                 $this->updateChunkStatus();
00083                 return $this->mLocalFile;
00084         }
00085 
00089         public function continueChunks( $name, $key, $webRequestUpload ) {
00090                 $this->mFileKey = $key;
00091                 $this->mUpload = $webRequestUpload;
00092                 // Get the chunk status form the db:
00093                 $this->getChunkStatus();
00094 
00095                 $metadata = $this->stash->getMetadata( $key );
00096                 $this->initializePathInfo( $name,
00097                         $this->getRealPath( $metadata['us_path'] ),
00098                         $metadata['us_size'],
00099                         false
00100                 );
00101         }
00102 
00107         public function concatenateChunks() {
00108                 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
00109                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00110 
00111                 // Concatenate all the chunks to mVirtualTempPath
00112                 $fileList = Array();
00113                 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
00114                 for( $i = 0; $i <= $this->getChunkIndex(); $i++ ){
00115                         $fileList[] = $this->getVirtualChunkLocation( $i );
00116                 }
00117 
00118                 // Get the file extension from the last chunk
00119                 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
00120                 // Get a 0-byte temp file to perform the concatenation at
00121                 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
00122                 $tmpPath = $tmpFile
00123                         ? $tmpFile->getPath()
00124                         : false; // fail in concatenate()
00125                 // Concatenate the chunks at the temp file
00126                 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
00127                 if( !$status->isOk() ){
00128                         return $status;
00129                 }
00130                 // Update the mTempPath and mLocalFile
00131                 // ( for FileUpload or normal Stash to take over )
00132                 $this->mTempPath = $tmpPath; // file system path
00133                 $this->mLocalFile = parent::stashFile();
00134 
00135                 return $status;
00136         }
00137 
00146         public function performUpload( $comment, $pageText, $watch, $user ) {
00147                 $rv = parent::performUpload( $comment, $pageText, $watch, $user );
00148                 return $rv;
00149         }
00150 
00156         function getVirtualChunkLocation( $index ){
00157                 return $this->repo->getVirtualUrl( 'temp' ) .
00158                                 '/' .
00159                                 $this->repo->getHashPath(
00160                                         $this->getChunkFileKey( $index )
00161                                 ) .
00162                                 $this->getChunkFileKey( $index );
00163         }
00164 
00173         public function addChunk( $chunkPath, $chunkSize, $offset ) {
00174                 // Get the offset before we add the chunk to the file system
00175                 $preAppendOffset = $this->getOffset();
00176 
00177                 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize()) {
00178                         $status = Status::newFatal( 'file-too-large' );
00179                 } else {
00180                         // Make sure the client is uploading the correct chunk with a matching offset.
00181                         if ( $preAppendOffset == $offset ) {
00182                                 // Update local chunk index for the current chunk
00183                                 $this->mChunkIndex++;
00184                                 $status = $this->outputChunk( $chunkPath );
00185                                 if( $status->isGood() ){
00186                                         // Update local offset:
00187                                         $this->mOffset = $preAppendOffset + $chunkSize;
00188                                         // Update chunk table status db
00189                                         $this->updateChunkStatus();
00190                                 }
00191                         } else {
00192                                 $status = Status::newFatal( 'invalid-chunk-offset' );
00193                         }
00194                 }
00195                 return $status;
00196         }
00197 
00201         private function updateChunkStatus(){
00202                 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
00203                                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00204 
00205                 $dbw = $this->repo->getMasterDb();
00206                 $dbw->update(
00207                         'uploadstash',
00208                         array(
00209                                 'us_status' => 'chunks',
00210                                 'us_chunk_inx' => $this->getChunkIndex(),
00211                                 'us_size' => $this->getOffset()
00212                         ),
00213                         array( 'us_key' => $this->mFileKey ),
00214                         __METHOD__
00215                 );
00216         }
00217 
00221         private function getChunkStatus(){
00222                 // get Master db to avoid race conditions.
00223                 // Otherwise, if chunk upload time < replag there will be spurious errors
00224                 $dbw = $this->repo->getMasterDb();
00225                 $row = $dbw->selectRow(
00226                         'uploadstash',
00227                         array(
00228                                 'us_chunk_inx',
00229                                 'us_size',
00230                                 'us_path',
00231                         ),
00232                         array( 'us_key' => $this->mFileKey ),
00233                         __METHOD__
00234                 );
00235                 // Handle result:
00236                 if ( $row ) {
00237                         $this->mChunkIndex = $row->us_chunk_inx;
00238                         $this->mOffset = $row->us_size;
00239                         $this->mVirtualTempPath = $row->us_path;
00240                 }
00241         }
00242 
00247         private function getChunkIndex(){
00248                 if( $this->mChunkIndex !== null ){
00249                         return $this->mChunkIndex;
00250                 }
00251                 return 0;
00252         }
00253 
00258         private function getOffset(){
00259                 if ( $this->mOffset !== null ){
00260                         return $this->mOffset;
00261                 }
00262                 return 0;
00263         }
00264 
00272         private function outputChunk( $chunkPath ){
00273                 // Key is fileKey + chunk index
00274                 $fileKey = $this->getChunkFileKey();
00275 
00276                 // Store the chunk per its indexed fileKey:
00277                 $hashPath = $this->repo->getHashPath( $fileKey );
00278                 $storeStatus = $this->repo->quickImport( $chunkPath,
00279                         $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" );
00280 
00281                 // Check for error in stashing the chunk:
00282                 if ( ! $storeStatus->isOK() ) {
00283                         $error = $storeStatus->getErrorsArray();
00284                         $error = reset( $error );
00285                         if ( ! count( $error ) ) {
00286                                 $error = $storeStatus->getWarningsArray();
00287                                 $error = reset( $error );
00288                                 if ( ! count( $error ) ) {
00289                                         $error = array( 'unknown', 'no error recorded' );
00290                                 }
00291                         }
00292                         throw new UploadChunkFileException( "error storing file in '$chunkPath': " . implode( '; ', $error ) );
00293                 }
00294                 return $storeStatus;
00295         }
00296 
00297         private function getChunkFileKey( $index = null ){
00298                 if( $index === null ){
00299                         $index = $this->getChunkIndex();
00300                 }
00301                 return $this->mFileKey . '.' . $index ;
00302         }
00303 }
00304 
00305 class UploadChunkZeroLengthFileException extends MWException {};
00306 class UploadChunkFileException extends MWException {};