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