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