MediaWiki  REL1_20
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 
00073                 $this->verifyChunk();
00074                 // Create a local stash target
00075                 $this->mLocalFile = parent::stashFile();
00076                 // Update the initial file offset ( based on file size )
00077                 $this->mOffset = $this->mLocalFile->getSize();
00078                 $this->mFileKey = $this->mLocalFile->getFileKey();
00079 
00080                 // Output a copy of this first to chunk 0 location:
00081                 $status = $this->outputChunk( $this->mLocalFile->getPath() );
00082 
00083                 // Update db table to reflect initial "chunk" state
00084                 $this->updateChunkStatus();
00085                 return $this->mLocalFile;
00086         }
00087 
00091         public function continueChunks( $name, $key, $webRequestUpload ) {
00092                 $this->mFileKey = $key;
00093                 $this->mUpload = $webRequestUpload;
00094                 // Get the chunk status form the db:
00095                 $this->getChunkStatus();
00096 
00097                 $metadata = $this->stash->getMetadata( $key );
00098                 $this->initializePathInfo( $name,
00099                         $this->getRealPath( $metadata['us_path'] ),
00100                         $metadata['us_size'],
00101                         false
00102                 );
00103         }
00104 
00109         public function concatenateChunks() {
00110                 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
00111                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00112 
00113                 // Concatenate all the chunks to mVirtualTempPath
00114                 $fileList = Array();
00115                 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
00116                 for( $i = 0; $i <= $this->getChunkIndex(); $i++ ){
00117                         $fileList[] = $this->getVirtualChunkLocation( $i );
00118                 }
00119 
00120                 // Get the file extension from the last chunk
00121                 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
00122                 // Get a 0-byte temp file to perform the concatenation at
00123                 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
00124                 $tmpPath = $tmpFile
00125                         ? $tmpFile->getPath()
00126                         : false; // fail in concatenate()
00127                 // Concatenate the chunks at the temp file
00128                 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
00129                 if( !$status->isOk() ){
00130                         return $status;
00131                 }
00132 
00133                 $this->mTempPath = $tmpPath; // file system path
00134                 $this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously
00135                 $ret = $this->verifyUpload();
00136                 if ( $ret['status'] !== UploadBase::OK ) {
00137                         wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" );
00138                         $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) );
00139                         return $status;
00140                 }
00141 
00142                 // Update the mTempPath and mLocalFile
00143                 // ( for FileUpload or normal Stash to take over )
00144                 $this->mLocalFile = parent::stashFile();
00145 
00146                 return $status;
00147         }
00148 
00157         public function performUpload( $comment, $pageText, $watch, $user ) {
00158                 $rv = parent::performUpload( $comment, $pageText, $watch, $user );
00159                 return $rv;
00160         }
00161 
00167         function getVirtualChunkLocation( $index ){
00168                 return $this->repo->getVirtualUrl( 'temp' ) .
00169                                 '/' .
00170                                 $this->repo->getHashPath(
00171                                         $this->getChunkFileKey( $index )
00172                                 ) .
00173                                 $this->getChunkFileKey( $index );
00174         }
00175 
00184         public function addChunk( $chunkPath, $chunkSize, $offset ) {
00185                 // Get the offset before we add the chunk to the file system
00186                 $preAppendOffset = $this->getOffset();
00187 
00188                 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize()) {
00189                         $status = Status::newFatal( 'file-too-large' );
00190                 } else {
00191                         // Make sure the client is uploading the correct chunk with a matching offset.
00192                         if ( $preAppendOffset == $offset ) {
00193                                 // Update local chunk index for the current chunk
00194                                 $this->mChunkIndex++;
00195                                 try {
00196                                         # For some reason mTempPath is set to first part
00197                                         $oldTemp = $this->mTempPath;
00198                                         $this->mTempPath = $chunkPath;
00199                                         $this->verifyChunk();
00200                                         $this->mTempPath = $oldTemp;
00201                                 } catch ( UploadChunkVerificationException $e ) {
00202                                         return Status::newFatal( $e->getMessage() );
00203                                 }
00204                                 $status = $this->outputChunk( $chunkPath );
00205                                 if( $status->isGood() ){
00206                                         // Update local offset:
00207                                         $this->mOffset = $preAppendOffset + $chunkSize;
00208                                         // Update chunk table status db
00209                                         $this->updateChunkStatus();
00210                                 }
00211                         } else {
00212                                 $status = Status::newFatal( 'invalid-chunk-offset' );
00213                         }
00214                 }
00215                 return $status;
00216         }
00217 
00221         private function updateChunkStatus(){
00222                 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
00223                                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00224 
00225                 $dbw = $this->repo->getMasterDb();
00226                 $dbw->update(
00227                         'uploadstash',
00228                         array(
00229                                 'us_status' => 'chunks',
00230                                 'us_chunk_inx' => $this->getChunkIndex(),
00231                                 'us_size' => $this->getOffset()
00232                         ),
00233                         array( 'us_key' => $this->mFileKey ),
00234                         __METHOD__
00235                 );
00236         }
00237 
00241         private function getChunkStatus(){
00242                 // get Master db to avoid race conditions.
00243                 // Otherwise, if chunk upload time < replag there will be spurious errors
00244                 $dbw = $this->repo->getMasterDb();
00245                 $row = $dbw->selectRow(
00246                         'uploadstash',
00247                         array(
00248                                 'us_chunk_inx',
00249                                 'us_size',
00250                                 'us_path',
00251                         ),
00252                         array( 'us_key' => $this->mFileKey ),
00253                         __METHOD__
00254                 );
00255                 // Handle result:
00256                 if ( $row ) {
00257                         $this->mChunkIndex = $row->us_chunk_inx;
00258                         $this->mOffset = $row->us_size;
00259                         $this->mVirtualTempPath = $row->us_path;
00260                 }
00261         }
00262 
00267         private function getChunkIndex(){
00268                 if( $this->mChunkIndex !== null ){
00269                         return $this->mChunkIndex;
00270                 }
00271                 return 0;
00272         }
00273 
00278         private function getOffset(){
00279                 if ( $this->mOffset !== null ){
00280                         return $this->mOffset;
00281                 }
00282                 return 0;
00283         }
00284 
00292         private function outputChunk( $chunkPath ){
00293                 // Key is fileKey + chunk index
00294                 $fileKey = $this->getChunkFileKey();
00295 
00296                 // Store the chunk per its indexed fileKey:
00297                 $hashPath = $this->repo->getHashPath( $fileKey );
00298                 $storeStatus = $this->repo->quickImport( $chunkPath,
00299                         $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" );
00300 
00301                 // Check for error in stashing the chunk:
00302                 if ( ! $storeStatus->isOK() ) {
00303                         $error = $storeStatus->getErrorsArray();
00304                         $error = reset( $error );
00305                         if ( ! count( $error ) ) {
00306                                 $error = $storeStatus->getWarningsArray();
00307                                 $error = reset( $error );
00308                                 if ( ! count( $error ) ) {
00309                                         $error = array( 'unknown', 'no error recorded' );
00310                                 }
00311                         }
00312                         throw new UploadChunkFileException( "error storing file in '$chunkPath': " . implode( '; ', $error ) );
00313                 }
00314                 return $storeStatus;
00315         }
00316 
00317         private function getChunkFileKey( $index = null ){
00318                 if( $index === null ){
00319                         $index = $this->getChunkIndex();
00320                 }
00321                 return $this->mFileKey . '.' . $index ;
00322         }
00323 
00329         private function verifyChunk() {
00330                 // Rest mDesiredDestName here so we verify the name as if it were mFileKey
00331                 $oldDesiredDestName = $this->mDesiredDestName;
00332                 $this->mDesiredDestName = $this->mFileKey;
00333                 $this->mTitle = false;
00334                 $res = $this->verifyPartialFile();
00335                 $this->mDesiredDestName = $oldDesiredDestName;
00336                 $this->mTitle = false;
00337                 if( is_array( $res ) ) {
00338                         throw new UploadChunkVerificationException( $res[0] );
00339                 }
00340         }
00341 }
00342 
00343 class UploadChunkZeroLengthFileException extends MWException {};
00344 class UploadChunkFileException extends MWException {};
00345 class UploadChunkVerificationException extends MWException {};