MediaWiki master
AssembleUploadChunksJob.php
Go to the documentation of this file.
1<?php
8
9use Exception;
18use UnexpectedValueException;
19use UploadBase;
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 $apiUpload = ApiUpload::getDummyInstance();
144 $imageInfo = $apiUpload->getUploadImageInfo( $upload );
145
146 // Cleanup any temporary local file
147 $upload->cleanupTempFile();
148
149 // Cache the info so the user doesn't have to wait forever to get the final info
150 UploadBase::setSessionStatus(
151 $user,
152 $this->params['filekey'],
153 [
154 'result' => 'Success',
155 'stage' => 'assembling',
156 'filekey' => $newFileKey,
157 'imageinfo' => $imageInfo,
158 'status' => $status
159 ]
160 );
161 $logger->info( "{filekey} successfully assembled into {newkey}",
162 [
163 'filekey' => $this->params['filekey'],
164 'newkey' => $newFileKey,
165 'filename' => $this->params['filename'],
166 'user' => $user->getName(),
167 'status' => (string)$status
168 ]
169 );
170 } catch ( UploadStashException $e ) {
171 UploadBase::setSessionStatus(
172 $user,
173 $this->params['filekey'],
174 [
175 'result' => 'Failure',
176 'stage' => 'assembling',
177 'status' => Status::newFatal( $e->getMessageObject() ),
178 ]
179 );
180 $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
181 return false;
182 } catch ( Exception $e ) {
183 UploadBase::setSessionStatus(
184 $user,
185 $this->params['filekey'],
186 [
187 'result' => 'Failure',
188 'stage' => 'assembling',
189 'status' => Status::newFatal( 'api-error-stashfailed' )
190 ]
191 );
192 $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
193 // To be extra robust.
194 MWExceptionHandler::rollbackPrimaryChangesAndLog( $e );
195
196 return false;
197 }
198
199 return true;
200 }
201
203 public function getDeduplicationInfo() {
204 $info = parent::getDeduplicationInfo();
205 if ( is_array( $info['params'] ) ) {
206 $info['params'] = [ 'filekey' => $info['params']['filekey'] ];
207 }
208
209 return $info;
210 }
211
213 public function allowRetries() {
214 return false;
215 }
216}
217
219class_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:27
array $params
Array of job parameters.
Definition Job.php:32
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
UploadBase and subclasses are the backend of MediaWiki's file uploads.
cleanupTempFile()
If we've modified the upload file, then we need to manually remove it on exit to clean up.
checkWarnings( $user=null)
Check for non fatal problems with the file.
Implements uploading from chunks.
getMessageObject()
Return a Message object for this exception.Message
Interface for generic jobs only uses the parameters field and are JSON serializable.