Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 114 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
AssembleUploadChunksJob | |
0.00% |
0 / 113 |
|
0.00% |
0 / 4 |
272 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 106 |
|
0.00% |
0 / 1 |
156 | |||
getDeduplicationInfo | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
allowRetries | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\JobQueue\Jobs; |
22 | |
23 | use Exception; |
24 | use MediaWiki\Api\ApiUpload; |
25 | use MediaWiki\Context\RequestContext; |
26 | use MediaWiki\JobQueue\GenericParameterJob; |
27 | use MediaWiki\JobQueue\Job; |
28 | use MediaWiki\Logger\LoggerFactory; |
29 | use MediaWiki\Request\WebRequestUpload; |
30 | use MediaWiki\Status\Status; |
31 | use MWExceptionHandler; |
32 | use UnexpectedValueException; |
33 | use UploadBase; |
34 | use UploadFromChunks; |
35 | use Wikimedia\ScopedCallback; |
36 | |
37 | /** |
38 | * Assemble the segments of a chunked upload. |
39 | * |
40 | * @ingroup Upload |
41 | * @ingroup JobQueue |
42 | */ |
43 | class AssembleUploadChunksJob extends Job implements GenericParameterJob { |
44 | public function __construct( array $params ) { |
45 | parent::__construct( 'AssembleUploadChunks', $params ); |
46 | $this->removeDuplicates = true; |
47 | } |
48 | |
49 | public function run() { |
50 | $scope = RequestContext::importScopedSession( $this->params['session'] ); |
51 | $this->addTeardownCallback( static function () use ( &$scope ) { |
52 | ScopedCallback::consume( $scope ); // T126450 |
53 | } ); |
54 | |
55 | $logger = LoggerFactory::getInstance( 'upload' ); |
56 | $context = RequestContext::getMain(); |
57 | $user = $context->getUser(); |
58 | try { |
59 | if ( !$user->isRegistered() ) { |
60 | $this->setLastError( "Could not load the author user from session." ); |
61 | |
62 | return false; |
63 | } |
64 | |
65 | // TODO add some sort of proper locking maybe |
66 | $startingStatus = UploadBase::getSessionStatus( $user, $this->params['filekey'] ); |
67 | if ( |
68 | !$startingStatus || |
69 | ( $startingStatus['result'] ?? '' ) !== 'Poll' || |
70 | ( $startingStatus['stage'] ?? '' ) !== 'queued' |
71 | ) { |
72 | $logger->warning( "Tried to assemble upload that is in stage {stage}/{result}", |
73 | [ |
74 | 'stage' => $startingStatus['stage'] ?? '-', |
75 | 'result' => $startingStatus['result'] ?? '-', |
76 | 'status' => (string)( $startingStatus['status'] ?? '-' ), |
77 | 'filekey' => $this->params['filekey'], |
78 | 'filename' => $this->params['filename'], |
79 | 'user' => $user->getName(), |
80 | ] |
81 | ); |
82 | // If it is marked as currently in progress, abort. Otherwise |
83 | // assume it is some sort of replag issue or maybe a retry even |
84 | // though retries are impossible and just warn. |
85 | if ( |
86 | $startingStatus && |
87 | $startingStatus['stage'] === 'assembling' && |
88 | $startingStatus['result'] !== 'Failure' |
89 | ) { |
90 | $this->setLastError( __METHOD__ . " already in progress" ); |
91 | return false; |
92 | } |
93 | } |
94 | UploadBase::setSessionStatus( |
95 | $user, |
96 | $this->params['filekey'], |
97 | [ 'result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood() ] |
98 | ); |
99 | |
100 | $upload = new UploadFromChunks( $user ); |
101 | $upload->continueChunks( |
102 | $this->params['filename'], |
103 | $this->params['filekey'], |
104 | new WebRequestUpload( $context->getRequest(), 'null' ) |
105 | ); |
106 | if ( |
107 | isset( $this->params['filesize'] ) && |
108 | $this->params['filesize'] !== (int)$upload->getOffset() |
109 | ) { |
110 | // Check to make sure we are not executing prior to the API's |
111 | // transaction being committed. (T350917) |
112 | throw new UnexpectedValueException( |
113 | "UploadStash file size does not match job's. Potential mis-nested transaction?" |
114 | ); |
115 | } |
116 | // Combine all of the chunks into a local file and upload that to a new stash file |
117 | $status = $upload->concatenateChunks(); |
118 | if ( !$status->isGood() ) { |
119 | UploadBase::setSessionStatus( |
120 | $user, |
121 | $this->params['filekey'], |
122 | [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ] |
123 | ); |
124 | $logger->info( "Chunked upload assembly job failed for {filekey} because {status}", |
125 | [ |
126 | 'filekey' => $this->params['filekey'], |
127 | 'filename' => $this->params['filename'], |
128 | 'user' => $user->getName(), |
129 | 'status' => (string)$status |
130 | ] |
131 | ); |
132 | // the chunks did not get assembled, but this should not be considered a job |
133 | // failure - they simply didn't pass verification for some reason, and that |
134 | // reason is stored in above session to inform the clients |
135 | return true; |
136 | } |
137 | |
138 | // We can only get warnings like 'duplicate' after concatenating the chunks |
139 | $status = Status::newGood(); |
140 | $status->value = [ |
141 | 'warnings' => UploadBase::makeWarningsSerializable( |
142 | $upload->checkWarnings( $user ) |
143 | ) |
144 | ]; |
145 | |
146 | // We have a new filekey for the fully concatenated file |
147 | $newFileKey = $upload->getStashFile()->getFileKey(); |
148 | |
149 | // Remove the old stash file row and first chunk file |
150 | // Note: This does not delete the chunks, only the stash file |
151 | // which is same as first chunk but with a different name. |
152 | $upload->stash->removeFileNoAuth( $this->params['filekey'] ); |
153 | |
154 | // Build the image info array while we have the local reference handy |
155 | $apiUpload = ApiUpload::getDummyInstance(); |
156 | $imageInfo = $apiUpload->getUploadImageInfo( $upload ); |
157 | |
158 | // Cleanup any temporary local file |
159 | $upload->cleanupTempFile(); |
160 | |
161 | // Cache the info so the user doesn't have to wait forever to get the final info |
162 | UploadBase::setSessionStatus( |
163 | $user, |
164 | $this->params['filekey'], |
165 | [ |
166 | 'result' => 'Success', |
167 | 'stage' => 'assembling', |
168 | 'filekey' => $newFileKey, |
169 | 'imageinfo' => $imageInfo, |
170 | 'status' => $status |
171 | ] |
172 | ); |
173 | $logger->info( "{filekey} successfully assembled into {newkey}", |
174 | [ |
175 | 'filekey' => $this->params['filekey'], |
176 | 'newkey' => $newFileKey, |
177 | 'filename' => $this->params['filename'], |
178 | 'user' => $user->getName(), |
179 | 'status' => (string)$status |
180 | ] |
181 | ); |
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 | |
202 | public function getDeduplicationInfo() { |
203 | $info = parent::getDeduplicationInfo(); |
204 | if ( is_array( $info['params'] ) ) { |
205 | $info['params'] = [ 'filekey' => $info['params']['filekey'] ]; |
206 | } |
207 | |
208 | return $info; |
209 | } |
210 | |
211 | public function allowRetries() { |
212 | return false; |
213 | } |
214 | } |
215 | |
216 | /** @deprecated class alias since 1.44 */ |
217 | class_alias( AssembleUploadChunksJob::class, 'AssembleUploadChunksJob' ); |