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