Merge "ToC: Use display:table, so that we can behave like a block element"
[lhc/web/wiklou.git] / includes / upload / UploadFromChunks.php
1 <?php
2 /**
3 * Backend for uploading files from chunks.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Upload
22 */
23
24 /**
25 * Implements uploading from chunks
26 *
27 * @ingroup Upload
28 * @author Michael Dale
29 */
30 class UploadFromChunks extends UploadFromFile {
31 protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath;
32
33 /**
34 * Setup local pointers to stash, repo and user (similar to UploadFromStash)
35 *
36 * @param $user User
37 * @param $stash UploadStash
38 * @param $repo FileRepo
39 */
40 public function __construct( $user = null, $stash = false, $repo = false ) {
41 // user object. sometimes this won't exist, as when running from cron.
42 $this->user = $user;
43
44 if ( $repo ) {
45 $this->repo = $repo;
46 } else {
47 $this->repo = RepoGroup::singleton()->getLocalRepo();
48 }
49
50 if ( $stash ) {
51 $this->stash = $stash;
52 } else {
53 if ( $user ) {
54 wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" );
55 } else {
56 wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" );
57 }
58 $this->stash = new UploadStash( $this->repo, $this->user );
59 }
60
61 return true;
62 }
63
64 /**
65 * Calls the parent stashFile and updates the uploadsession table to handle "chunks"
66 *
67 * @return UploadStashFile stashed file
68 */
69 public function stashFile( User $user = null ) {
70 // Stash file is the called on creating a new chunk session:
71 $this->mChunkIndex = 0;
72 $this->mOffset = 0;
73
74 $this->verifyChunk();
75 // Create a local stash target
76 $this->mLocalFile = parent::stashFile();
77 // Update the initial file offset (based on file size)
78 $this->mOffset = $this->mLocalFile->getSize();
79 $this->mFileKey = $this->mLocalFile->getFileKey();
80
81 // Output a copy of this first to chunk 0 location:
82 $this->outputChunk( $this->mLocalFile->getPath() );
83
84 // Update db table to reflect initial "chunk" state
85 $this->updateChunkStatus();
86 return $this->mLocalFile;
87 }
88
89 /**
90 * Continue chunk uploading
91 */
92 public function continueChunks( $name, $key, $webRequestUpload ) {
93 $this->mFileKey = $key;
94 $this->mUpload = $webRequestUpload;
95 // Get the chunk status form the db:
96 $this->getChunkStatus();
97
98 $metadata = $this->stash->getMetadata( $key );
99 $this->initializePathInfo( $name,
100 $this->getRealPath( $metadata['us_path'] ),
101 $metadata['us_size'],
102 false
103 );
104 }
105
106 /**
107 * Append the final chunk and ready file for parent::performUpload()
108 * @return FileRepoStatus
109 */
110 public function concatenateChunks() {
111 wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
112 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
113
114 // Concatenate all the chunks to mVirtualTempPath
115 $fileList = Array();
116 // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1"
117 for ( $i = 0; $i <= $this->getChunkIndex(); $i++ ) {
118 $fileList[] = $this->getVirtualChunkLocation( $i );
119 }
120
121 // Get the file extension from the last chunk
122 $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
123 // Get a 0-byte temp file to perform the concatenation at
124 $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
125 $tmpPath = $tmpFile
126 ? $tmpFile->bind( $this )->getPath() // keep alive with $this
127 : false; // fail in concatenate()
128 // Concatenate the chunks at the temp file
129 $tStart = microtime( true );
130 $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
131 $tAmount = microtime( true ) - $tStart;
132 if ( !$status->isOk() ) {
133 return $status;
134 }
135 wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds.\n" );
136
137 $this->mTempPath = $tmpPath; // file system path
138 $this->mFileSize = filesize( $this->mTempPath ); //Since this was set for the last chunk previously
139 $ret = $this->verifyUpload();
140 if ( $ret['status'] !== UploadBase::OK ) {
141 wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" );
142 $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) );
143 return $status;
144 }
145
146 // Update the mTempPath and mLocalFile
147 // (for FileUpload or normal Stash to take over)
148 $tStart = microtime( true );
149 $this->mLocalFile = parent::stashFile( $this->user );
150 $tAmount = microtime( true ) - $tStart;
151 $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
152 wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds.\n" );
153
154 return $status;
155 }
156
157 /**
158 * Perform the upload, then remove the temp copy afterward
159 * @param $comment string
160 * @param $pageText string
161 * @param $watch bool
162 * @param $user User
163 * @return Status
164 */
165 public function performUpload( $comment, $pageText, $watch, $user ) {
166 $rv = parent::performUpload( $comment, $pageText, $watch, $user );
167 return $rv;
168 }
169
170 /**
171 * Returns the virtual chunk location:
172 * @param $index
173 * @return string
174 */
175 function getVirtualChunkLocation( $index ) {
176 return $this->repo->getVirtualUrl( 'temp' ) .
177 '/' .
178 $this->repo->getHashPath(
179 $this->getChunkFileKey( $index )
180 ) .
181 $this->getChunkFileKey( $index );
182 }
183
184 /**
185 * Add a chunk to the temporary directory
186 *
187 * @param string $chunkPath path to temporary chunk file
188 * @param int $chunkSize size of the current chunk
189 * @param int $offset offset of current chunk ( mutch match database chunk offset )
190 * @return Status
191 */
192 public function addChunk( $chunkPath, $chunkSize, $offset ) {
193 // Get the offset before we add the chunk to the file system
194 $preAppendOffset = $this->getOffset();
195
196 if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) {
197 $status = Status::newFatal( 'file-too-large' );
198 } else {
199 // Make sure the client is uploading the correct chunk with a matching offset.
200 if ( $preAppendOffset == $offset ) {
201 // Update local chunk index for the current chunk
202 $this->mChunkIndex++;
203 try {
204 # For some reason mTempPath is set to first part
205 $oldTemp = $this->mTempPath;
206 $this->mTempPath = $chunkPath;
207 $this->verifyChunk();
208 $this->mTempPath = $oldTemp;
209 } catch ( UploadChunkVerificationException $e ) {
210 return Status::newFatal( $e->getMessage() );
211 }
212 $status = $this->outputChunk( $chunkPath );
213 if ( $status->isGood() ) {
214 // Update local offset:
215 $this->mOffset = $preAppendOffset + $chunkSize;
216 // Update chunk table status db
217 $this->updateChunkStatus();
218 }
219 } else {
220 $status = Status::newFatal( 'invalid-chunk-offset' );
221 }
222 }
223 return $status;
224 }
225
226 /**
227 * Update the chunk db table with the current status:
228 */
229 private function updateChunkStatus() {
230 wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
231 $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
232
233 $dbw = $this->repo->getMasterDb();
234 // Use a quick transaction since we will upload the full temp file into shared
235 // storage, which takes time for large files. We don't want to hold locks then.
236 $dbw->begin( __METHOD__ );
237 $dbw->update(
238 'uploadstash',
239 array(
240 'us_status' => 'chunks',
241 'us_chunk_inx' => $this->getChunkIndex(),
242 'us_size' => $this->getOffset()
243 ),
244 array( 'us_key' => $this->mFileKey ),
245 __METHOD__
246 );
247 $dbw->commit( __METHOD__ );
248 }
249
250 /**
251 * Get the chunk db state and populate update relevant local values
252 */
253 private function getChunkStatus() {
254 // get Master db to avoid race conditions.
255 // Otherwise, if chunk upload time < replag there will be spurious errors
256 $dbw = $this->repo->getMasterDb();
257 $row = $dbw->selectRow(
258 'uploadstash',
259 array(
260 'us_chunk_inx',
261 'us_size',
262 'us_path',
263 ),
264 array( 'us_key' => $this->mFileKey ),
265 __METHOD__
266 );
267 // Handle result:
268 if ( $row ) {
269 $this->mChunkIndex = $row->us_chunk_inx;
270 $this->mOffset = $row->us_size;
271 $this->mVirtualTempPath = $row->us_path;
272 }
273 }
274
275 /**
276 * Get the current Chunk index
277 * @return Integer index of the current chunk
278 */
279 private function getChunkIndex() {
280 if ( $this->mChunkIndex !== null ) {
281 return $this->mChunkIndex;
282 }
283 return 0;
284 }
285
286 /**
287 * Gets the current offset in fromt the stashedupload table
288 * @return Integer current byte offset of the chunk file set
289 */
290 private function getOffset() {
291 if ( $this->mOffset !== null ) {
292 return $this->mOffset;
293 }
294 return 0;
295 }
296
297 /**
298 * Output the chunk to disk
299 *
300 * @param $chunkPath string
301 * @throws UploadChunkFileException
302 * @return FileRepoStatus
303 */
304 private function outputChunk( $chunkPath ) {
305 // Key is fileKey + chunk index
306 $fileKey = $this->getChunkFileKey();
307
308 // Store the chunk per its indexed fileKey:
309 $hashPath = $this->repo->getHashPath( $fileKey );
310 $storeStatus = $this->repo->quickImport( $chunkPath,
311 $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" );
312
313 // Check for error in stashing the chunk:
314 if ( ! $storeStatus->isOK() ) {
315 $error = $storeStatus->getErrorsArray();
316 $error = reset( $error );
317 if ( ! count( $error ) ) {
318 $error = $storeStatus->getWarningsArray();
319 $error = reset( $error );
320 if ( ! count( $error ) ) {
321 $error = array( 'unknown', 'no error recorded' );
322 }
323 }
324 throw new UploadChunkFileException( "error storing file in '$chunkPath': " . implode( '; ', $error ) );
325 }
326 return $storeStatus;
327 }
328
329 private function getChunkFileKey( $index = null ) {
330 if ( $index === null ) {
331 $index = $this->getChunkIndex();
332 }
333 return $this->mFileKey . '.' . $index;
334 }
335
336 /**
337 * Verify that the chunk isn't really an evil html file
338 *
339 * @throws UploadChunkVerificationException
340 */
341 private function verifyChunk() {
342 // Rest mDesiredDestName here so we verify the name as if it were mFileKey
343 $oldDesiredDestName = $this->mDesiredDestName;
344 $this->mDesiredDestName = $this->mFileKey;
345 $this->mTitle = false;
346 $res = $this->verifyPartialFile();
347 $this->mDesiredDestName = $oldDesiredDestName;
348 $this->mTitle = false;
349 if ( is_array( $res ) ) {
350 throw new UploadChunkVerificationException( $res[0] );
351 }
352 }
353 }
354
355 class UploadChunkZeroLengthFileException extends MWException {};
356 class UploadChunkFileException extends MWException {};
357 class UploadChunkVerificationException extends MWException {};