Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 113 |
|
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 | use MediaWiki\Context\RequestContext; |
22 | use MediaWiki\Logger\LoggerFactory; |
23 | use MediaWiki\Request\WebRequestUpload; |
24 | use MediaWiki\Status\Status; |
25 | use Wikimedia\ScopedCallback; |
26 | |
27 | /** |
28 | * Assemble the segments of a chunked upload. |
29 | * |
30 | * @ingroup Upload |
31 | * @ingroup JobQueue |
32 | */ |
33 | class AssembleUploadChunksJob extends Job implements GenericParameterJob { |
34 | public function __construct( array $params ) { |
35 | parent::__construct( 'AssembleUploadChunks', $params ); |
36 | $this->removeDuplicates = true; |
37 | } |
38 | |
39 | public function run() { |
40 | $scope = RequestContext::importScopedSession( $this->params['session'] ); |
41 | $this->addTeardownCallback( static function () use ( &$scope ) { |
42 | ScopedCallback::consume( $scope ); // T126450 |
43 | } ); |
44 | |
45 | $logger = LoggerFactory::getInstance( 'upload' ); |
46 | $context = RequestContext::getMain(); |
47 | $user = $context->getUser(); |
48 | try { |
49 | if ( !$user->isRegistered() ) { |
50 | $this->setLastError( "Could not load the author user from session." ); |
51 | |
52 | return false; |
53 | } |
54 | |
55 | // TODO add some sort of proper locking maybe |
56 | $startingStatus = UploadBase::getSessionStatus( $user, $this->params['filekey'] ); |
57 | if ( |
58 | !$startingStatus || |
59 | ( $startingStatus['result'] ?? '' ) !== 'Poll' || |
60 | ( $startingStatus['stage'] ?? '' ) !== 'queued' |
61 | ) { |
62 | $logger->warning( "Tried to assemble upload that is in stage {stage}/{result}", |
63 | [ |
64 | 'stage' => $startingStatus['stage'] ?? '-', |
65 | 'result' => $startingStatus['result'] ?? '-', |
66 | 'status' => (string)( $startingStatus['status'] ?? '-' ), |
67 | 'filekey' => $this->params['filekey'], |
68 | 'filename' => $this->params['filename'], |
69 | 'user' => $user->getName(), |
70 | ] |
71 | ); |
72 | // If it is marked as currently in progress, abort. Otherwise |
73 | // assume it is some sort of replag issue or maybe a retry even |
74 | // though retries are impossible and just warn. |
75 | if ( |
76 | $startingStatus && |
77 | $startingStatus['stage'] === 'assembling' && |
78 | $startingStatus['result'] !== 'Failure' |
79 | ) { |
80 | $this->setLastError( __METHOD__ . " already in progress" ); |
81 | return false; |
82 | } |
83 | } |
84 | UploadBase::setSessionStatus( |
85 | $user, |
86 | $this->params['filekey'], |
87 | [ 'result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood() ] |
88 | ); |
89 | |
90 | $upload = new UploadFromChunks( $user ); |
91 | $upload->continueChunks( |
92 | $this->params['filename'], |
93 | $this->params['filekey'], |
94 | new WebRequestUpload( $context->getRequest(), 'null' ) |
95 | ); |
96 | if ( |
97 | isset( $this->params['filesize'] ) && |
98 | $this->params['filesize'] !== (int)$upload->getOffset() |
99 | ) { |
100 | // Check to make sure we are not executing prior to the API's |
101 | // transaction being committed. (T350917) |
102 | throw new UnexpectedValueException( |
103 | "UploadStash file size does not match job's. Potential mis-nested transaction?" |
104 | ); |
105 | } |
106 | // Combine all of the chunks into a local file and upload that to a new stash file |
107 | $status = $upload->concatenateChunks(); |
108 | if ( !$status->isGood() ) { |
109 | UploadBase::setSessionStatus( |
110 | $user, |
111 | $this->params['filekey'], |
112 | [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ] |
113 | ); |
114 | $logger->info( "Chunked upload assembly job failed for {filekey} because {status}", |
115 | [ |
116 | 'filekey' => $this->params['filekey'], |
117 | 'filename' => $this->params['filename'], |
118 | 'user' => $user->getName(), |
119 | 'status' => (string)$status |
120 | ] |
121 | ); |
122 | // the chunks did not get assembled, but this should not be considered a job |
123 | // failure - they simply didn't pass verification for some reason, and that |
124 | // reason is stored in above session to inform the clients |
125 | return true; |
126 | } |
127 | |
128 | // We can only get warnings like 'duplicate' after concatenating the chunks |
129 | $status = Status::newGood(); |
130 | $status->value = [ |
131 | 'warnings' => UploadBase::makeWarningsSerializable( |
132 | $upload->checkWarnings( $user ) |
133 | ) |
134 | ]; |
135 | |
136 | // We have a new filekey for the fully concatenated file |
137 | $newFileKey = $upload->getStashFile()->getFileKey(); |
138 | |
139 | // Remove the old stash file row and first chunk file |
140 | // Note: This does not delete the chunks, only the stash file |
141 | // which is same as first chunk but with a different name. |
142 | $upload->stash->removeFileNoAuth( $this->params['filekey'] ); |
143 | |
144 | // Build the image info array while we have the local reference handy |
145 | $apiUpload = ApiUpload::getDummyInstance(); |
146 | $imageInfo = $apiUpload->getUploadImageInfo( $upload ); |
147 | |
148 | // Cleanup any temporary local file |
149 | $upload->cleanupTempFile(); |
150 | |
151 | // Cache the info so the user doesn't have to wait forever to get the final info |
152 | UploadBase::setSessionStatus( |
153 | $user, |
154 | $this->params['filekey'], |
155 | [ |
156 | 'result' => 'Success', |
157 | 'stage' => 'assembling', |
158 | 'filekey' => $newFileKey, |
159 | 'imageinfo' => $imageInfo, |
160 | 'status' => $status |
161 | ] |
162 | ); |
163 | $logger->info( "{filekey} successfully assembled into {newkey}", |
164 | [ |
165 | 'filekey' => $this->params['filekey'], |
166 | 'newkey' => $newFileKey, |
167 | 'filename' => $this->params['filename'], |
168 | 'user' => $user->getName(), |
169 | 'status' => (string)$status |
170 | ] |
171 | ); |
172 | } catch ( Exception $e ) { |
173 | UploadBase::setSessionStatus( |
174 | $user, |
175 | $this->params['filekey'], |
176 | [ |
177 | 'result' => 'Failure', |
178 | 'stage' => 'assembling', |
179 | 'status' => Status::newFatal( 'api-error-stashfailed' ) |
180 | ] |
181 | ); |
182 | $this->setLastError( get_class( $e ) . ": " . $e->getMessage() ); |
183 | // To be extra robust. |
184 | MWExceptionHandler::rollbackPrimaryChangesAndLog( $e ); |
185 | |
186 | return false; |
187 | } |
188 | |
189 | return true; |
190 | } |
191 | |
192 | public function getDeduplicationInfo() { |
193 | $info = parent::getDeduplicationInfo(); |
194 | if ( is_array( $info['params'] ) ) { |
195 | $info['params'] = [ 'filekey' => $info['params']['filekey'] ]; |
196 | } |
197 | |
198 | return $info; |
199 | } |
200 | |
201 | public function allowRetries() { |
202 | return false; |
203 | } |
204 | } |