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.
13 * Use of large fields should be avoided as we want to support
14 * potentially many FileOp classes in large arrays in memory.
16 * @ingroup FileBackend
19 abstract class FileOp
{
21 protected $params = array();
22 /** @var FileBackendBase */
25 protected $state = self
::STATE_NEW
; // integer
26 protected $failed = false; // boolean
27 protected $useLatest = true; // boolean
29 protected $sourceSha1; // string
30 protected $destSameAsSource; // boolean
32 /* Object life-cycle */
34 const STATE_CHECKED
= 2;
35 const STATE_ATTEMPTED
= 3;
38 * Build a new file operation transaction
40 * @params $backend FileBackend
41 * @params $params Array
43 final public function __construct( FileBackendBase
$backend, array $params ) {
44 $this->backend
= $backend;
45 foreach ( $this->allowedParams() as $name ) {
46 if ( isset( $params[$name] ) ) {
47 $this->params
[$name] = $params[$name];
50 $this->params
= $params;
54 * Allow stale data for file reads and existence checks.
58 final protected function allowStaleReads() {
59 $this->useLatest
= false;
63 * Attempt a series of file operations.
64 * Callers are responsible for handling file locking.
66 * $opts is an array of options, including:
67 * 'force' : Errors that would normally cause a rollback do not.
68 * The remaining operations are still attempted if any fail.
69 * 'allowStale' : Don't require the latest available data.
70 * This can increase performance for non-critical writes.
71 * This has no effect unless the 'force' flag is set.
73 * @param $performOps Array List of FileOp operations
74 * @param $opts Array Batch operation options
77 final public static function attemptBatch( array $performOps, array $opts ) {
78 $status = Status
::newGood();
80 $allowStale = !empty( $opts['allowStale'] );
81 $ignoreErrors = !empty( $opts['force'] );
83 $predicates = FileOp
::newPredicates(); // account for previous op in prechecks
84 // Do pre-checks for each operation; abort on failure...
85 foreach ( $performOps as $index => $fileOp ) {
87 $fileOp->allowStaleReads(); // allow potentially stale reads
89 $subStatus = $fileOp->precheck( $predicates );
90 $status->merge( $subStatus );
91 if ( !$subStatus->isOK() ) { // operation failed?
92 $status->success
[$index] = false;
94 if ( !$ignoreErrors ) {
95 return $status; // abort
100 // Attempt each operation...
101 foreach ( $performOps as $index => $fileOp ) {
102 if ( $fileOp->failed() ) {
103 continue; // nothing to do
105 $subStatus = $fileOp->attempt();
106 $status->merge( $subStatus );
107 if ( $subStatus->isOK() ) {
108 $status->success
[$index] = true;
109 ++
$status->successCount
;
111 $status->success
[$index] = false;
112 ++
$status->failCount
;
113 if ( !$ignoreErrors ) {
114 // Log remaining ops as failed for recovery...
115 for ( $i = ($index +
1); $i < count( $performOps ); $i++
) {
116 $performOps[$i]->logFailure( 'attempt_aborted' );
118 return $status; // bail out
127 * Get the value of the parameter with the given name
129 * @param $name string
130 * @return mixed Returns null if the parameter is not set
132 final public function getParam( $name ) {
133 return isset( $this->params
[$name] ) ?
$this->params
[$name] : null;
137 * Check if this operation failed precheck() or attempt()
141 final public function failed() {
142 return $this->failed
;
146 * Get a new empty predicates array for precheck()
150 final public static function newPredicates() {
151 return array( 'exists' => array(), 'sha1' => array() );
155 * Check preconditions of the operation without writing anything
157 * @param $predicates Array
160 final public function precheck( array &$predicates ) {
161 if ( $this->state
!== self
::STATE_NEW
) {
162 return Status
::newFatal( 'fileop-fail-state', self
::STATE_NEW
, $this->state
);
164 $this->state
= self
::STATE_CHECKED
;
165 $status = $this->doPrecheck( $predicates );
166 if ( !$status->isOK() ) {
167 $this->failed
= true;
173 * Attempt the operation, backing up files as needed; this must be reversible
177 final public function attempt() {
178 if ( $this->state
!== self
::STATE_CHECKED
) {
179 return Status
::newFatal( 'fileop-fail-state', self
::STATE_CHECKED
, $this->state
);
180 } elseif ( $this->failed
) { // failed precheck
181 return Status
::newFatal( 'fileop-fail-attempt-precheck' );
183 $this->state
= self
::STATE_ATTEMPTED
;
184 $status = $this->doAttempt();
185 if ( !$status->isOK() ) {
186 $this->failed
= true;
187 $this->logFailure( 'attempt' );
193 * Get a list of storage paths read from for this operation
197 public function storagePathsRead() {
202 * Get a list of storage paths written to for this operation
206 public function storagePathsChanged() {
211 * @return Array List of allowed parameters
213 protected function allowedParams() {
220 protected function doPrecheck( array &$predicates ) {
221 return Status
::newGood();
227 abstract protected function doAttempt();
230 * Check for errors with regards to the destination file already existing.
231 * This also updates the destSameAsSource and sourceSha1 member variables.
232 * A bad status will be returned if there is no chance it can be overwritten.
234 * @param $predicates Array
237 protected function precheckDestExistence( array $predicates ) {
238 $status = Status
::newGood();
239 // Get hash of source file/string and the destination file
240 $this->sourceSha1
= $this->getSourceSha1Base36(); // FS file or data string
241 if ( $this->sourceSha1
=== null ) { // file in storage?
242 $this->sourceSha1
= $this->fileSha1( $this->params
['src'], $predicates );
244 $this->destSameAsSource
= false;
245 if ( $this->fileExists( $this->params
['dst'], $predicates ) ) {
246 if ( $this->getParam( 'overwriteDest' ) ) {
247 return $status; // OK
248 } elseif ( $this->getParam( 'overwriteSame' ) ) {
249 $dhash = $this->fileSha1( $this->params
['dst'], $predicates );
250 // Check if hashes are valid and match each other...
251 if ( !strlen( $this->sourceSha1
) ||
!strlen( $dhash ) ) {
252 $status->fatal( 'backend-fail-hashes' );
253 } elseif ( $this->sourceSha1
!== $dhash ) {
254 // Give an error if the files are not identical
255 $status->fatal( 'backend-fail-notsame', $this->params
['dst'] );
257 $this->destSameAsSource
= true; // OK
259 return $status; // do nothing; either OK or bad status
261 $status->fatal( 'backend-fail-alreadyexists', $this->params
['dst'] );
269 * precheckDestExistence() helper function to get the source file SHA-1.
270 * Subclasses should overwride this iff the source is not in storage.
272 * @return string|false Returns false on failure
274 protected function getSourceSha1Base36() {
279 * Check if a file will exist in storage when this operation is attempted
281 * @param $source string Storage path
282 * @param $predicates Array
285 final protected function fileExists( $source, array $predicates ) {
286 if ( isset( $predicates['exists'][$source] ) ) {
287 return $predicates['exists'][$source]; // previous op assures this
289 $params = array( 'src' => $source, 'latest' => $this->useLatest
);
290 return $this->backend
->fileExists( $params );
295 * Get the SHA-1 of a file in storage when this operation is attempted
297 * @param $source string Storage path
298 * @param $predicates Array
299 * @return string|false
301 final protected function fileSha1( $source, array $predicates ) {
302 if ( isset( $predicates['sha1'][$source] ) ) {
303 return $predicates['sha1'][$source]; // previous op assures this
305 $params = array( 'src' => $source, 'latest' => $this->useLatest
);
306 return $this->backend
->getFileSha1Base36( $params );
311 * Log a file operation failure and preserve any temp files
313 * @param $action string
316 final protected function logFailure( $action ) {
317 $params = $this->params
;
318 $params['failedAction'] = $action;
320 wfDebugLog( 'FileOperation',
321 get_class( $this ) . ' failed:' . serialize( $params ) );
322 } catch ( Exception
$e ) {
323 // bad config? debug log error?
329 * Store a file into the backend from a file on the file system.
330 * Parameters similar to FileBackend::storeInternal(), which include:
331 * src : source path on file system
332 * dst : destination storage path
333 * overwriteDest : do nothing and pass if an identical file exists at destination
334 * overwriteSame : override any existing file at destination
336 class StoreFileOp
extends FileOp
{
337 protected function allowedParams() {
338 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
341 protected function doPrecheck( array &$predicates ) {
342 $status = Status
::newGood();
343 // Check if the source file exists on the file system
344 if ( !is_file( $this->params
['src'] ) ) {
345 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
348 // Check if destination file exists
349 $status->merge( $this->precheckDestExistence( $predicates ) );
350 if ( !$status->isOK() ) {
353 // Update file existence predicates
354 $predicates['exists'][$this->params
['dst']] = true;
355 $predicates['sha1'][$this->params
['dst']] = $this->sourceSha1
;
356 return $status; // safe to call attempt()
359 protected function doAttempt() {
360 $status = Status
::newGood();
361 // Store the file at the destination
362 if ( !$this->destSameAsSource
) {
363 $status->merge( $this->backend
->storeInternal( $this->params
) );
368 protected function getSourceSha1Base36() {
369 wfSuppressWarnings();
370 $hash = sha1_file( $this->params
['src'] );
372 if ( $hash !== false ) {
373 $hash = wfBaseConvert( $hash, 16, 36, 31 );
378 public function storagePathsChanged() {
379 return array( $this->params
['dst'] );
384 * Create a file in the backend with the given content.
385 * Parameters similar to FileBackend::createInternal(), which include:
386 * content : a string of raw file contents
387 * dst : destination storage path
388 * overwriteDest : do nothing and pass if an identical file exists at destination
389 * overwriteSame : override any existing file at destination
391 class CreateFileOp
extends FileOp
{
392 protected function allowedParams() {
393 return array( 'content', 'dst', 'overwriteDest', 'overwriteSame' );
396 protected function doPrecheck( array &$predicates ) {
397 $status = Status
::newGood();
398 // Check if destination file exists
399 $status->merge( $this->precheckDestExistence( $predicates ) );
400 if ( !$status->isOK() ) {
403 // Update file existence predicates
404 $predicates['exists'][$this->params
['dst']] = true;
405 $predicates['sha1'][$this->params
['dst']] = $this->sourceSha1
;
406 return $status; // safe to call attempt()
409 protected function doAttempt() {
410 $status = Status
::newGood();
411 // Create the file at the destination
412 if ( !$this->destSameAsSource
) {
413 $status->merge( $this->backend
->createInternal( $this->params
) );
418 protected function getSourceSha1Base36() {
419 return wfBaseConvert( sha1( $this->params
['content'] ), 16, 36, 31 );
422 public function storagePathsChanged() {
423 return array( $this->params
['dst'] );
428 * Copy a file from one storage path to another in the backend.
429 * Parameters similar to FileBackend::copyInternal(), which include:
430 * src : source storage path
431 * dst : destination storage path
432 * overwriteDest : do nothing and pass if an identical file exists at destination
433 * overwriteSame : override any existing file at destination
435 class CopyFileOp
extends FileOp
{
436 protected function allowedParams() {
437 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
440 protected function doPrecheck( array &$predicates ) {
441 $status = Status
::newGood();
442 // Check if the source file exists
443 if ( !$this->fileExists( $this->params
['src'], $predicates ) ) {
444 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
447 // Check if destination file exists
448 $status->merge( $this->precheckDestExistence( $predicates ) );
449 if ( !$status->isOK() ) {
452 // Update file existence predicates
453 $predicates['exists'][$this->params
['dst']] = true;
454 $predicates['sha1'][$this->params
['dst']] = $this->sourceSha1
;
455 return $status; // safe to call attempt()
458 protected function doAttempt() {
459 $status = Status
::newGood();
460 // Do nothing if the src/dst paths are the same
461 if ( $this->params
['src'] !== $this->params
['dst'] ) {
462 // Copy the file into the destination
463 if ( !$this->destSameAsSource
) {
464 $status->merge( $this->backend
->copyInternal( $this->params
) );
470 public function storagePathsRead() {
471 return array( $this->params
['src'] );
474 public function storagePathsChanged() {
475 return array( $this->params
['dst'] );
480 * Move a file from one storage path to another in the backend.
481 * Parameters similar to FileBackend::moveInternal(), which include:
482 * src : source storage path
483 * dst : destination storage path
484 * overwriteDest : do nothing and pass if an identical file exists at destination
485 * overwriteSame : override any existing file at destination
487 class MoveFileOp
extends FileOp
{
488 protected function allowedParams() {
489 return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
492 protected function doPrecheck( array &$predicates ) {
493 $status = Status
::newGood();
494 // Check if the source file exists
495 if ( !$this->fileExists( $this->params
['src'], $predicates ) ) {
496 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
499 // Check if destination file exists
500 $status->merge( $this->precheckDestExistence( $predicates ) );
501 if ( !$status->isOK() ) {
504 // Update file existence predicates
505 $predicates['exists'][$this->params
['src']] = false;
506 $predicates['sha1'][$this->params
['src']] = false;
507 $predicates['exists'][$this->params
['dst']] = true;
508 $predicates['sha1'][$this->params
['dst']] = $this->sourceSha1
;
509 return $status; // safe to call attempt()
512 protected function doAttempt() {
513 $status = Status
::newGood();
514 // Do nothing if the src/dst paths are the same
515 if ( $this->params
['src'] !== $this->params
['dst'] ) {
516 if ( !$this->destSameAsSource
) {
517 // Move the file into the destination
518 $status->merge( $this->backend
->moveInternal( $this->params
) );
520 // Just delete source as the destination needs no changes
521 $params = array( 'src' => $this->params
['src'] );
522 $status->merge( $this->backend
->deleteInternal( $params ) );
523 if ( !$status->isOK() ) {
531 public function storagePathsRead() {
532 return array( $this->params
['src'] );
535 public function storagePathsChanged() {
536 return array( $this->params
['dst'] );
541 * Delete a file at the storage path.
542 * Parameters similar to FileBackend::deleteInternal(), which include:
543 * src : source storage path
544 * ignoreMissingSource : don't return an error if the file does not exist
546 class DeleteFileOp
extends FileOp
{
547 protected $needsDelete = true;
549 protected function allowedParams() {
550 return array( 'src', 'ignoreMissingSource' );
553 protected function doPrecheck( array &$predicates ) {
554 $status = Status
::newGood();
555 // Check if the source file exists
556 if ( !$this->fileExists( $this->params
['src'], $predicates ) ) {
557 if ( !$this->getParam( 'ignoreMissingSource' ) ) {
558 $status->fatal( 'backend-fail-notexists', $this->params
['src'] );
561 $this->needsDelete
= false;
563 // Update file existence predicates
564 $predicates['exists'][$this->params
['src']] = false;
565 $predicates['sha1'][$this->params
['src']] = false;
566 return $status; // safe to call attempt()
569 protected function doAttempt() {
570 $status = Status
::newGood();
571 if ( $this->needsDelete
) {
572 // Delete the source file
573 $status->merge( $this->backend
->deleteInternal( $this->params
) );
574 if ( !$status->isOK() ) {
581 public function storagePathsChanged() {
582 return array( $this->params
['src'] );
587 * Placeholder operation that has no params and does nothing
589 class NullFileOp
extends FileOp
{
590 protected function doAttempt() {
591 return Status
::newGood();