[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/upload/ -> UploadFromChunks.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1