11fe865b5633a4111802f0d4d859a73b759853ac
9 * Helper class for representing operations with transaction support.
10 * FileBackend::doOperations() will require these classes for supported operations.
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.
15 * @ingroup FileBackend
18 abstract class FileOp
{
20 protected $params = array();
21 /** $var FileBackendBase */
23 /** @var TempFSFile|null */
24 protected $tmpSourceFile, $tmpDestFile;
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
33 /* Object life-cycle */
35 const STATE_CHECKED
= 2;
36 const STATE_ATTEMPTED
= 3;
40 * Build a new file operation transaction
42 * @params $backend FileBackend
43 * @params $params Array
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];
52 $this->params
= $params;
56 * Disable file backups for this operation
60 final protected function disableBackups() {
61 $this->useBackups
= false;
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.
71 final protected function allowStaleReads() {
72 $this->useLatest
= false;
76 * Attempt a series of file operations.
77 * Callers are responsible for handling file locking.
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.
86 * @param $performOps Array List of FileOp operations
87 * @param $opts Array Batch operation options
90 final public static function attemptBatch( array $performOps, array $opts ) {
91 $status = Status
::newGood();
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 ) {
99 $fileOp->allowStaleReads(); // allow potentially stale reads
101 $status->merge( $fileOp->precheck( $predicates ) );
102 if ( !$status->isOK() ) { // operation failed?
103 if ( $ignoreErrors ) {
104 ++
$status->failCount
;
105 $status->success
[$index] = false;
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
119 $status->merge( $fileOp->attempt() );
120 if ( !$status->isOK() ) { // operation failed?
121 if ( $ignoreErrors ) {
122 ++
$status->failCount
;
123 $status->success
[$index] = false;
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() );
139 $wasOk = $status->isOK();
140 // Finish each operation...
141 foreach ( $performOps as $index => $fileOp ) {
142 if ( $fileOp->failed() ) {
143 continue; // nothing to do
145 $subStatus = $fileOp->finish();
146 if ( $subStatus->isOK() ) {
147 ++
$status->successCount
;
148 $status->success
[$index] = true;
150 ++
$status->failCount
;
151 $status->success
[$index] = false;
153 $status->merge( $subStatus );
156 // Make sure status is OK, despite any finish() fatals
157 $status->setResult( $wasOk, $status->value
);
163 * Get the value of the parameter with the given name.
164 * Returns null if the parameter is not set.
166 * @param $name string
169 final public function getParam( $name ) {
170 return isset( $this->params
[$name] ) ?
$this->params
[$name] : null;
174 * Check if this operation failed precheck() or attempt()
177 final public function failed() {
178 return $this->failed
;
182 * Get a new empty predicates array for precheck()
186 final public static function newPredicates() {
187 return array( 'exists' => array() );
191 * Check preconditions of the operation without writing anything
193 * @param $predicates Array
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
);
200 $this->state
= self
::STATE_CHECKED
;
201 $status = $this->doPrecheck( $predicates );
202 if ( !$status->isOK() ) {
203 $this->failed
= true;
209 * Attempt the operation, backing up files as needed; this must be reversible
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' );
219 $this->state
= self
::STATE_ATTEMPTED
;
220 $status = $this->doAttempt();
221 if ( !$status->isOK() ) {
222 $this->failed
= true;
223 $this->logFailure( 'attempt' );
229 * Revert the operation; affected files are restored
233 final public function revert() {
234 if ( $this->state
!== self
::STATE_ATTEMPTED
) {
235 return Status
::newFatal( 'fileop-fail-state', self
::STATE_ATTEMPTED
, $this->state
);
237 $this->state
= self
::STATE_DONE
;
238 if ( $this->failed
) {
239 $status = Status
::newGood(); // nothing to revert
241 $status = $this->doRevert();
242 if ( !$status->isOK() ) {
243 $this->logFailure( 'revert' );
250 * Finish the operation; this may be irreversible
254 final public function finish() {
255 if ( $this->state
!== self
::STATE_ATTEMPTED
) {
256 return Status
::newFatal( 'fileop-fail-state', self
::STATE_ATTEMPTED
, $this->state
);
258 $this->state
= self
::STATE_DONE
;
259 if ( $this->failed
) {
260 $status = Status
::newGood(); // nothing to finish
262 $status = $this->doFinish();
268 * Get a list of storage paths read from for this operation
272 public function storagePathsRead() {
277 * Get a list of storage paths written to for this operation
281 public function storagePathsChanged() {
286 * @return Array List of allowed parameters
288 protected function allowedParams() {
295 protected function doPrecheck( array &$predicates ) {
296 return Status
::newGood();
302 abstract protected function doAttempt();
307 abstract protected function doRevert();
312 protected function doFinish() {
313 return Status
::newGood();
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.
321 * @param $predicates Array
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'] );
333 $this->destAlreadyExists
= false;
339 * Backup any file at the source to a temporary file
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'] );
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.
370 protected function checkAndBackupDest() {
371 $status = Status
::newGood();
372 $this->destSameAsSource
= false;
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'] );
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'] );
397 $this->destSameAsSource
= true;
399 return $status; // do nothing; either OK or bad status
402 $status->fatal( 'backend-fail-alreadyexists', $this->params
['dst'] );
410 * checkAndBackupDest() helper function to get the source file Sha1.
411 * Returns false on failure and null if there is no single source.
413 * @return string|false|null
415 protected function getSourceSha1Base36() {
420 * checkAndBackupDest() helper function to get the Sha1 of a file.
422 * @return string|false False on failure
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
433 wfSuppressWarnings();
434 $hash = sha1_file( $path );
436 if ( $hash !== false ) {
437 $hash = wfBaseConvert( $hash, 16, 36, 31 );
444 * Restore any temporary source backup file
448 protected function restoreSource() {
449 $status = Status
::newGood();
450 // Restore any file that was at the destination
451 if ( $this->tmpSourcePath
!== null ) {
453 'src' => $this->tmpSourcePath
,
454 'dst' => $this->params
['src'],
455 'overwriteDest' => true
457 $status = $this->backend
->storeInternal( $params );
458 if ( !$status->isOK() ) {
466 * Restore any temporary destination backup file
470 protected function restoreDest() {
471 $status = Status
::newGood();
472 // Restore any file that was at the destination
473 if ( $this->tmpDestFile
) {
475 'src' => $this->tmpDestFile
->getPath(),
476 'dst' => $this->params
['dst'],
477 'overwriteDest' => true
479 $status = $this->backend
->storeInternal( $params );
480 if ( !$status->isOK() ) {
488 * Check if a file will exist in storage when this operation is attempted
490 * @param $source string Storage path
491 * @param $predicates Array
494 final protected function fileExists( $source, array $predicates ) {
495 if ( isset( $predicates['exists'][$source] ) ) {
496 return $predicates['exists'][$source]; // previous op assures this
498 $params = array( 'src' => $source, 'latest' => $this->useLatest
);
499 return $this->backend
->fileExists( $params );
504 * Log a file operation failure and preserve any temp files
506 * @param $fileOp FileOp
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();
517 if ( $this->tmpDestFile
) {
518 $this->tmpDestFile
->preserve(); // don't purge
519 $params['dstBackupPath'] = $this->tmpDestFile
->getPath();
522 wfDebugLog( 'FileOperation',
523 get_class( $this ) . ' failed:' . serialize( $params ) );
524 } catch ( Exception
$e ) {
525 // bad config? debug log error?
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
538 class StoreFileOp
extends FileOp
{
539 protected function allowedParams() {
540 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
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() ) {
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'] );
555 // Update file existence predicates
556 $predicates['exists'][$this->params
['dst']] = true;
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() ) {
569 // Store the file at the destination
570 if ( !$this->destSameAsSource
) {
571 $status->merge( $this->backend
->storeInternal( $this->params
) );
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() );
586 protected function getSourceSha1Base36() {
587 return $this->getFileSha1Base36( $this->params
['src'] );
590 public function storagePathsChanged() {
591 return array( $this->params
['dst'] );
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
603 class CreateFileOp
extends FileOp
{
604 protected function allowedParams() {
605 return array( 'content', 'dst', 'overwriteDest', 'overwriteSame' );
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() ) {
615 // Update file existence predicates
616 $predicates['exists'][$this->params
['dst']] = true;
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() ) {
629 // Create the file at the destination
630 if ( !$this->destSameAsSource
) {
631 $status->merge( $this->backend
->createInternal( $this->params
) );
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() );
646 protected function getSourceSha1Base36() {
647 return wfBaseConvert( sha1( $this->params
['content'] ), 16, 36, 31 );
650 public function storagePathsChanged() {
651 return array( $this->params
['dst'] );
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
663 class CopyFileOp
extends FileOp
{
664 protected function allowedParams() {
665 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
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() ) {
675 // Check if the source file exists
676 if ( !$this->fileExists( $this->params
['src'], $predicates ) ) {
677 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
680 // Update file existence predicates
681 $predicates['exists'][$this->params
['dst']] = true;
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() ) {
694 // Copy the file into the destination
695 if ( !$this->destSameAsSource
) {
696 $status->merge( $this->backend
->copyInternal( $this->params
) );
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() );
711 protected function getSourceSha1Base36() {
712 return $this->getFileSha1Base36( $this->params
['src'] );
715 public function storagePathsRead() {
716 return array( $this->params
['src'] );
719 public function storagePathsChanged() {
720 return array( $this->params
['dst'] );
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
732 class MoveFileOp
extends FileOp
{
733 protected function allowedParams() {
734 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
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() ) {
744 // Check if the source file exists
745 if ( !$this->fileExists( $this->params
['src'], $predicates ) ) {
746 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
749 // Update file existence predicates
750 $predicates['exists'][$this->params
['src']] = false;
751 $predicates['exists'][$this->params
['dst']] = true;
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() ) {
764 if ( !$this->destSameAsSource
) {
765 // Move the file into the destination
766 $status->merge( $this->backend
->moveInternal( $this->params
) );
768 // Create a source backup copy as needed
769 $status->merge( $this->backupSource() );
770 if ( !$status->isOK() ) {
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() ) {
783 protected function doRevert() {
784 $status = Status
::newGood();
785 if ( !$this->destSameAsSource
) {
786 // Move the file back to the source
788 'src' => $this->params
['dst'],
789 'dst' => $this->params
['src']
791 $status->merge( $this->backend
->moveInternal( $params ) );
792 if ( !$status->isOK() ) {
793 return $status; // also can't restore any dest file
795 // Restore any file that was at the destination
796 $status->merge( $this->restoreDest() );
798 // Restore any source file
799 return $this->restoreSource();
805 protected function getSourceSha1Base36() {
806 return $this->getFileSha1Base36( $this->params
['src'] );
809 public function storagePathsRead() {
810 return array( $this->params
['src'] );
813 public function storagePathsChanged() {
814 return array( $this->params
['dst'] );
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
824 class DeleteFileOp
extends FileOp
{
825 protected $needsDelete = true;
827 protected function allowedParams() {
828 return array( 'src', 'ignoreMissingSource' );
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'] );
839 $this->needsDelete
= false;
841 // Update file existence predicates
842 $predicates['exists'][$this->params
['src']] = false;
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() ) {
854 // Delete the source file
855 $status->merge( $this->backend
->deleteInternal( $this->params
) );
856 if ( !$status->isOK() ) {
863 protected function doRevert() {
864 // Restore any source file that we deleted
865 return $this->restoreSource();
868 public function storagePathsChanged() {
869 return array( $this->params
['src'] );
874 * Placeholder operation that has no params and does nothing
876 class NullFileOp
extends FileOp
{
877 protected function doAttempt() {
878 return Status
::newGood();
881 protected function doRevert() {
882 return Status
::newGood();