10use Psr\Log\LoggerInterface;
58 private LoggerInterface $logger;
75 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
79 $this->stash = $stash;
81 wfDebug( __METHOD__ .
" creating new UploadFromChunks instance for " . $user->
getId() );
82 $this->stash =
new UploadStash( $this->repo, $this->user );
85 $this->logger = LoggerFactory::getInstance(
'upload' );
95 return Status::newFatal( $e->msg );
98 return parent::tryStashFile( $user, $isPartial );
109 $this->mChunkIndex = 0;
113 $this->mStashFile = parent::doStashFile( $user );
115 $this->mOffset = $this->mStashFile->getSize();
116 $this->mFileKey = $this->mStashFile->getFileKey();
119 $this->outputChunk( $this->mStashFile->getPath() );
122 $this->updateChunkStatus();
124 return $this->mStashFile;
135 $this->mFileKey = $key;
136 $this->mUpload = $webRequestUpload;
138 $this->getChunkStatus();
140 $metadata = $this->stash->getMetadata( $key );
143 $metadata[
'us_size'],
153 $oldFileKey = $this->mFileKey;
154 $chunkIndex = $this->getChunkIndex();
155 $this->logger->debug(
156 __METHOD__ .
' concatenate {totalChunks} chunks: {offset} inx: {curIndex}',
159 'totalChunks' => $this->mChunkIndex,
160 'curIndex' => $chunkIndex,
161 'filekey' => $oldFileKey
168 for ( $i = 0; $i <= $chunkIndex; $i++ ) {
169 $fileList[] = $this->getVirtualChunkLocation( $i );
173 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
175 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
176 ->newTempFSFile(
'chunkedupload_', $ext );
180 $tmpPath = $tmpFile->bind( $this )->getPath();
182 $this->logger->warning(
"Error getting tmp file", [
'filekey' => $oldFileKey ] );
186 $tStart = microtime(
true );
187 $status = $this->repo->concatenate( $fileList, $tmpPath );
188 $tAmount = microtime(
true ) - $tStart;
189 if ( !$status->
isOK() ) {
192 $this->logFileBackendStatus(
194 '[{type}] Error on concatenate {chunks} stashed files ({details})',
195 [
'chunks' => $chunkIndex,
'filekey' => $oldFileKey ]
206 $this->repo->getPrimaryDB(),
208 function () use( $fileList, $oldFileKey ) {
209 $status = $this->repo->quickPurgeBatch( $fileList );
210 if ( !$status->
isOK() ) {
211 $this->logger->warning(
212 "Could not delete chunks of {filekey} - {status}",
214 'status' => (string)$status,
215 'filekey' => $oldFileKey,
223 wfDebugLog(
'fileconcatenate',
"Combined $i chunks in $tAmount seconds." );
231 "Verification failed for chunked upload {filekey}",
233 'user' => $this->user->getName(),
234 'filekey' => $oldFileKey
244 $tStart = microtime(
true );
249 $status->
fatal( ...$error );
250 $this->logger->info(
"Aborting stash upload due to hook - {status}",
252 'status' => (
string)$status,
253 'user' => $this->user->getName(),
254 'filekey' => $this->mFileKey
260 $this->mStashFile = parent::doStashFile( $this->user );
262 $this->logger->warning(
"Could not stash file for {user} because {error} {msg}",
264 'user' => $this->user->getName(),
265 'error' => get_class( $e ),
266 'msg' => $e->getMessage(),
267 'filekey' => $this->mFileKey
270 $status->
fatal(
'uploadstash-exception', get_class( $e ), $e->getMessage() );
274 $tAmount = microtime(
true ) - $tStart;
276 $this->mStashFile->setLocalReference( $tmpFile );
277 $this->logger->info(
"Stashed combined ({chunks} chunks) of {oldkey} under new name {filekey}",
280 'stashTime' => $tAmount,
281 'oldpath' => $this->mVirtualTempPath,
282 'filekey' => $this->mStashFile->getFileKey(),
283 'oldkey' => $oldFileKey,
284 'newpath' => $this->mStashFile->getPath(),
285 'user' => $this->user->getName()
288 wfDebugLog(
'fileconcatenate',
"Stashed combined file ($i chunks) in $tAmount seconds." );
298 private function getVirtualChunkLocation( $index ) {
299 return $this->repo->getVirtualUrl(
'temp' ) .
301 $this->repo->getHashPath(
302 $this->getChunkFileKey( $index )
304 $this->getChunkFileKey( $index );
315 public function addChunk( $chunkPath, $chunkSize, $offset ) {
320 $status = Status::newFatal(
'file-too-large' );
323 if ( $preAppendOffset == $offset ) {
325 $this->mChunkIndex++;
327 # For some reason mTempPath is set to first part
328 $oldTemp = $this->mTempPath;
329 $this->mTempPath = $chunkPath;
330 $this->verifyChunk();
331 $this->mTempPath = $oldTemp;
333 $this->logger->info(
"Error verifying upload chunk {msg}",
335 'user' => $this->user->getName(),
336 'msg' => $e->getMessage(),
337 'chunkIndex' => $this->mChunkIndex,
338 'filekey' => $this->mFileKey
342 return Status::newFatal( $e->msg );
344 $status = $this->outputChunk( $chunkPath );
345 if ( $status->
isGood() ) {
347 $this->mOffset = $preAppendOffset + $chunkSize;
349 $this->updateChunkStatus();
352 $status = Status::newFatal(
'invalid-chunk-offset' );
362 private function updateChunkStatus() {
363 $this->logger->info(
"update chunk status for {filekey} offset: {offset} inx: {inx}",
366 'inx' => $this->getChunkIndex(),
367 'filekey' => $this->mFileKey,
368 'user' => $this->user->getName()
372 $dbw = $this->repo->getPrimaryDB();
373 $dbw->newUpdateQueryBuilder()
374 ->update(
'uploadstash' )
376 'us_status' =>
'chunks',
377 'us_chunk_inx' => $this->getChunkIndex(),
380 ->where( [
'us_key' => $this->mFileKey ] )
381 ->caller( __METHOD__ )->execute();
387 private function getChunkStatus() {
390 $dbw = $this->repo->getPrimaryDB();
391 $row = $dbw->newSelectQueryBuilder()
392 ->select( [
'us_chunk_inx',
'us_size',
'us_path' ] )
393 ->from(
'uploadstash' )
394 ->where( [
'us_key' => $this->mFileKey ] )
395 ->caller( __METHOD__ )->fetchRow();
398 $this->mChunkIndex = $row->us_chunk_inx;
399 $this->mOffset = $row->us_size;
400 $this->mVirtualTempPath = $row->us_path;
408 private function getChunkIndex() {
409 return $this->mChunkIndex ?? 0;
417 return $this->mOffset ?? 0;
427 private function outputChunk( $chunkPath ) {
429 $fileKey = $this->getChunkFileKey();
432 $hashPath = $this->repo->getHashPath( $fileKey );
433 $storeStatus = $this->repo->quickImport( $chunkPath,
434 $this->repo->getZonePath(
'temp' ) .
"/{$hashPath}{$fileKey}" );
437 if ( !$storeStatus->isOK() ) {
438 $error = $this->logFileBackendStatus(
440 '[{type}] Error storing chunk in "{chunkPath}" for {fileKey} ({details})',
441 [
'chunkPath' => $chunkPath,
'fileKey' => $fileKey ]
444 implode(
'; ', $error ), [
'chunkPath' => $chunkPath ] );
450 private function getChunkFileKey( $index =
null ) {
451 return $this->mFileKey .
'.' . ( $index ?? $this->getChunkIndex() );
459 private function verifyChunk() {
461 $oldDesiredDestName = $this->mDesiredDestName;
462 $this->mDesiredDestName = $this->mFileKey;
463 $this->mTitle =
false;
465 $this->mDesiredDestName = $oldDesiredDestName;
466 $this->mTitle =
false;
467 if ( is_array( $res ) ) {
481 private function logFileBackendStatus(
Status $status,
string $logMessage, array $context = [] ): array {
482 $logger = $this->logger;
483 $errorToThrow =
null;
484 $warningToThrow =
null;
486 foreach ( $status->
getErrors() as $errorItem ) {
489 $logMessageType = str_replace(
'{type}', $errorItem[
'message'], $logMessage );
493 $context[
'details'] = implode(
'; ', $errorItem[
'params'] );
494 $context[
'user'] = $this->user->getName();
496 if ( $errorItem[
'type'] ===
'error' ) {
498 $errorToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
499 $logger->error( $logMessageType, $context );
502 $warningToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
503 $logger->warning( $logMessageType, $context );
506 return $errorToThrow ?? $warningToThrow ?? [
'unknown',
'no error recorded' ];
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.
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
getErrors()
Get the list of errors.
isOK()
Returns whether the operation completed.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
runUploadStashFileHook(User $user)
verifyPartialFile()
A verification routine suitable for partial files.
getVerificationErrorCode( $error)
setTempFile( $tempPath, $fileSize=null)
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for a given type of upload, based on $wgMaxUploadSize.
Implements uploading from chunks.
addChunk( $chunkPath, $chunkSize, $offset)
Add a chunk to the temporary directory.
string null $mVirtualTempPath
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.
doStashFile(?User $user=null)
Calls the parent doStashFile and updates the uploadsession table to handle "chunks".
concatenateChunks()
Append the final chunk and ready file for parent::performUpload()
Implements regular file uploads.
UploadStash is intended to accomplish a few things: