MediaWiki master
AssembleUploadChunksJob.php
Go to the documentation of this file.
1<?php
8
9use Exception;
21use UnexpectedValueException;
22use Wikimedia\ScopedCallback;
23
31 public function __construct( array $params ) {
32 parent::__construct( 'AssembleUploadChunks', $params );
33 $this->removeDuplicates = true;
34 }
35
37 public function run() {
38 $scope = RequestContext::importScopedSession( $this->params['session'] );
39 $this->addTeardownCallback( static function () use ( &$scope ) {
40 ScopedCallback::consume( $scope ); // T126450
41 } );
42
43 $logger = LoggerFactory::getInstance( 'upload' );
44 $context = RequestContext::getMain();
45 $user = $context->getUser();
46 try {
47 if ( !$user->isRegistered() ) {
48 $this->setLastError( "Could not load the author user from session." );
49
50 return false;
51 }
52
53 // TODO add some sort of proper locking maybe
54 $startingStatus = UploadBase::getSessionStatus( $user, $this->params['filekey'] );
55 if (
56 !$startingStatus ||
57 ( $startingStatus['result'] ?? '' ) !== 'Poll' ||
58 ( $startingStatus['stage'] ?? '' ) !== 'queued'
59 ) {
60 $logger->warning( "Tried to assemble upload that is in stage {stage}/{result}",
61 [
62 'stage' => $startingStatus['stage'] ?? '-',
63 'result' => $startingStatus['result'] ?? '-',
64 'status' => (string)( $startingStatus['status'] ?? '-' ),
65 'filekey' => $this->params['filekey'],
66 'filename' => $this->params['filename'],
67 'user' => $user->getName(),
68 ]
69 );
70 // If it is marked as currently in progress, abort. Otherwise
71 // assume it is some sort of replag issue or maybe a retry even
72 // though retries are impossible and just warn.
73 if (
74 $startingStatus &&
75 $startingStatus['stage'] === 'assembling' &&
76 $startingStatus['result'] !== 'Failure'
77 ) {
78 $this->setLastError( __METHOD__ . " already in progress" );
79 return false;
80 }
81 }
82 UploadBase::setSessionStatus(
83 $user,
84 $this->params['filekey'],
85 [ 'result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood() ]
86 );
87
88 $upload = new UploadFromChunks( $user );
89 $upload->continueChunks(
90 $this->params['filename'],
91 $this->params['filekey'],
92 new WebRequestUpload( $context->getRequest(), 'null' )
93 );
94 if (
95 isset( $this->params['filesize'] ) &&
96 $this->params['filesize'] !== (int)$upload->getOffset()
97 ) {
98 // Check to make sure we are not executing prior to the API's
99 // transaction being committed. (T350917)
100 throw new UnexpectedValueException(
101 "UploadStash file size does not match job's. Potential mis-nested transaction?"
102 );
103 }
104 // Combine all of the chunks into a local file and upload that to a new stash file
105 $status = $upload->concatenateChunks();
106 if ( !$status->isGood() ) {
107 UploadBase::setSessionStatus(
108 $user,
109 $this->params['filekey'],
110 [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
111 );
112 $logger->info( "Chunked upload assembly job failed for {filekey} because {status}",
113 [
114 'filekey' => $this->params['filekey'],
115 'filename' => $this->params['filename'],
116 'user' => $user->getName(),
117 'status' => (string)$status
118 ]
119 );
120 // the chunks did not get assembled, but this should not be considered a job
121 // failure - they simply didn't pass verification for some reason, and that
122 // reason is stored in above session to inform the clients
123 return true;
124 }
125
126 // We can only get warnings like 'duplicate' after concatenating the chunks
127 $status = Status::newGood();
128 $status->value = [
129 'warnings' => UploadBase::makeWarningsSerializable(
130 $upload->checkWarnings( $user )
131 )
132 ];
133
134 // We have a new filekey for the fully concatenated file
135 $newFileKey = $upload->getStashFile()->getFileKey();
136
137 // Remove the old stash file row and first chunk file
138 // Note: This does not delete the chunks, only the stash file
139 // which is same as first chunk but with a different name.
140 $upload->stash->removeFileNoAuth( $this->params['filekey'] );
141
142 // Build the image info array while we have the local reference handy
143 // Deprecated, kept for backward compatibility on deployment
144 $apiUpload = ApiUpload::getDummyInstance();
145 $imageInfo = $apiUpload->getUploadImageInfo( $upload );
146
147 // Cleanup any temporary local file
148 $upload->cleanupTempFile();
149
150 // Cache the info so the user doesn't have to wait forever to get the final info
151 UploadBase::setSessionStatus(
152 $user,
153 $this->params['filekey'],
154 [
155 'result' => 'Success',
156 'stage' => 'assembling',
157 'filekey' => $newFileKey,
158 'imageinfo' => $imageInfo,
159 'status' => $status
160 ]
161 );
162 $logger->info( "{filekey} successfully assembled into {newkey}",
163 [
164 'filekey' => $this->params['filekey'],
165 'newkey' => $newFileKey,
166 'filename' => $this->params['filename'],
167 'user' => $user->getName(),
168 'status' => (string)$status
169 ]
170 );
171 } catch ( UploadStashException $e ) {
172 UploadBase::setSessionStatus(
173 $user,
174 $this->params['filekey'],
175 [
176 'result' => 'Failure',
177 'stage' => 'assembling',
178 'status' => Status::newFatal( $e->getMessageObject() ),
179 ]
180 );
181 $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
182 return false;
183 } catch ( Exception $e ) {
184 UploadBase::setSessionStatus(
185 $user,
186 $this->params['filekey'],
187 [
188 'result' => 'Failure',
189 'stage' => 'assembling',
190 'status' => Status::newFatal( 'api-error-stashfailed' )
191 ]
192 );
193 $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
194 // To be extra robust.
195 MWExceptionHandler::rollbackPrimaryChangesAndLog( $e );
196
197 return false;
198 }
199
200 return true;
201 }
202
204 public function getDeduplicationInfo() {
205 $info = parent::getDeduplicationInfo();
206 if ( is_array( $info['params'] ) ) {
207 $info['params'] = [ 'filekey' => $info['params']['filekey'] ];
208 }
209
210 return $info;
211 }
212
214 public function allowRetries() {
215 return false;
216 }
217}
218
220class_alias( AssembleUploadChunksJob::class, 'AssembleUploadChunksJob' );
Group all the pieces relevant to the context of a request into one instance.
Handler class for MWExceptions.
Describe and execute a background job.
Definition Job.php:28
array $params
Array of job parameters.
Definition Job.php:33
addTeardownCallback( $callback)
Definition Job.php:343
setLastError( $error)
Definition Job.php:424
Assemble the segments of a chunked upload.
allowRetries()
Whether to retry execution of this job if run() returned false or threw an exception....
getDeduplicationInfo()
Subclasses may need to override this to make duplication detection work.The resulting map conveys eve...
run()
Run the job.If this method returns false or completes exceptionally, the job runner will retry execut...
Create PSR-3 logger objects.
Object to access the $_FILES array.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
getMessageObject()
Return a Message object for this exception.Message
UploadBase and subclasses are the backend of MediaWiki's file uploads.
checkWarnings( $user=null)
Check for non fatal problems with the file.
cleanupTempFile()
If we've modified the upload file, then we need to manually remove it on exit to clean up.
Implements uploading from chunks.
Interface for generic jobs only uses the parameters field and are JSON serializable.