MediaWiki
master
|
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 {};