a43989079c2a8c6f3bce2544a0d7eb8297795535
[lhc/web/wiklou.git] / includes / filerepo / backend / FileOp.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Aaron Schulz
6 */
7
8 /**
9 * Helper class for representing operations with transaction support.
10 * FileBackend::doOperations() will require these classes for supported operations.
11 *
12 * Use of large fields should be avoided as we want to be able to support
13 * potentially many FileOp classes in large arrays in memory.
14 *
15 * @ingroup FileBackend
16 * @since 1.19
17 */
18 abstract class FileOp {
19 /** $var Array */
20 protected $params = array();
21 /** $var FileBackendBase */
22 protected $backend;
23 /** @var TempFSFile|null */
24 protected $tmpSourceFile, $tmpDestFile;
25
26 protected $state = self::STATE_NEW; // integer
27 protected $failed = false; // boolean
28 protected $useBackups = true; // boolean
29 protected $useLatest = true; // boolean
30 protected $destSameAsSource = false; // boolean
31 protected $destAlreadyExists = false; // boolean
32
33 /* Object life-cycle */
34 const STATE_NEW = 1;
35 const STATE_CHECKED = 2;
36 const STATE_ATTEMPTED = 3;
37 const STATE_DONE = 4;
38
39 /**
40 * Build a new file operation transaction
41 *
42 * @params $backend FileBackend
43 * @params $params Array
44 */
45 final public function __construct( FileBackendBase $backend, array $params ) {
46 $this->backend = $backend;
47 foreach ( $this->allowedParams() as $name ) {
48 if ( isset( $params[$name] ) ) {
49 $this->params[$name] = $params[$name];
50 }
51 }
52 $this->params = $params;
53 }
54
55 /**
56 * Disable file backups for this operation
57 *
58 * @return void
59 */
60 final protected function disableBackups() {
61 $this->useBackups = false;
62 }
63
64 /**
65 * Allow stale data for file reads and existence checks.
66 * If this is called, then disableBackups() should also be called
67 * unless the affected files are known to have not changed recently.
68 *
69 * @return void
70 */
71 final protected function allowStaleReads() {
72 $this->useLatest = false;
73 }
74
75 /**
76 * Attempt a series of file operations.
77 * Callers are responsible for handling file locking.
78 *
79 * @param $performOps Array List of FileOp operations
80 * @param $opts Array Batch operation options
81 * @return Status
82 */
83 final public static function attemptBatch( array $performOps, array $opts ) {
84 $status = Status::newGood();
85
86 $allowStale = isset( $opts['allowStale'] ) && $opts['allowStale'];
87 $ignoreErrors = isset( $opts['force'] ) && $opts['force'];
88 $predicates = FileOp::newPredicates(); // account for previous op in prechecks
89 // Do pre-checks for each operation; abort on failure...
90 foreach ( $performOps as $index => $fileOp ) {
91 if ( $allowStale ) {
92 $fileOp->allowStaleReads(); // allow potentially stale reads
93 }
94 $status->merge( $fileOp->precheck( $predicates ) );
95 if ( !$status->isOK() ) { // operation failed?
96 if ( $ignoreErrors ) {
97 ++$status->failCount;
98 $status->success[$index] = false;
99 } else {
100 return $status;
101 }
102 }
103 }
104
105 // Attempt each operation; abort on failure...
106 foreach ( $performOps as $index => $fileOp ) {
107 if ( $fileOp->failed() ) {
108 continue; // nothing to do
109 } elseif ( $ignoreErrors ) {
110 $fileOp->disableBackups(); // no chance of revert() calls
111 }
112 $status->merge( $fileOp->attempt() );
113 if ( !$status->isOK() ) { // operation failed?
114 if ( $ignoreErrors ) {
115 ++$status->failCount;
116 $status->success[$index] = false;
117 } else {
118 // Revert everything done so far and abort.
119 // Do this by reverting each previous operation in reverse order.
120 $pos = $index - 1; // last one failed; no need to revert()
121 while ( $pos >= 0 ) {
122 if ( !$performOps[$pos]->failed() ) {
123 $status->merge( $performOps[$pos]->revert() );
124 }
125 $pos--;
126 }
127 return $status;
128 }
129 }
130 }
131
132 // Finish each operation...
133 foreach ( $performOps as $index => $fileOp ) {
134 if ( $fileOp->failed() ) {
135 continue; // nothing to do
136 }
137 $subStatus = $fileOp->finish();
138 if ( $subStatus->isOK() ) {
139 ++$status->successCount;
140 $status->success[$index] = true;
141 } else {
142 ++$status->failCount;
143 $status->success[$index] = false;
144 }
145 $status->merge( $subStatus );
146 }
147
148 // Make sure status is OK, despite any finish() fatals
149 $status->setResult( true, $status->value );
150
151 return $status;
152 }
153
154 /**
155 * Get the value of the parameter with the given name.
156 * Returns null if the parameter is not set.
157 *
158 * @param $name string
159 * @return mixed
160 */
161 final public function getParam( $name ) {
162 return isset( $this->params[$name] ) ? $this->params[$name] : null;
163 }
164
165 /**
166 * Check if this operation failed precheck() or attempt()
167 * @return type
168 */
169 final public function failed() {
170 return $this->failed;
171 }
172
173 /**
174 * Get a new empty predicates array for precheck()
175 *
176 * @return Array
177 */
178 final public static function newPredicates() {
179 return array( 'exists' => array() );
180 }
181
182 /**
183 * Check preconditions of the operation without writing anything
184 *
185 * @param $predicates Array
186 * @return Status
187 */
188 final public function precheck( array &$predicates ) {
189 if ( $this->state !== self::STATE_NEW ) {
190 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
191 }
192 $this->state = self::STATE_CHECKED;
193 $status = $this->doPrecheck( $predicates );
194 if ( !$status->isOK() ) {
195 $this->failed = true;
196 }
197 return $status;
198 }
199
200 /**
201 * Attempt the operation, backing up files as needed; this must be reversible
202 *
203 * @return Status
204 */
205 final public function attempt() {
206 if ( $this->state !== self::STATE_CHECKED ) {
207 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
208 } elseif ( $this->failed ) { // failed precheck
209 return Status::newFatal( 'fileop-fail-attempt-precheck' );
210 }
211 $this->state = self::STATE_ATTEMPTED;
212 $status = $this->doAttempt();
213 if ( !$status->isOK() ) {
214 $this->failed = true;
215 $this->logFailure( 'attempt' );
216 }
217 return $status;
218 }
219
220 /**
221 * Revert the operation; affected files are restored
222 *
223 * @return Status
224 */
225 final public function revert() {
226 if ( $this->state !== self::STATE_ATTEMPTED ) {
227 return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state );
228 }
229 $this->state = self::STATE_DONE;
230 if ( $this->failed ) {
231 $status = Status::newGood(); // nothing to revert
232 } else {
233 $status = $this->doRevert();
234 if ( !$status->isOK() ) {
235 $this->logFailure( 'revert' );
236 }
237 }
238 return $status;
239 }
240
241 /**
242 * Finish the operation; this may be irreversible
243 *
244 * @return Status
245 */
246 final public function finish() {
247 if ( $this->state !== self::STATE_ATTEMPTED ) {
248 return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state );
249 }
250 $this->state = self::STATE_DONE;
251 if ( $this->failed ) {
252 $status = Status::newGood(); // nothing to finish
253 } else {
254 $status = $this->doFinish();
255 }
256 return $status;
257 }
258
259 /**
260 * Get a list of storage paths read from for this operation
261 *
262 * @return Array
263 */
264 public function storagePathsRead() {
265 return array();
266 }
267
268 /**
269 * Get a list of storage paths written to for this operation
270 *
271 * @return Array
272 */
273 public function storagePathsChanged() {
274 return array();
275 }
276
277 /**
278 * @return Array List of allowed parameters
279 */
280 protected function allowedParams() {
281 return array();
282 }
283
284 /**
285 * @return Status
286 */
287 protected function doPrecheck( array &$predicates ) {
288 return Status::newGood();
289 }
290
291 /**
292 * @return Status
293 */
294 abstract protected function doAttempt();
295
296 /**
297 * @return Status
298 */
299 abstract protected function doRevert();
300
301 /**
302 * @return Status
303 */
304 protected function doFinish() {
305 return Status::newGood();
306 }
307
308 /**
309 * Check if the destination file exists and update the
310 * destAlreadyExists member variable. A bad status will
311 * be returned if there is no chance it can be overwritten.
312 *
313 * @param $predicates Array
314 * @return Status
315 */
316 protected function precheckDestExistence( array $predicates ) {
317 $status = Status::newGood();
318 if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
319 $this->destAlreadyExists = true;
320 if ( !$this->getParam( 'overwriteDest' ) && !$this->getParam( 'overwriteSame' ) ) {
321 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
322 return $status;
323 }
324 } else {
325 $this->destAlreadyExists = false;
326 }
327 return $status;
328 }
329
330 /**
331 * Backup any file at the source to a temporary file
332 *
333 * @return Status
334 */
335 protected function backupSource() {
336 $status = Status::newGood();
337 if ( $this->useBackups ) {
338 // Check if a file already exists at the source...
339 $params = array( 'src' => $this->params['src'], 'latest' => $this->useLatest );
340 if ( $this->backend->fileExists( $params ) ) {
341 // Create a temporary backup copy...
342 $this->tmpSourcePath = $this->backend->getLocalCopy( $params );
343 if ( $this->tmpSourcePath === null ) {
344 $status->fatal( 'backend-fail-backup', $this->params['src'] );
345 return $status;
346 }
347 }
348 }
349 return $status;
350 }
351
352 /**
353 * Backup the file at the destination to a temporary file.
354 * Don't bother backing it up unless we might overwrite the file.
355 * This assumes that the destination is in the backend and that
356 * the source is either in the backend or on the file system.
357 * This also handles the 'overwriteSame' check logic and updates
358 * the destSameAsSource member variable.
359 *
360 * @return Status
361 */
362 protected function checkAndBackupDest() {
363 $status = Status::newGood();
364 $this->destSameAsSource = false;
365
366 if ( $this->getParam( 'overwriteDest' ) ) {
367 if ( $this->useBackups ) {
368 // Create a temporary backup copy...
369 $params = array( 'src' => $this->params['dst'], 'latest' => $this->useLatest );
370 $this->tmpDestFile = $this->backend->getLocalCopy( $params );
371 if ( !$this->tmpDestFile ) {
372 $status->fatal( 'backend-fail-backup', $this->params['dst'] );
373 return $status;
374 }
375 }
376 } elseif ( $this->getParam( 'overwriteSame' ) ) {
377 $shash = $this->getSourceSha1Base36();
378 // If there is a single source, then we can do some checks already.
379 // For things like concatenate(), we would need to build a temp file
380 // first and thus don't support 'overwriteSame' ($shash is null).
381 if ( $shash !== null ) {
382 $dhash = $this->getFileSha1Base36( $this->params['dst'] );
383 if ( !strlen( $shash ) || !strlen( $dhash ) ) {
384 $status->fatal( 'backend-fail-hashes' );
385 } elseif ( $shash !== $dhash ) {
386 // Give an error if the files are not identical
387 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
388 } else {
389 $this->destSameAsSource = true;
390 }
391 return $status; // do nothing; either OK or bad status
392 }
393 } else {
394 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
395 return $status;
396 }
397
398 return $status;
399 }
400
401 /**
402 * checkAndBackupDest() helper function to get the source file Sha1.
403 * Returns false on failure and null if there is no single source.
404 *
405 * @return string|false|null
406 */
407 protected function getSourceSha1Base36() {
408 return null; // N/A
409 }
410
411 /**
412 * checkAndBackupDest() helper function to get the Sha1 of a file.
413 *
414 * @return string|false False on failure
415 */
416 protected function getFileSha1Base36( $path ) {
417 // Source file is in backend
418 if ( FileBackend::isStoragePath( $path ) ) {
419 // For some backends (e.g. Swift, Azure) we can get
420 // standard hashes to use for this types of comparisons.
421 $params = array( 'src' => $path, 'latest' => $this->useLatest );
422 $hash = $this->backend->getFileSha1Base36( $params );
423 // Source file is on file system
424 } else {
425 wfSuppressWarnings();
426 $hash = sha1_file( $path );
427 wfRestoreWarnings();
428 if ( $hash !== false ) {
429 $hash = wfBaseConvert( $hash, 16, 36, 31 );
430 }
431 }
432 return $hash;
433 }
434
435 /**
436 * Restore any temporary source backup file
437 *
438 * @return Status
439 */
440 protected function restoreSource() {
441 $status = Status::newGood();
442 // Restore any file that was at the destination
443 if ( $this->tmpSourcePath !== null ) {
444 $params = array(
445 'src' => $this->tmpSourcePath,
446 'dst' => $this->params['src'],
447 'overwriteDest' => true
448 );
449 $status = $this->backend->storeInternal( $params );
450 if ( !$status->isOK() ) {
451 return $status;
452 }
453 }
454 return $status;
455 }
456
457 /**
458 * Restore any temporary destination backup file
459 *
460 * @return Status
461 */
462 protected function restoreDest() {
463 $status = Status::newGood();
464 // Restore any file that was at the destination
465 if ( $this->tmpDestFile ) {
466 $params = array(
467 'src' => $this->tmpDestFile->getPath(),
468 'dst' => $this->params['dst'],
469 'overwriteDest' => true
470 );
471 $status = $this->backend->storeInternal( $params );
472 if ( !$status->isOK() ) {
473 return $status;
474 }
475 }
476 return $status;
477 }
478
479 /**
480 * Check if a file will exist in storage when this operation is attempted
481 *
482 * @param $source string Storage path
483 * @param $predicates Array
484 * @return bool
485 */
486 final protected function fileExists( $source, array $predicates ) {
487 if ( isset( $predicates['exists'][$source] ) ) {
488 return $predicates['exists'][$source]; // previous op assures this
489 } else {
490 $params = array( 'src' => $source, 'latest' => $this->useLatest );
491 return $this->backend->fileExists( $params );
492 }
493 }
494
495 /**
496 * Log a file operation failure and preserve any temp files
497 *
498 * @param $fileOp FileOp
499 * @return void
500 */
501 final protected function logFailure( $action ) {
502 $params = $this->params;
503 $params['failedAction'] = $action;
504 // Preserve backup files just in case (for recovery)
505 if ( $this->tmpSourceFile ) {
506 $this->tmpSourceFile->preserve(); // don't purge
507 $params['srcBackupPath'] = $this->tmpSourceFile->getPath();
508 }
509 if ( $this->tmpDestFile ) {
510 $this->tmpDestFile->preserve(); // don't purge
511 $params['dstBackupPath'] = $this->tmpDestFile->getPath();
512 }
513 try {
514 wfDebugLog( 'FileOperation',
515 get_class( $this ) . ' failed:' . serialize( $params ) );
516 } catch ( Exception $e ) {
517 // bad config? debug log error?
518 }
519 }
520 }
521
522 /**
523 * Store a file into the backend from a file on the file system.
524 * Parameters similar to FileBackend::storeInternal(), which include:
525 * src : source path on file system
526 * dst : destination storage path
527 * overwriteDest : do nothing and pass if an identical file exists at destination
528 * overwriteSame : override any existing file at destination
529 */
530 class StoreFileOp extends FileOp {
531 protected function allowedParams() {
532 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
533 }
534
535 protected function doPrecheck( array &$predicates ) {
536 $status = Status::newGood();
537 // Check if destination file exists
538 $status->merge( $this->precheckDestExistence( $predicates ) );
539 if ( !$status->isOK() ) {
540 return $status;
541 }
542 // Check if the source file exists on the file system
543 if ( !is_file( $this->params['src'] ) ) {
544 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
545 return $status;
546 }
547 // Update file existence predicates
548 $predicates['exists'][$this->params['dst']] = true;
549 return $status;
550 }
551
552 protected function doAttempt() {
553 $status = Status::newGood();
554 // Create a destination backup copy as needed
555 if ( $this->destAlreadyExists ) {
556 $status->merge( $this->checkAndBackupDest() );
557 if ( !$status->isOK() ) {
558 return $status;
559 }
560 }
561 // Store the file at the destination
562 if ( !$this->destSameAsSource ) {
563 $status->merge( $this->backend->storeInternal( $this->params ) );
564 }
565 return $status;
566 }
567
568 protected function doRevert() {
569 $status = Status::newGood();
570 if ( !$this->destSameAsSource ) {
571 // Restore any file that was at the destination,
572 // overwritting what was put there in attempt()
573 $status->merge( $this->restoreDest() );
574 }
575 return $status;
576 }
577
578 protected function getSourceSha1Base36() {
579 return $this->getFileSha1Base36( $this->params['src'] );
580 }
581
582 public function storagePathsChanged() {
583 return array( $this->params['dst'] );
584 }
585 }
586
587 /**
588 * Create a file in the backend with the given content.
589 * Parameters similar to FileBackend::create(), which include:
590 * content : a string of raw file contents
591 * dst : destination storage path
592 * overwriteDest : do nothing and pass if an identical file exists at destination
593 * overwriteSame : override any existing file at destination
594 */
595 class CreateFileOp extends FileOp {
596 protected function allowedParams() {
597 return array( 'content', 'dst', 'overwriteDest', 'overwriteSame' );
598 }
599
600 protected function doPrecheck( array &$predicates ) {
601 $status = Status::newGood();
602 // Check if destination file exists
603 $status->merge( $this->precheckDestExistence( $predicates ) );
604 if ( !$status->isOK() ) {
605 return $status;
606 }
607 // Update file existence predicates
608 $predicates['exists'][$this->params['dst']] = true;
609 return $status;
610 }
611
612 protected function doAttempt() {
613 $status = Status::newGood();
614 // Create a destination backup copy as needed
615 if ( $this->destAlreadyExists ) {
616 $status->merge( $this->checkAndBackupDest() );
617 if ( !$status->isOK() ) {
618 return $status;
619 }
620 }
621 // Create the file at the destination
622 if ( !$this->destSameAsSource ) {
623 $status->merge( $this->backend->createInternal( $this->params ) );
624 }
625 return $status;
626 }
627
628 protected function doRevert() {
629 $status = Status::newGood();
630 if ( !$this->destSameAsSource ) {
631 // Restore any file that was at the destination,
632 // overwritting what was put there in attempt()
633 $status->merge( $this->restoreDest() );
634 }
635 return $status;
636 }
637
638 protected function getSourceSha1Base36() {
639 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
640 }
641
642 public function storagePathsChanged() {
643 return array( $this->params['dst'] );
644 }
645 }
646
647 /**
648 * Copy a file from one storage path to another in the backend.
649 * Parameters similar to FileBackend::copy(), which include:
650 * src : source storage path
651 * dst : destination storage path
652 * overwriteDest : do nothing and pass if an identical file exists at destination
653 * overwriteSame : override any existing file at destination
654 */
655 class CopyFileOp extends FileOp {
656 protected function allowedParams() {
657 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
658 }
659
660 protected function doPrecheck( array &$predicates ) {
661 $status = Status::newGood();
662 // Check if destination file exists
663 $status->merge( $this->precheckDestExistence( $predicates ) );
664 if ( !$status->isOK() ) {
665 return $status;
666 }
667 // Check if the source file exists
668 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
669 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
670 return $status;
671 }
672 // Update file existence predicates
673 $predicates['exists'][$this->params['dst']] = true;
674 return $status;
675 }
676
677 protected function doAttempt() {
678 $status = Status::newGood();
679 // Create a destination backup copy as needed
680 if ( $this->destAlreadyExists ) {
681 $status->merge( $this->checkAndBackupDest() );
682 if ( !$status->isOK() ) {
683 return $status;
684 }
685 }
686 // Copy the file into the destination
687 if ( !$this->destSameAsSource ) {
688 $status->merge( $this->backend->copyInternal( $this->params ) );
689 }
690 return $status;
691 }
692
693 protected function doRevert() {
694 $status = Status::newGood();
695 if ( !$this->destSameAsSource ) {
696 // Restore any file that was at the destination,
697 // overwritting what was put there in attempt()
698 $status->merge( $this->restoreDest() );
699 }
700 return $status;
701 }
702
703 protected function getSourceSha1Base36() {
704 return $this->getFileSha1Base36( $this->params['src'] );
705 }
706
707 public function storagePathsRead() {
708 return array( $this->params['src'] );
709 }
710
711 public function storagePathsChanged() {
712 return array( $this->params['dst'] );
713 }
714 }
715
716 /**
717 * Move a file from one storage path to another in the backend.
718 * Parameters similar to FileBackend::move(), which include:
719 * src : source storage path
720 * dst : destination storage path
721 * overwriteDest : do nothing and pass if an identical file exists at destination
722 * overwriteSame : override any existing file at destination
723 */
724 class MoveFileOp extends FileOp {
725 protected function allowedParams() {
726 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
727 }
728
729 protected function doPrecheck( array &$predicates ) {
730 $status = Status::newGood();
731 // Check if destination file exists
732 $status->merge( $this->precheckDestExistence( $predicates ) );
733 if ( !$status->isOK() ) {
734 return $status;
735 }
736 // Check if the source file exists
737 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
738 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
739 return $status;
740 }
741 // Update file existence predicates
742 $predicates['exists'][$this->params['src']] = false;
743 $predicates['exists'][$this->params['dst']] = true;
744 return $status;
745 }
746
747 protected function doAttempt() {
748 $status = Status::newGood();
749 // Create a destination backup copy as needed
750 if ( $this->destAlreadyExists ) {
751 $status->merge( $this->checkAndBackupDest() );
752 if ( !$status->isOK() ) {
753 return $status;
754 }
755 }
756 if ( !$this->destSameAsSource ) {
757 // Move the file into the destination
758 $status->merge( $this->backend->moveInternal( $this->params ) );
759 } else {
760 // Create a source backup copy as needed
761 $status->merge( $this->backupSource() );
762 if ( !$status->isOK() ) {
763 return $status;
764 }
765 // Just delete source as the destination needs no changes
766 $params = array( 'src' => $this->params['src'] );
767 $status->merge( $this->backend->deleteInternal( $params ) );
768 if ( !$status->isOK() ) {
769 return $status;
770 }
771 }
772 return $status;
773 }
774
775 protected function doRevert() {
776 $status = Status::newGood();
777 if ( !$this->destSameAsSource ) {
778 // Move the file back to the source
779 $params = array(
780 'src' => $this->params['dst'],
781 'dst' => $this->params['src']
782 );
783 $status->merge( $this->backend->moveInternal( $params ) );
784 if ( !$status->isOK() ) {
785 return $status; // also can't restore any dest file
786 }
787 // Restore any file that was at the destination
788 $status->merge( $this->restoreDest() );
789 } else {
790 // Restore any source file
791 return $this->restoreSource();
792 }
793
794 return $status;
795 }
796
797 protected function getSourceSha1Base36() {
798 return $this->getFileSha1Base36( $this->params['src'] );
799 }
800
801 public function storagePathsRead() {
802 return array( $this->params['src'] );
803 }
804
805 public function storagePathsChanged() {
806 return array( $this->params['dst'] );
807 }
808 }
809
810 /**
811 * Combines files from severals storage paths into a new file in the backend.
812 * Parameters similar to FileBackend::concatenate(), which include:
813 * srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
814 * dst : destination file system path to 0-byte temp file
815 */
816 class ConcatenateFileOp extends FileOp {
817 protected function allowedParams() {
818 return array( 'srcs', 'dst' );
819 }
820
821 protected function doPrecheck( array &$predicates ) {
822 $status = Status::newGood();
823 // Check destination temp file
824 wfSuppressWarnings();
825 $ok = ( is_file( $this->params['dst'] ) && !filesize( $this->params['dst'] ) );
826 wfRestoreWarnings();
827 if ( !$ok ) { // not present or not empty
828 $status->fatal( 'backend-fail-opentemp', $this->params['dst'] );
829 return $status;
830 }
831 // Check that source files exists
832 foreach ( $this->params['srcs'] as $source ) {
833 if ( !$this->fileExists( $source, $predicates ) ) {
834 $status->fatal( 'backend-fail-notexists', $source );
835 return $status;
836 }
837 }
838 return $status;
839 }
840
841 protected function doAttempt() {
842 $status = Status::newGood();
843 // Concatenate the file at the destination
844 $status->merge( $this->backend->concatenateInternal( $this->params ) );
845 return $status;
846 }
847
848 protected function doRevert() {
849 $status = Status::newGood();
850 // Clear out the temp file back to 0-bytes
851 wfSuppressWarnings();
852 $ok = file_put_contents( $this->params['dst'], '' );
853 wfRestoreWarnings();
854 if ( !$ok ) {
855 $status->fatal( 'backend-fail-writetemp', $this->params['dst'] );
856 }
857 return $status;
858 }
859
860 public function storagePathsRead() {
861 return $this->params['srcs'];
862 }
863 }
864
865 /**
866 * Delete a file at the storage path.
867 * Parameters similar to FileBackend::delete(), which include:
868 * src : source storage path
869 * ignoreMissingSource : don't return an error if the file does not exist
870 */
871 class DeleteFileOp extends FileOp {
872 protected $needsDelete = true;
873
874 protected function allowedParams() {
875 return array( 'src', 'ignoreMissingSource' );
876 }
877
878 protected function doPrecheck( array &$predicates ) {
879 $status = Status::newGood();
880 // Check if the source file exists
881 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
882 if ( !$this->getParam( 'ignoreMissingSource' ) ) {
883 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
884 return $status;
885 }
886 $this->needsDelete = false;
887 }
888 // Update file existence predicates
889 $predicates['exists'][$this->params['src']] = false;
890 return $status;
891 }
892
893 protected function doAttempt() {
894 $status = Status::newGood();
895 if ( $this->needsDelete ) {
896 // Create a source backup copy as needed
897 $status->merge( $this->backupSource() );
898 if ( !$status->isOK() ) {
899 return $status;
900 }
901 // Delete the source file
902 $status->merge( $this->backend->deleteInternal( $this->params ) );
903 if ( !$status->isOK() ) {
904 return $status;
905 }
906 }
907 return $status;
908 }
909
910 protected function doRevert() {
911 // Restore any source file that we deleted
912 return $this->restoreSource();
913 }
914
915 public function storagePathsChanged() {
916 return array( $this->params['src'] );
917 }
918 }
919
920 /**
921 * Placeholder operation that has no params and does nothing
922 */
923 class NullFileOp extends FileOp {
924 protected function doAttempt() {
925 return Status::newGood();
926 }
927
928 protected function doRevert() {
929 return Status::newGood();
930 }
931 }