MediaWiki
REL1_23
|
00001 <?php 00030 class UploadFromChunks extends UploadFromFile { 00031 protected $mOffset; 00032 protected $mChunkIndex; 00033 protected $mFileKey; 00034 protected $mVirtualTempPath; 00035 00043 public function __construct( $user = null, $stash = false, $repo = false ) { 00044 // user object. sometimes this won't exist, as when running from cron. 00045 $this->user = $user; 00046 00047 if ( $repo ) { 00048 $this->repo = $repo; 00049 } else { 00050 $this->repo = RepoGroup::singleton()->getLocalRepo(); 00051 } 00052 00053 if ( $stash ) { 00054 $this->stash = $stash; 00055 } else { 00056 if ( $user ) { 00057 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" ); 00058 } else { 00059 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" ); 00060 } 00061 $this->stash = new UploadStash( $this->repo, $this->user ); 00062 } 00063 00064 return true; 00065 } 00066 00072 public function stashFile( User $user = null ) { 00073 // Stash file is the called on creating a new chunk session: 00074 $this->mChunkIndex = 0; 00075 $this->mOffset = 0; 00076 00077 $this->verifyChunk(); 00078 // Create a local stash target 00079 $this->mLocalFile = parent::stashFile(); 00080 // Update the initial file offset (based on file size) 00081 $this->mOffset = $this->mLocalFile->getSize(); 00082 $this->mFileKey = $this->mLocalFile->getFileKey(); 00083 00084 // Output a copy of this first to chunk 0 location: 00085 $this->outputChunk( $this->mLocalFile->getPath() ); 00086 00087 // Update db table to reflect initial "chunk" state 00088 $this->updateChunkStatus(); 00089 return $this->mLocalFile; 00090 } 00091 00095 public function continueChunks( $name, $key, $webRequestUpload ) { 00096 $this->mFileKey = $key; 00097 $this->mUpload = $webRequestUpload; 00098 // Get the chunk status form the db: 00099 $this->getChunkStatus(); 00100 00101 $metadata = $this->stash->getMetadata( $key ); 00102 $this->initializePathInfo( $name, 00103 $this->getRealPath( $metadata['us_path'] ), 00104 $metadata['us_size'], 00105 false 00106 ); 00107 } 00108 00113 public function concatenateChunks() { 00114 $chunkIndex = $this->getChunkIndex(); 00115 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . 00116 $this->getOffset() . ' inx:' . $chunkIndex . "\n" ); 00117 00118 // Concatenate all the chunks to mVirtualTempPath 00119 $fileList = array(); 00120 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1" 00121 for ( $i = 0; $i <= $chunkIndex; $i++ ) { 00122 $fileList[] = $this->getVirtualChunkLocation( $i ); 00123 } 00124 00125 // Get the file extension from the last chunk 00126 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); 00127 // Get a 0-byte temp file to perform the concatenation at 00128 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext ); 00129 $tmpPath = false; // fail in concatenate() 00130 if ( $tmpFile ) { 00131 // keep alive with $this 00132 $tmpPath = $tmpFile->bind( $this )->getPath(); 00133 } 00134 00135 // Concatenate the chunks at the temp file 00136 $tStart = microtime( true ); 00137 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); 00138 $tAmount = microtime( true ) - $tStart; 00139 if ( !$status->isOk() ) { 00140 return $status; 00141 } 00142 wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds." ); 00143 00144 // File system path 00145 $this->mTempPath = $tmpPath; 00146 // Since this was set for the last chunk previously 00147 $this->mFileSize = filesize( $this->mTempPath ); 00148 $ret = $this->verifyUpload(); 00149 if ( $ret['status'] !== UploadBase::OK ) { 00150 wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); 00151 $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); 00152 return $status; 00153 } 00154 00155 // Update the mTempPath and mLocalFile 00156 // (for FileUpload or normal Stash to take over) 00157 $tStart = microtime( true ); 00158 $this->mLocalFile = parent::stashFile( $this->user ); 00159 $tAmount = microtime( true ) - $tStart; 00160 $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo()) 00161 wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." ); 00162 00163 return $status; 00164 } 00165 00174 public function performUpload( $comment, $pageText, $watch, $user ) { 00175 $rv = parent::performUpload( $comment, $pageText, $watch, $user ); 00176 return $rv; 00177 } 00178 00184 function getVirtualChunkLocation( $index ) { 00185 return $this->repo->getVirtualUrl( 'temp' ) . 00186 '/' . 00187 $this->repo->getHashPath( 00188 $this->getChunkFileKey( $index ) 00189 ) . 00190 $this->getChunkFileKey( $index ); 00191 } 00192 00201 public function addChunk( $chunkPath, $chunkSize, $offset ) { 00202 // Get the offset before we add the chunk to the file system 00203 $preAppendOffset = $this->getOffset(); 00204 00205 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) { 00206 $status = Status::newFatal( 'file-too-large' ); 00207 } else { 00208 // Make sure the client is uploading the correct chunk with a matching offset. 00209 if ( $preAppendOffset == $offset ) { 00210 // Update local chunk index for the current chunk 00211 $this->mChunkIndex++; 00212 try { 00213 # For some reason mTempPath is set to first part 00214 $oldTemp = $this->mTempPath; 00215 $this->mTempPath = $chunkPath; 00216 $this->verifyChunk(); 00217 $this->mTempPath = $oldTemp; 00218 } catch ( UploadChunkVerificationException $e ) { 00219 return Status::newFatal( $e->getMessage() ); 00220 } 00221 $status = $this->outputChunk( $chunkPath ); 00222 if ( $status->isGood() ) { 00223 // Update local offset: 00224 $this->mOffset = $preAppendOffset + $chunkSize; 00225 // Update chunk table status db 00226 $this->updateChunkStatus(); 00227 } 00228 } else { 00229 $status = Status::newFatal( 'invalid-chunk-offset' ); 00230 } 00231 } 00232 return $status; 00233 } 00234 00238 private function updateChunkStatus() { 00239 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . 00240 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); 00241 00242 $dbw = $this->repo->getMasterDb(); 00243 // Use a quick transaction since we will upload the full temp file into shared 00244 // storage, which takes time for large files. We don't want to hold locks then. 00245 $dbw->begin( __METHOD__ ); 00246 $dbw->update( 00247 'uploadstash', 00248 array( 00249 'us_status' => 'chunks', 00250 'us_chunk_inx' => $this->getChunkIndex(), 00251 'us_size' => $this->getOffset() 00252 ), 00253 array( 'us_key' => $this->mFileKey ), 00254 __METHOD__ 00255 ); 00256 $dbw->commit( __METHOD__ ); 00257 } 00258 00262 private function getChunkStatus() { 00263 // get Master db to avoid race conditions. 00264 // Otherwise, if chunk upload time < replag there will be spurious errors 00265 $dbw = $this->repo->getMasterDb(); 00266 $row = $dbw->selectRow( 00267 'uploadstash', 00268 array( 00269 'us_chunk_inx', 00270 'us_size', 00271 'us_path', 00272 ), 00273 array( 'us_key' => $this->mFileKey ), 00274 __METHOD__ 00275 ); 00276 // Handle result: 00277 if ( $row ) { 00278 $this->mChunkIndex = $row->us_chunk_inx; 00279 $this->mOffset = $row->us_size; 00280 $this->mVirtualTempPath = $row->us_path; 00281 } 00282 } 00283 00288 private function getChunkIndex() { 00289 if ( $this->mChunkIndex !== null ) { 00290 return $this->mChunkIndex; 00291 } 00292 return 0; 00293 } 00294 00299 private function getOffset() { 00300 if ( $this->mOffset !== null ) { 00301 return $this->mOffset; 00302 } 00303 return 0; 00304 } 00305 00313 private function outputChunk( $chunkPath ) { 00314 // Key is fileKey + chunk index 00315 $fileKey = $this->getChunkFileKey(); 00316 00317 // Store the chunk per its indexed fileKey: 00318 $hashPath = $this->repo->getHashPath( $fileKey ); 00319 $storeStatus = $this->repo->quickImport( $chunkPath, 00320 $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); 00321 00322 // Check for error in stashing the chunk: 00323 if ( ! $storeStatus->isOK() ) { 00324 $error = $storeStatus->getErrorsArray(); 00325 $error = reset( $error ); 00326 if ( ! count( $error ) ) { 00327 $error = $storeStatus->getWarningsArray(); 00328 $error = reset( $error ); 00329 if ( ! count( $error ) ) { 00330 $error = array( 'unknown', 'no error recorded' ); 00331 } 00332 } 00333 throw new UploadChunkFileException( "Error storing file in '$chunkPath': " . 00334 implode( '; ', $error ) ); 00335 } 00336 return $storeStatus; 00337 } 00338 00339 private function getChunkFileKey( $index = null ) { 00340 if ( $index === null ) { 00341 $index = $this->getChunkIndex(); 00342 } 00343 return $this->mFileKey . '.' . $index; 00344 } 00345 00351 private function verifyChunk() { 00352 // Rest mDesiredDestName here so we verify the name as if it were mFileKey 00353 $oldDesiredDestName = $this->mDesiredDestName; 00354 $this->mDesiredDestName = $this->mFileKey; 00355 $this->mTitle = false; 00356 $res = $this->verifyPartialFile(); 00357 $this->mDesiredDestName = $oldDesiredDestName; 00358 $this->mTitle = false; 00359 if ( is_array( $res ) ) { 00360 throw new UploadChunkVerificationException( $res[0] ); 00361 } 00362 } 00363 } 00364 00365 class UploadChunkZeroLengthFileException extends MWException { 00366 } 00367 00368 class UploadChunkFileException extends MWException { 00369 } 00370 00371 class UploadChunkVerificationException extends MWException { 00372 }