MediaWiki  master
UploadFromChunks.php
Go to the documentation of this file.
1 <?php
2 
5 
36  private $repo;
38  public $stash;
40  public $user;
41 
42  protected $mOffset;
43  protected $mChunkIndex;
44  protected $mFileKey;
45  protected $mVirtualTempPath;
46 
56  public function __construct( User $user, $stash = false, $repo = false ) {
57  $this->user = $user;
58 
59  if ( $repo ) {
60  $this->repo = $repo;
61  } else {
62  $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
63  }
64 
65  if ( $stash ) {
66  $this->stash = $stash;
67  } else {
68  wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() );
69  $this->stash = new UploadStash( $this->repo, $this->user );
70  }
71  }
72 
76  public function tryStashFile( User $user, $isPartial = false ) {
77  try {
78  $this->verifyChunk();
79  } catch ( UploadChunkVerificationException $e ) {
80  return Status::newFatal( $e->msg );
81  }
82 
83  return parent::tryStashFile( $user, $isPartial );
84  }
85 
92  protected function doStashFile( User $user = null ) {
93  // Stash file is the called on creating a new chunk session:
94  $this->mChunkIndex = 0;
95  $this->mOffset = 0;
96 
97  // Create a local stash target
98  $this->mStashFile = parent::doStashFile( $user );
99  // Update the initial file offset (based on file size)
100  $this->mOffset = $this->mStashFile->getSize();
101  $this->mFileKey = $this->mStashFile->getFileKey();
102 
103  // Output a copy of this first to chunk 0 location:
104  $this->outputChunk( $this->mStashFile->getPath() );
105 
106  // Update db table to reflect initial "chunk" state
107  $this->updateChunkStatus();
108 
109  return $this->mStashFile;
110  }
111 
119  public function continueChunks( $name, $key, $webRequestUpload ) {
120  $this->mFileKey = $key;
121  $this->mUpload = $webRequestUpload;
122  // Get the chunk status form the db:
123  $this->getChunkStatus();
124 
125  $metadata = $this->stash->getMetadata( $key );
126  $this->initializePathInfo( $name,
127  $this->getRealPath( $metadata['us_path'] ),
128  $metadata['us_size'],
129  false
130  );
131  }
132 
137  public function concatenateChunks() {
138  $chunkIndex = $this->getChunkIndex();
139  wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
140  $this->getOffset() . ' inx:' . $chunkIndex );
141 
142  // Concatenate all the chunks to mVirtualTempPath
143  $fileList = [];
144  // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
145  for ( $i = 0; $i <= $chunkIndex; $i++ ) {
146  $fileList[] = $this->getVirtualChunkLocation( $i );
147  }
148 
149  // Get the file extension from the last chunk
150  $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
151  // Get a 0-byte temp file to perform the concatenation at
152  $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
153  ->newTempFSFile( 'chunkedupload_', $ext );
154  $tmpPath = false; // fail in concatenate()
155  if ( $tmpFile ) {
156  // keep alive with $this
157  $tmpPath = $tmpFile->bind( $this )->getPath();
158  }
159 
160  // Concatenate the chunks at the temp file
161  $tStart = microtime( true );
162  $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
163  $tAmount = microtime( true ) - $tStart;
164  if ( !$status->isOK() ) {
165  return $status;
166  }
167 
168  wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds." );
169 
170  // File system path of the actual full temp file
171  $this->setTempFile( $tmpPath );
172 
173  $ret = $this->verifyUpload();
174  if ( $ret['status'] !== UploadBase::OK ) {
175  wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" );
176  $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) );
177 
178  return $status;
179  }
180 
181  // Update the mTempPath and mStashFile
182  // (for FileUpload or normal Stash to take over)
183  $tStart = microtime( true );
184  // This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
185  // override doStashFile() with completely different functionality in this class...
186  $error = $this->runUploadStashFileHook( $this->user );
187  if ( $error ) {
188  $status->fatal( ...$error );
189  return $status;
190  }
191  try {
192  $this->mStashFile = parent::doStashFile( $this->user );
193  } catch ( UploadStashException $e ) {
194  $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
195  return $status;
196  }
197 
198  $tAmount = microtime( true ) - $tStart;
199  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable tmpFile is set when tmpPath is set here
200  $this->mStashFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
201  wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
202 
203  return $status;
204  }
205 
211  private function getVirtualChunkLocation( $index ) {
212  return $this->repo->getVirtualUrl( 'temp' ) .
213  '/' .
214  $this->repo->getHashPath(
215  $this->getChunkFileKey( $index )
216  ) .
217  $this->getChunkFileKey( $index );
218  }
219 
228  public function addChunk( $chunkPath, $chunkSize, $offset ) {
229  // Get the offset before we add the chunk to the file system
230  $preAppendOffset = $this->getOffset();
231 
232  if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) {
233  $status = Status::newFatal( 'file-too-large' );
234  } else {
235  // Make sure the client is uploading the correct chunk with a matching offset.
236  if ( $preAppendOffset == $offset ) {
237  // Update local chunk index for the current chunk
238  $this->mChunkIndex++;
239  try {
240  # For some reason mTempPath is set to first part
241  $oldTemp = $this->mTempPath;
242  $this->mTempPath = $chunkPath;
243  $this->verifyChunk();
244  $this->mTempPath = $oldTemp;
245  } catch ( UploadChunkVerificationException $e ) {
246  return Status::newFatal( $e->msg );
247  }
248  $status = $this->outputChunk( $chunkPath );
249  if ( $status->isGood() ) {
250  // Update local offset:
251  $this->mOffset = $preAppendOffset + $chunkSize;
252  // Update chunk table status db
253  $this->updateChunkStatus();
254  }
255  } else {
256  $status = Status::newFatal( 'invalid-chunk-offset' );
257  }
258  }
259 
260  return $status;
261  }
262 
266  private function updateChunkStatus() {
267  wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
268  $this->getOffset() . ' inx:' . $this->getChunkIndex() );
269 
270  $dbw = $this->repo->getPrimaryDB();
271  $dbw->update(
272  'uploadstash',
273  [
274  'us_status' => 'chunks',
275  'us_chunk_inx' => $this->getChunkIndex(),
276  'us_size' => $this->getOffset()
277  ],
278  [ 'us_key' => $this->mFileKey ],
279  __METHOD__
280  );
281  }
282 
286  private function getChunkStatus() {
287  // get primary db to avoid race conditions.
288  // Otherwise, if chunk upload time < replag there will be spurious errors
289  $dbw = $this->repo->getPrimaryDB();
290  $row = $dbw->selectRow(
291  'uploadstash',
292  [
293  'us_chunk_inx',
294  'us_size',
295  'us_path',
296  ],
297  [ 'us_key' => $this->mFileKey ],
298  __METHOD__
299  );
300  // Handle result:
301  if ( $row ) {
302  $this->mChunkIndex = $row->us_chunk_inx;
303  $this->mOffset = $row->us_size;
304  $this->mVirtualTempPath = $row->us_path;
305  }
306  }
307 
312  private function getChunkIndex() {
313  if ( $this->mChunkIndex !== null ) {
314  return $this->mChunkIndex;
315  }
316 
317  return 0;
318  }
319 
324  public function getOffset() {
325  if ( $this->mOffset !== null ) {
326  return $this->mOffset;
327  }
328 
329  return 0;
330  }
331 
339  private function outputChunk( $chunkPath ) {
340  // Key is fileKey + chunk index
341  $fileKey = $this->getChunkFileKey();
342 
343  // Store the chunk per its indexed fileKey:
344  $hashPath = $this->repo->getHashPath( $fileKey );
345  $storeStatus = $this->repo->quickImport( $chunkPath,
346  $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" );
347 
348  // Check for error in stashing the chunk:
349  if ( !$storeStatus->isOK() ) {
350  $error = $storeStatus->getErrorsArray();
351  $error = reset( $error );
352  if ( !count( $error ) ) {
353  $error = $storeStatus->getWarningsArray();
354  $error = reset( $error );
355  if ( !count( $error ) ) {
356  $error = [ 'unknown', 'no error recorded' ];
357  }
358  }
359  throw new UploadChunkFileException( "Error storing file in '{chunkPath}': " .
360  implode( '; ', $error ), [ 'chunkPath' => $chunkPath ] );
361  }
362 
363  return $storeStatus;
364  }
365 
366  private function getChunkFileKey( $index = null ) {
367  return $this->mFileKey . '.' . ( $index ?? $this->getChunkIndex() );
368  }
369 
375  private function verifyChunk() {
376  // Rest mDesiredDestName here so we verify the name as if it were mFileKey
377  $oldDesiredDestName = $this->mDesiredDestName;
378  $this->mDesiredDestName = $this->mFileKey;
379  $this->mTitle = false;
380  $res = $this->verifyPartialFile();
381  $this->mDesiredDestName = $oldDesiredDestName;
382  $this->mTitle = false;
383  if ( is_array( $res ) ) {
385  }
386  }
387 }
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
const DELETE_SOURCE
Definition: FileRepo.php:49
msg( $key, $fallback,... $params)
Get a message from i18n.
Definition: MWException.php:85
Service locator for MediaWiki core services.
Object to access the $_FILES array.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
UploadStashFile null $mStashFile
Definition: UploadBase.php:77
getRealPath( $srcPath)
Definition: UploadBase.php:339
runUploadStashFileHook(User $user)
verifyPartialFile()
A verification routine suitable for partial files.
Definition: UploadBase.php:537
getVerificationErrorCode( $error)
Definition: UploadBase.php:125
string null $mDesiredDestName
Definition: UploadBase.php:59
setTempFile( $tempPath, $fileSize=null)
Definition: UploadBase.php:288
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
Definition: UploadBase.php:267
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
string null $mTempPath
Local file system path to the file to upload (or a local copy)
Definition: UploadBase.php:55
Implements uploading from chunks.
addChunk( $chunkPath, $chunkSize, $offset)
Add a chunk to the temporary directory.
doStashFile(User $user=null)
Calls the parent doStashFile and updates the uploadsession table to handle "chunks".
continueChunks( $name, $key, $webRequestUpload)
Continue chunk uploading.
tryStashFile(User $user, $isPartial=false)
Like stashFile(), but respects extensions' wishes to prevent the stashing.verifyUpload() must be call...
__construct(User $user, $stash=false, $repo=false)
@noinspection PhpMissingParentConstructorInspection
getOffset()
Get the offset at which the next uploaded chunk will be appended to.
concatenateChunks()
Append the final chunk and ready file for parent::performUpload()
Implements regular file uploads.
UploadStash is intended to accomplish a few things:
Definition: UploadStash.php:57
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:71
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition: User.php:1651
if(!is_readable( $file)) $ext
Definition: router.php:48