MediaWiki  REL1_19
UploadFromChunks.php
Go to the documentation of this file.
00001 <?php
00008 class UploadFromChunks extends UploadFromFile {
00009         protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath;
00010         
00018         public function __construct( $user = false, $stash = false, $repo = false ) {
00019                 // user object. sometimes this won't exist, as when running from cron.
00020                 $this->user = $user;
00021 
00022                 if( $repo ) {
00023                         $this->repo = $repo;
00024                 } else {
00025                         $this->repo = RepoGroup::singleton()->getLocalRepo();
00026                 }
00027 
00028                 if( $stash ) {
00029                         $this->stash = $stash;
00030                 } else {
00031                         if( $user ) {
00032                                 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" );
00033                         } else {
00034                                 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" );
00035                         }
00036                         $this->stash = new UploadStash( $this->repo, $this->user );
00037                 }
00038 
00039                 return true;
00040         }
00046         public function stashFile() {
00047                 // Stash file is the called on creating a new chunk session: 
00048                 $this->mChunkIndex = 0;
00049                 $this->mOffset = 0;
00050 
00051                 $this->verifyChunk();
00052                 // Create a local stash target
00053                 $this->mLocalFile = parent::stashFile();
00054                 // Update the initial file offset ( based on file size ) 
00055                 $this->mOffset = $this->mLocalFile->getSize();
00056                 $this->mFileKey = $this->mLocalFile->getFileKey();
00057 
00058                 // Output a copy of this first to chunk 0 location:
00059                 $status = $this->outputChunk( $this->mLocalFile->getPath() );
00060                 
00061                 // Update db table to reflect initial "chunk" state 
00062                 $this->updateChunkStatus();
00063                 return $this->mLocalFile;
00064         }
00065         
00069         public function continueChunks( $name, $key, $webRequestUpload ) {
00070                 $this->mFileKey = $key;
00071                 $this->mUpload = $webRequestUpload;
00072                 // Get the chunk status form the db: 
00073                 $this->getChunkStatus();
00074                 
00075                 $metadata = $this->stash->getMetadata( $key );
00076                 $this->initializePathInfo( $name,
00077                         $this->getRealPath( $metadata['us_path'] ),
00078                         $metadata['us_size'],
00079                         false
00080                 );
00081         }
00082         
00087         public function concatenateChunks() {
00088                 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . 
00089                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00090 
00091                 // Concatenate all the chunks to mVirtualTempPath
00092                 $fileList = Array();
00093                 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
00094                 for( $i = 0; $i <= $this->getChunkIndex(); $i++ ){
00095                         $fileList[] = $this->getVirtualChunkLocation( $i );
00096                 }
00097 
00098                 // Get the file extension from the last chunk
00099                 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
00100                 // Get a 0-byte temp file to perform the concatenation at
00101                 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
00102                 $tmpPath = $tmpFile
00103                         ? $tmpFile->getPath()
00104                         : false; // fail in concatenate()
00105                 // Concatenate the chunks at the temp file
00106                 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
00107                 if( !$status->isOk() ){
00108                         return $status; 
00109                 }
00110 
00111                 $this->mTempPath = $tmpPath; // file system path
00112                 $this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously
00113                 $ret = $this->verifyUpload();
00114                 if ( $ret['status'] !== UploadBase::OK ) {
00115                         wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" );
00116                         $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) );
00117                         return $status;
00118                 }
00119 
00120                 // Update the mTempPath and mLocalFile
00121                 // ( for FileUpload or normal Stash to take over )  
00122                 $this->mLocalFile = parent::stashFile();
00123 
00124                 return $status;
00125         }
00126 
00135         public function performUpload( $comment, $pageText, $watch, $user ) {
00136                 $rv = parent::performUpload( $comment, $pageText, $watch, $user );
00137                 return $rv;
00138         }
00139 
00144         function getVirtualChunkLocation( $index ){
00145                 return $this->repo->getVirtualUrl( 'temp' ) . 
00146                                 '/' .
00147                                 $this->repo->getHashPath( 
00148                                         $this->getChunkFileKey( $index )
00149                                 ) . 
00150                                 $this->getChunkFileKey( $index );
00151         }
00160         public function addChunk( $chunkPath, $chunkSize, $offset ) {
00161                 // Get the offset before we add the chunk to the file system
00162                 $preAppendOffset = $this->getOffset();
00163                 
00164                 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize()) {
00165                         $status = Status::newFatal( 'file-too-large' );
00166                 } else {
00167                         // Make sure the client is uploading the correct chunk with a matching offset.
00168                         if ( $preAppendOffset == $offset ) {
00169                                 // Update local chunk index for the current chunk   
00170                                 $this->mChunkIndex++;
00171                                 try {
00172                                         # For some reason mTempPath is set to first part
00173                                         $oldTemp = $this->mTempPath;
00174                                         $this->mTempPath = $chunkPath;
00175                                         $this->verifyChunk();
00176                                         $this->mTempPath = $oldTemp;
00177                                 } catch ( UploadChunkVerificationException $e ) {
00178                                         return Status::newFatal( $e->getMessage() );
00179                                 }
00180                                 $status = $this->outputChunk( $chunkPath );
00181                                 if( $status->isGood() ){
00182                                         // Update local offset: 
00183                                         $this->mOffset = $preAppendOffset + $chunkSize;
00184                                         // Update chunk table status db         
00185                                         $this->updateChunkStatus();             
00186                                 }
00187                         } else {
00188                                 $status = Status::newFatal( 'invalid-chunk-offset' );
00189                         }
00190                 }
00191                 return $status;
00192         }
00193         
00197         private function updateChunkStatus(){
00198                 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . 
00199                                         $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
00200                 
00201                 $dbw = $this->repo->getMasterDb();
00202                 $dbw->update(
00203                         'uploadstash',
00204                         array( 
00205                                 'us_status' => 'chunks',
00206                                 'us_chunk_inx' => $this->getChunkIndex(),
00207                                 'us_size' => $this->getOffset()
00208                         ),
00209                         array( 'us_key' => $this->mFileKey ),
00210                         __METHOD__
00211                 );
00212         }
00216         private function getChunkStatus(){
00217                 // get Master db to avoid race conditions. 
00218                 // Otherwise, if chunk upload time < replag there will be spurious errors
00219                 $dbw = $this->repo->getMasterDb();
00220                 $row = $dbw->selectRow(
00221                         'uploadstash', 
00222                         array( 
00223                                 'us_chunk_inx',
00224                                 'us_size',
00225                                 'us_path',
00226                         ),
00227                         array( 'us_key' => $this->mFileKey ),
00228                         __METHOD__
00229                 );
00230                 // Handle result:
00231                 if ( $row ) {
00232                         $this->mChunkIndex = $row->us_chunk_inx;
00233                         $this->mOffset = $row->us_size;
00234                         $this->mVirtualTempPath = $row->us_path;
00235                 }
00236         }
00241         private function getChunkIndex(){
00242                 if( $this->mChunkIndex !== null ){
00243                         return $this->mChunkIndex;
00244                 }
00245                 return 0;
00246         }
00247         
00252         private function getOffset(){
00253                 if ( $this->mOffset !== null ){
00254                         return $this->mOffset;
00255                 }
00256                 return 0;
00257         }
00258         
00264         private function outputChunk( $chunkPath ){
00265                 // Key is fileKey + chunk index
00266                 $fileKey = $this->getChunkFileKey();
00267                 
00268                 // Store the chunk per its indexed fileKey: 
00269                 $hashPath = $this->repo->getHashPath( $fileKey );
00270                 $storeStatus = $this->repo->store( $chunkPath, 'temp', "$hashPath$fileKey" );
00271                 
00272                 // Check for error in stashing the chunk:
00273                 if ( ! $storeStatus->isOK() ) {
00274                         $error = $storeStatus->getErrorsArray();
00275                         $error = reset( $error );
00276                         if ( ! count( $error ) ) {
00277                                 $error = $storeStatus->getWarningsArray();
00278                                 $error = reset( $error );
00279                                 if ( ! count( $error ) ) {
00280                                         $error = array( 'unknown', 'no error recorded' );
00281                                 }
00282                         }
00283                         throw new UploadChunkFileException( "error storing file in '$chunkPath': " . implode( '; ', $error ) );
00284                 }
00285                 return $storeStatus;
00286         }
00287         private function getChunkFileKey( $index = null ){
00288                 if( $index === null ){
00289                         $index = $this->getChunkIndex();
00290                 }
00291                 return $this->mFileKey . '.' . $index ;
00292         }
00293 
00299         private function verifyChunk() {
00300                 // Rest mDesiredDestName here so we verify the name as if it were mFileKey
00301                 $oldDesiredDestName = $this->mDesiredDestName;
00302                 $this->mDesiredDestName = $this->mFileKey;
00303                 $this->mTitle = false;
00304                 $res = $this->verifyPartialFile();
00305                 $this->mDesiredDestName = $oldDesiredDestName;
00306                 $this->mTitle = false;
00307                 if( is_array( $res ) ) {
00308                         throw new UploadChunkVerificationException( $res[0] );
00309                 }
00310         }
00311 }
00312 
00313 class UploadChunkZeroLengthFileException extends MWException {};
00314 class UploadChunkFileException extends MWException {};
00315 class UploadChunkVerificationException extends MWException {};