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