10use Psr\Log\LoggerInterface;
54 private LoggerInterface $logger;
71 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
77 wfDebug( __METHOD__ .
" creating new UploadFromChunks instance for " .
$user->
getId() );
78 $this->stash =
new UploadStash( $this->repo, $this->user );
81 $this->logger = LoggerFactory::getInstance(
'upload' );
91 return Status::newFatal( $e->msg );
94 return parent::tryStashFile(
$user, $isPartial );
105 $this->mChunkIndex = 0;
109 $this->mStashFile = parent::doStashFile(
$user );
111 $this->mOffset = $this->mStashFile->getSize();
112 $this->mFileKey = $this->mStashFile->getFileKey();
115 $this->outputChunk( $this->mStashFile->getPath() );
118 $this->updateChunkStatus();
131 $this->mFileKey = $key;
132 $this->mUpload = $webRequestUpload;
134 $this->getChunkStatus();
136 $metadata = $this->stash->getMetadata( $key );
139 $metadata[
'us_size'],
150 $chunkIndex = $this->getChunkIndex();
151 $this->logger->debug(
152 __METHOD__ .
' concatenate {totalChunks} chunks: {offset} inx: {curIndex}',
155 'totalChunks' => $this->mChunkIndex,
156 'curIndex' => $chunkIndex,
157 'filekey' => $oldFileKey
164 for ( $i = 0; $i <= $chunkIndex; $i++ ) {
165 $fileList[] = $this->getVirtualChunkLocation( $i );
169 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
171 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
172 ->newTempFSFile(
'chunkedupload_', $ext );
176 $tmpPath = $tmpFile->bind( $this )->getPath();
178 $this->logger->warning(
"Error getting tmp file", [
'filekey' => $oldFileKey ] );
182 $tStart = microtime(
true );
183 $status = $this->repo->concatenate( $fileList, $tmpPath );
184 $tAmount = microtime(
true ) - $tStart;
185 if ( !$status->
isOK() ) {
188 $this->logFileBackendStatus(
190 '[{type}] Error on concatenate {chunks} stashed files ({details})',
191 [
'chunks' => $chunkIndex,
'filekey' => $oldFileKey ]
202 $this->repo->getPrimaryDB(),
204 function () use( $fileList, $oldFileKey ) {
205 $status = $this->repo->quickPurgeBatch( $fileList );
206 if ( !$status->
isOK() ) {
207 $this->logger->warning(
208 "Could not delete chunks of {filekey} - {status}",
210 'status' => (string)$status,
211 'filekey' => $oldFileKey,
219 wfDebugLog(
'fileconcatenate',
"Combined $i chunks in $tAmount seconds." );
227 "Verification failed for chunked upload {filekey}",
229 'user' => $this->user->getName(),
230 'filekey' => $oldFileKey
240 $tStart = microtime(
true );
245 $status->
fatal( ...$error );
246 $this->logger->info(
"Aborting stash upload due to hook - {status}",
248 'status' => (
string)$status,
249 'user' => $this->user->getName(),
250 'filekey' => $this->mFileKey
256 $this->mStashFile = parent::doStashFile( $this->user );
258 $this->logger->warning(
"Could not stash file for {user} because {error} {msg}",
260 'user' => $this->user->getName(),
261 'error' => get_class( $e ),
262 'msg' => $e->getMessage(),
263 'filekey' => $this->mFileKey
266 $status->
fatal(
'uploadstash-exception', get_class( $e ), $e->getMessage() );
270 $tAmount = microtime(
true ) - $tStart;
272 $this->mStashFile->setLocalReference( $tmpFile );
273 $this->logger->info(
"Stashed combined ({chunks} chunks) of {oldkey} under new name {filekey}",
276 'stashTime' => $tAmount,
277 'oldpath' => $this->mVirtualTempPath,
278 'filekey' => $this->mStashFile->getFileKey(),
279 'oldkey' => $oldFileKey,
280 'newpath' => $this->mStashFile->getPath(),
281 'user' => $this->user->getName()
284 wfDebugLog(
'fileconcatenate',
"Stashed combined file ($i chunks) in $tAmount seconds." );
294 private function getVirtualChunkLocation( $index ) {
295 return $this->repo->getVirtualUrl(
'temp' ) .
297 $this->repo->getHashPath(
298 $this->getChunkFileKey( $index )
300 $this->getChunkFileKey( $index );
311 public function addChunk( $chunkPath, $chunkSize, $offset ) {
316 $status = Status::newFatal(
'file-too-large' );
319 if ( $preAppendOffset == $offset ) {
321 $this->mChunkIndex++;
323 # For some reason mTempPath is set to first part
325 $this->mTempPath = $chunkPath;
326 $this->verifyChunk();
327 $this->mTempPath = $oldTemp;
329 $this->logger->info(
"Error verifying upload chunk {msg}",
331 'user' => $this->user->getName(),
332 'msg' => $e->getMessage(),
333 'chunkIndex' => $this->mChunkIndex,
334 'filekey' => $this->mFileKey
338 return Status::newFatal( $e->msg );
340 $status = $this->outputChunk( $chunkPath );
341 if ( $status->
isGood() ) {
343 $this->mOffset = $preAppendOffset + $chunkSize;
345 $this->updateChunkStatus();
348 $status = Status::newFatal(
'invalid-chunk-offset' );
358 private function updateChunkStatus() {
359 $this->logger->info(
"update chunk status for {filekey} offset: {offset} inx: {inx}",
362 'inx' => $this->getChunkIndex(),
363 'filekey' => $this->mFileKey,
364 'user' => $this->user->getName()
368 $dbw = $this->repo->getPrimaryDB();
369 $dbw->newUpdateQueryBuilder()
370 ->update(
'uploadstash' )
372 'us_status' =>
'chunks',
373 'us_chunk_inx' => $this->getChunkIndex(),
376 ->where( [
'us_key' => $this->mFileKey ] )
377 ->caller( __METHOD__ )->execute();
383 private function getChunkStatus() {
386 $dbw = $this->repo->getPrimaryDB();
387 $row = $dbw->newSelectQueryBuilder()
388 ->select( [
'us_chunk_inx',
'us_size',
'us_path' ] )
389 ->from(
'uploadstash' )
390 ->where( [
'us_key' => $this->mFileKey ] )
391 ->caller( __METHOD__ )->fetchRow();
394 $this->mChunkIndex = $row->us_chunk_inx;
395 $this->mOffset = $row->us_size;
396 $this->mVirtualTempPath = $row->us_path;
404 private function getChunkIndex() {
405 if ( $this->mChunkIndex !==
null ) {
417 if ( $this->mOffset !==
null ) {
431 private function outputChunk( $chunkPath ) {
433 $fileKey = $this->getChunkFileKey();
436 $hashPath = $this->repo->getHashPath( $fileKey );
437 $storeStatus = $this->repo->quickImport( $chunkPath,
438 $this->repo->getZonePath(
'temp' ) .
"/{$hashPath}{$fileKey}" );
441 if ( !$storeStatus->isOK() ) {
442 $error = $this->logFileBackendStatus(
444 '[{type}] Error storing chunk in "{chunkPath}" for {fileKey} ({details})',
445 [
'chunkPath' => $chunkPath,
'fileKey' => $fileKey ]
448 implode(
'; ', $error ), [
'chunkPath' => $chunkPath ] );
454 private function getChunkFileKey( $index =
null ) {
455 return $this->mFileKey .
'.' . ( $index ?? $this->getChunkIndex() );
463 private function verifyChunk() {
467 $this->mTitle =
false;
469 $this->mDesiredDestName = $oldDesiredDestName;
470 $this->mTitle =
false;
471 if ( is_array( $res ) ) {
485 private function logFileBackendStatus(
Status $status,
string $logMessage, array $context = [] ): array {
486 $logger = $this->logger;
487 $errorToThrow =
null;
488 $warningToThrow =
null;
490 foreach ( $status->
getErrors() as $errorItem ) {
493 $logMessageType = str_replace(
'{type}', $errorItem[
'message'], $logMessage );
497 $context[
'details'] = implode(
'; ', $errorItem[
'params'] );
498 $context[
'user'] = $this->user->getName();
500 if ( $errorItem[
'type'] ===
'error' ) {
502 $errorToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
503 $logger->error( $logMessageType, $context );
506 $warningToThrow ??= [ $errorItem[
'message'], ...$errorItem[
'params'] ];
507 $logger->warning( $logMessageType, $context );
510 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.
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: