[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Backend for uploading files from chunks. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Upload 22 */ 23 24 /** 25 * Implements uploading from chunks 26 * 27 * @ingroup Upload 28 * @author Michael Dale 29 */ 30 class UploadFromChunks extends UploadFromFile { 31 protected $mOffset; 32 protected $mChunkIndex; 33 protected $mFileKey; 34 protected $mVirtualTempPath; 35 /** @var LocalRepo */ 36 private $repo; 37 38 /** 39 * Setup local pointers to stash, repo and user (similar to UploadFromStash) 40 * 41 * @param User|null $user Default: null 42 * @param UploadStash|bool $stash Default: false 43 * @param FileRepo|bool $repo Default: false 44 */ 45 public function __construct( $user = null, $stash = false, $repo = false ) { 46 // user object. sometimes this won't exist, as when running from cron. 47 $this->user = $user; 48 49 if ( $repo ) { 50 $this->repo = $repo; 51 } else { 52 $this->repo = RepoGroup::singleton()->getLocalRepo(); 53 } 54 55 if ( $stash ) { 56 $this->stash = $stash; 57 } else { 58 if ( $user ) { 59 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" ); 60 } else { 61 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" ); 62 } 63 $this->stash = new UploadStash( $this->repo, $this->user ); 64 } 65 } 66 67 /** 68 * Calls the parent stashFile and updates the uploadsession table to handle "chunks" 69 * 70 * @param User|null $user 71 * @return UploadStashFile Stashed file 72 */ 73 public function stashFile( User $user = null ) { 74 // Stash file is the called on creating a new chunk session: 75 $this->mChunkIndex = 0; 76 $this->mOffset = 0; 77 78 $this->verifyChunk(); 79 // Create a local stash target 80 $this->mLocalFile = parent::stashFile(); 81 // Update the initial file offset (based on file size) 82 $this->mOffset = $this->mLocalFile->getSize(); 83 $this->mFileKey = $this->mLocalFile->getFileKey(); 84 85 // Output a copy of this first to chunk 0 location: 86 $this->outputChunk( $this->mLocalFile->getPath() ); 87 88 // Update db table to reflect initial "chunk" state 89 $this->updateChunkStatus(); 90 91 return $this->mLocalFile; 92 } 93 94 /** 95 * Continue chunk uploading 96 * 97 * @param string $name 98 * @param string $key 99 * @param WebRequestUpload $webRequestUpload 100 */ 101 public function continueChunks( $name, $key, $webRequestUpload ) { 102 $this->mFileKey = $key; 103 $this->mUpload = $webRequestUpload; 104 // Get the chunk status form the db: 105 $this->getChunkStatus(); 106 107 $metadata = $this->stash->getMetadata( $key ); 108 $this->initializePathInfo( $name, 109 $this->getRealPath( $metadata['us_path'] ), 110 $metadata['us_size'], 111 false 112 ); 113 } 114 115 /** 116 * Append the final chunk and ready file for parent::performUpload() 117 * @return FileRepoStatus 118 */ 119 public function concatenateChunks() { 120 $chunkIndex = $this->getChunkIndex(); 121 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . 122 $this->getOffset() . ' inx:' . $chunkIndex . "\n" ); 123 124 // Concatenate all the chunks to mVirtualTempPath 125 $fileList = array(); 126 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1" 127 for ( $i = 0; $i <= $chunkIndex; $i++ ) { 128 $fileList[] = $this->getVirtualChunkLocation( $i ); 129 } 130 131 // Get the file extension from the last chunk 132 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); 133 // Get a 0-byte temp file to perform the concatenation at 134 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext ); 135 $tmpPath = false; // fail in concatenate() 136 if ( $tmpFile ) { 137 // keep alive with $this 138 $tmpPath = $tmpFile->bind( $this )->getPath(); 139 } 140 141 // Concatenate the chunks at the temp file 142 $tStart = microtime( true ); 143 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); 144 $tAmount = microtime( true ) - $tStart; 145 if ( !$status->isOk() ) { 146 return $status; 147 } 148 wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds." ); 149 150 // File system path 151 $this->mTempPath = $tmpPath; 152 // Since this was set for the last chunk previously 153 $this->mFileSize = filesize( $this->mTempPath ); 154 $ret = $this->verifyUpload(); 155 if ( $ret['status'] !== UploadBase::OK ) { 156 wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); 157 $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); 158 159 return $status; 160 } 161 162 // Update the mTempPath and mLocalFile 163 // (for FileUpload or normal Stash to take over) 164 $tStart = microtime( true ); 165 $this->mLocalFile = parent::stashFile( $this->user ); 166 $tAmount = microtime( true ) - $tStart; 167 $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo()) 168 wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." ); 169 170 return $status; 171 } 172 173 /** 174 * Perform the upload, then remove the temp copy afterward 175 * @param string $comment 176 * @param string $pageText 177 * @param bool $watch 178 * @param User $user 179 * @return Status 180 */ 181 public function performUpload( $comment, $pageText, $watch, $user ) { 182 $rv = parent::performUpload( $comment, $pageText, $watch, $user ); 183 184 return $rv; 185 } 186 187 /** 188 * Returns the virtual chunk location: 189 * @param int $index 190 * @return string 191 */ 192 function getVirtualChunkLocation( $index ) { 193 return $this->repo->getVirtualUrl( 'temp' ) . 194 '/' . 195 $this->repo->getHashPath( 196 $this->getChunkFileKey( $index ) 197 ) . 198 $this->getChunkFileKey( $index ); 199 } 200 201 /** 202 * Add a chunk to the temporary directory 203 * 204 * @param string $chunkPath Path to temporary chunk file 205 * @param int $chunkSize Size of the current chunk 206 * @param int $offset Offset of current chunk ( mutch match database chunk offset ) 207 * @return Status 208 */ 209 public function addChunk( $chunkPath, $chunkSize, $offset ) { 210 // Get the offset before we add the chunk to the file system 211 $preAppendOffset = $this->getOffset(); 212 213 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) { 214 $status = Status::newFatal( 'file-too-large' ); 215 } else { 216 // Make sure the client is uploading the correct chunk with a matching offset. 217 if ( $preAppendOffset == $offset ) { 218 // Update local chunk index for the current chunk 219 $this->mChunkIndex++; 220 try { 221 # For some reason mTempPath is set to first part 222 $oldTemp = $this->mTempPath; 223 $this->mTempPath = $chunkPath; 224 $this->verifyChunk(); 225 $this->mTempPath = $oldTemp; 226 } catch ( UploadChunkVerificationException $e ) { 227 return Status::newFatal( $e->getMessage() ); 228 } 229 $status = $this->outputChunk( $chunkPath ); 230 if ( $status->isGood() ) { 231 // Update local offset: 232 $this->mOffset = $preAppendOffset + $chunkSize; 233 // Update chunk table status db 234 $this->updateChunkStatus(); 235 } 236 } else { 237 $status = Status::newFatal( 'invalid-chunk-offset' ); 238 } 239 } 240 241 return $status; 242 } 243 244 /** 245 * Update the chunk db table with the current status: 246 */ 247 private function updateChunkStatus() { 248 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . 249 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); 250 251 $dbw = $this->repo->getMasterDb(); 252 // Use a quick transaction since we will upload the full temp file into shared 253 // storage, which takes time for large files. We don't want to hold locks then. 254 $dbw->begin( __METHOD__ ); 255 $dbw->update( 256 'uploadstash', 257 array( 258 'us_status' => 'chunks', 259 'us_chunk_inx' => $this->getChunkIndex(), 260 'us_size' => $this->getOffset() 261 ), 262 array( 'us_key' => $this->mFileKey ), 263 __METHOD__ 264 ); 265 $dbw->commit( __METHOD__ ); 266 } 267 268 /** 269 * Get the chunk db state and populate update relevant local values 270 */ 271 private function getChunkStatus() { 272 // get Master db to avoid race conditions. 273 // Otherwise, if chunk upload time < replag there will be spurious errors 274 $dbw = $this->repo->getMasterDb(); 275 $row = $dbw->selectRow( 276 'uploadstash', 277 array( 278 'us_chunk_inx', 279 'us_size', 280 'us_path', 281 ), 282 array( 'us_key' => $this->mFileKey ), 283 __METHOD__ 284 ); 285 // Handle result: 286 if ( $row ) { 287 $this->mChunkIndex = $row->us_chunk_inx; 288 $this->mOffset = $row->us_size; 289 $this->mVirtualTempPath = $row->us_path; 290 } 291 } 292 293 /** 294 * Get the current Chunk index 295 * @return int Index of the current chunk 296 */ 297 private function getChunkIndex() { 298 if ( $this->mChunkIndex !== null ) { 299 return $this->mChunkIndex; 300 } 301 302 return 0; 303 } 304 305 /** 306 * Gets the current offset in fromt the stashedupload table 307 * @return int Current byte offset of the chunk file set 308 */ 309 private function getOffset() { 310 if ( $this->mOffset !== null ) { 311 return $this->mOffset; 312 } 313 314 return 0; 315 } 316 317 /** 318 * Output the chunk to disk 319 * 320 * @param string $chunkPath 321 * @throws UploadChunkFileException 322 * @return FileRepoStatus 323 */ 324 private function outputChunk( $chunkPath ) { 325 // Key is fileKey + chunk index 326 $fileKey = $this->getChunkFileKey(); 327 328 // Store the chunk per its indexed fileKey: 329 $hashPath = $this->repo->getHashPath( $fileKey ); 330 $storeStatus = $this->repo->quickImport( $chunkPath, 331 $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); 332 333 // Check for error in stashing the chunk: 334 if ( !$storeStatus->isOK() ) { 335 $error = $storeStatus->getErrorsArray(); 336 $error = reset( $error ); 337 if ( !count( $error ) ) { 338 $error = $storeStatus->getWarningsArray(); 339 $error = reset( $error ); 340 if ( !count( $error ) ) { 341 $error = array( 'unknown', 'no error recorded' ); 342 } 343 } 344 throw new UploadChunkFileException( "Error storing file in '$chunkPath': " . 345 implode( '; ', $error ) ); 346 } 347 348 return $storeStatus; 349 } 350 351 private function getChunkFileKey( $index = null ) { 352 if ( $index === null ) { 353 $index = $this->getChunkIndex(); 354 } 355 356 return $this->mFileKey . '.' . $index; 357 } 358 359 /** 360 * Verify that the chunk isn't really an evil html file 361 * 362 * @throws UploadChunkVerificationException 363 */ 364 private function verifyChunk() { 365 // Rest mDesiredDestName here so we verify the name as if it were mFileKey 366 $oldDesiredDestName = $this->mDesiredDestName; 367 $this->mDesiredDestName = $this->mFileKey; 368 $this->mTitle = false; 369 $res = $this->verifyPartialFile(); 370 $this->mDesiredDestName = $oldDesiredDestName; 371 $this->mTitle = false; 372 if ( is_array( $res ) ) { 373 throw new UploadChunkVerificationException( $res[0] ); 374 } 375 } 376 } 377 378 class UploadChunkZeroLengthFileException extends MWException { 379 } 380 381 class UploadChunkFileException extends MWException { 382 } 383 384 class UploadChunkVerificationException extends MWException { 385 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |