8use Psr\Log\LoggerInterface;
51 private LoggerInterface $logger;
68 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
74 wfDebug( __METHOD__ .
" creating new UploadFromChunks instance for " .
$user->
getId() );
75 $this->stash =
new UploadStash( $this->repo, $this->user );
78 $this->logger = LoggerFactory::getInstance(
'upload' );
88 return Status::newFatal( $e->msg );
91 return parent::tryStashFile(
$user, $isPartial );
102 $this->mChunkIndex = 0;
106 $this->mStashFile = parent::doStashFile(
$user );
108 $this->mOffset = $this->mStashFile->getSize();
109 $this->mFileKey = $this->mStashFile->getFileKey();
112 $this->outputChunk( $this->mStashFile->getPath() );
115 $this->updateChunkStatus();
128 $this->mFileKey = $key;
129 $this->mUpload = $webRequestUpload;
131 $this->getChunkStatus();
133 $metadata = $this->stash->getMetadata( $key );
136 $metadata[
'us_size'],
147 $chunkIndex = $this->getChunkIndex();
148 $this->logger->debug(
149 __METHOD__ .
' concatenate {totalChunks} chunks: {offset} inx: {curIndex}',
152 'totalChunks' => $this->mChunkIndex,
153 'curIndex' => $chunkIndex,
154 'filekey' => $oldFileKey
161 for ( $i = 0; $i <= $chunkIndex; $i++ ) {
162 $fileList[] = $this->getVirtualChunkLocation( $i );
168 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
169 ->newTempFSFile(
'chunkedupload_', $ext );
173 $tmpPath = $tmpFile->bind( $this )->getPath();
175 $this->logger->warning(
"Error getting tmp file", [
'filekey' => $oldFileKey ] );
179 $tStart = microtime(
true );
180 $status = $this->repo->concatenate( $fileList, $tmpPath );
181 $tAmount = microtime(
true ) - $tStart;
182 if ( !$status->
isOK() ) {
185 $this->logFileBackendStatus(
187 '[{type}] Error on concatenate {chunks} stashed files ({details})',
188 [
'chunks' => $chunkIndex,
'filekey' => $oldFileKey ]
198 DeferredUpdates::addUpdate(
new AutoCommitUpdate(
199 $this->repo->getPrimaryDB(),
201 function () use( $fileList, $oldFileKey ) {
202 $status = $this->repo->quickPurgeBatch( $fileList );
203 if ( !$status->
isOK() ) {
204 $this->logger->warning(
205 "Could not delete chunks of {filekey} - {status}",
207 'status' => (string)$status,
208 'filekey' => $oldFileKey,
216 wfDebugLog(
'fileconcatenate',
"Combined $i chunks in $tAmount seconds." );
224 "Verification failed for chunked upload {filekey}",
226 'user' => $this->user->getName(),
227 'filekey' => $oldFileKey
237 $tStart = microtime(
true );
242 $status->
fatal( ...$error );
243 $this->logger->info(
"Aborting stash upload due to hook - {status}",
245 'status' => (
string)$status,
246 'user' => $this->user->getName(),
247 'filekey' => $this->mFileKey
253 $this->mStashFile = parent::doStashFile( $this->user );
255 $this->logger->warning(
"Could not stash file for {user} because {error} {msg}",
257 'user' => $this->user->getName(),
258 'error' => get_class( $e ),
259 'msg' => $e->getMessage(),
260 'filekey' => $this->mFileKey
263 $status->
fatal(
'uploadstash-exception', get_class( $e ), $e->getMessage() );
267 $tAmount = microtime(
true ) - $tStart;
269 $this->mStashFile->setLocalReference( $tmpFile );
270 $this->logger->info(
"Stashed combined ({chunks} chunks) of {oldkey} under new name {filekey}",
273 'stashTime' => $tAmount,
274 'oldpath' => $this->mVirtualTempPath,
275 'filekey' => $this->mStashFile->getFileKey(),
276 'oldkey' => $oldFileKey,
277 'newpath' => $this->mStashFile->getPath(),
278 'user' => $this->user->getName()
281 wfDebugLog(
'fileconcatenate',
"Stashed combined file ($i chunks) in $tAmount seconds." );
291 private function getVirtualChunkLocation( $index ) {
292 return $this->repo->getVirtualUrl(
'temp' ) .
294 $this->repo->getHashPath(
295 $this->getChunkFileKey( $index )
297 $this->getChunkFileKey( $index );
308 public function addChunk( $chunkPath, $chunkSize, $offset ) {
313 $status = Status::newFatal(
'file-too-large' );
316 if ( $preAppendOffset == $offset ) {
318 $this->mChunkIndex++;
320 # For some reason mTempPath is set to first part
322 $this->mTempPath = $chunkPath;
323 $this->verifyChunk();
324 $this->mTempPath = $oldTemp;
326 $this->logger->info(
"Error verifying upload chunk {msg}",
328 'user' => $this->user->getName(),
329 'msg' => $e->getMessage(),
330 'chunkIndex' => $this->mChunkIndex,
331 'filekey' => $this->mFileKey
335 return Status::newFatal( $e->msg );
337 $status = $this->outputChunk( $chunkPath );
338 if ( $status->
isGood() ) {
340 $this->mOffset = $preAppendOffset + $chunkSize;
342 $this->updateChunkStatus();
345 $status = Status::newFatal(
'invalid-chunk-offset' );
355 private function updateChunkStatus() {
356 $this->logger->info(
"update chunk status for {filekey} offset: {offset} inx: {inx}",
359 'inx' => $this->getChunkIndex(),
360 'filekey' => $this->mFileKey,
361 'user' => $this->user->getName()
365 $dbw = $this->repo->getPrimaryDB();
366 $dbw->newUpdateQueryBuilder()
367 ->update(
'uploadstash' )
369 'us_status' =>
'chunks',
370 'us_chunk_inx' => $this->getChunkIndex(),
373 ->where( [
'us_key' => $this->mFileKey ] )
374 ->caller( __METHOD__ )->execute();
380 private function getChunkStatus() {
383 $dbw = $this->repo->getPrimaryDB();
384 $row = $dbw->newSelectQueryBuilder()
385 ->select( [
'us_chunk_inx',
'us_size',
'us_path' ] )
386 ->from(
'uploadstash' )
387 ->where( [
'us_key' => $this->mFileKey ] )
388 ->caller( __METHOD__ )->fetchRow();
391 $this->mChunkIndex = $row->us_chunk_inx;
392 $this->mOffset = $row->us_size;
393 $this->mVirtualTempPath = $row->us_path;
401 private function getChunkIndex() {
402 if ( $this->mChunkIndex !==
null ) {
414 if ( $this->mOffset !==
null ) {
428 private function outputChunk( $chunkPath ) {
430 $fileKey = $this->getChunkFileKey();
433 $hashPath = $this->repo->getHashPath( $fileKey );
434 $storeStatus = $this->repo->quickImport( $chunkPath,
435 $this->repo->getZonePath(
'temp' ) .
"/{$hashPath}{$fileKey}" );
438 if ( !$storeStatus->isOK() ) {
439 $error = $this->logFileBackendStatus(
441 '[{type}] Error storing chunk in "{chunkPath}" for {fileKey} ({details})',
442 [
'chunkPath' => $chunkPath,
'fileKey' => $fileKey ]
445 implode(
'; ', $error ), [
'chunkPath' => $chunkPath ] );
451 private function getChunkFileKey( $index =
null ) {
452 return $this->mFileKey .
'.' . ( $index ?? $this->getChunkIndex() );
460 private function verifyChunk() {
464 $this->mTitle =
false;
466 $this->mDesiredDestName = $oldDesiredDestName;
467 $this->mTitle =
false;
468 if ( is_array( $res ) ) {
482 private function logFileBackendStatus(
Status $status,
string $logMessage, array $context = [] ): array {
483 $logger = $this->logger;
484 $errorToThrow =
null;
485 $warningToThrow =
null;
487 foreach ( $status->
getErrors() as $errorItem ) {
490 $logMessageType = str_replace(
'{type}', $errorItem[
'message'], $logMessage );
494 $context[
'details'] = implode(
'; ', $errorItem[
'params'] );
495 $context[
'user'] = $this->user->getName();
497 if ( $errorItem[
'type'] ===
'error' ) {
499 $errorToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
500 $logger->error( $logMessageType, $context );
503 $warningToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
504 $logger->warning( $logMessageType, $context );
507 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.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
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.
UploadStashFile null $mStashFile
runUploadStashFileHook(User $user)
verifyPartialFile()
A verification routine suitable for partial files.
getVerificationErrorCode( $error)
string null $mDesiredDestName
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.
string null $mTempPath
Local file system path to the file to upload (or a local copy)
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: