[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 * Do not use this class from places outside FileBackend.
11 *
12 * Methods called from attemptBatch() should avoid throwing exceptions at all costs.
13 * FileOp objects should be lightweight in order to support 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 FileBackendStore */
22 protected $backend;
23
24 protected $state = self::STATE_NEW; // integer
25 protected $failed = false; // boolean
26 protected $useLatest = true; // boolean
27 protected $batchId; // string
28
29 protected $sourceSha1; // string
30 protected $destSameAsSource; // boolean
31
32 /* Object life-cycle */
33 const STATE_NEW = 1;
34 const STATE_CHECKED = 2;
35 const STATE_ATTEMPTED = 3;
36
37 /* Timeout related parameters */
38 const MAX_BATCH_SIZE = 1000;
39 const TIME_LIMIT_SEC = 300; // 5 minutes
40
41 /**
42 * Build a new file operation transaction
43 *
44 * @param $backend FileBackendStore
45 * @param $params Array
46 * @throws MWException
47 */
48 final public function __construct( FileBackendStore $backend, array $params ) {
49 $this->backend = $backend;
50 list( $required, $optional ) = $this->allowedParams();
51 foreach ( $required as $name ) {
52 if ( isset( $params[$name] ) ) {
53 $this->params[$name] = $params[$name];
54 } else {
55 throw new MWException( "File operation missing parameter '$name'." );
56 }
57 }
58 foreach ( $optional as $name ) {
59 if ( isset( $params[$name] ) ) {
60 $this->params[$name] = $params[$name];
61 }
62 }
63 $this->params = $params;
64 }
65
66 /**
67 * Set the batch UUID this operation belongs to
68 *
69 * @param $batchId string
70 * @return void
71 */
72 final protected function setBatchId( $batchId ) {
73 $this->batchId = $batchId;
74 }
75
76 /**
77 * Whether to allow stale data for file reads and stat checks
78 *
79 * @param $allowStale bool
80 * @return void
81 */
82 final protected function allowStaleReads( $allowStale ) {
83 $this->useLatest = !$allowStale;
84 }
85
86 /**
87 * Attempt to perform a series of file operations.
88 * Callers are responsible for handling file locking.
89 *
90 * $opts is an array of options, including:
91 * 'force' : Errors that would normally cause a rollback do not.
92 * The remaining operations are still attempted if any fail.
93 * 'allowStale' : Don't require the latest available data.
94 * This can increase performance for non-critical writes.
95 * This has no effect unless the 'force' flag is set.
96 * 'nonJournaled' : Don't log this operation batch in the file journal.
97 *
98 * The resulting Status will be "OK" unless:
99 * a) unexpected operation errors occurred (network partitions, disk full...)
100 * b) significant operation errors occured and 'force' was not set
101 *
102 * @param $performOps Array List of FileOp operations
103 * @param $opts Array Batch operation options
104 * @param $journal FileJournal Journal to log operations to
105 * @return Status
106 */
107 final public static function attemptBatch(
108 array $performOps, array $opts, FileJournal $journal
109 ) {
110 $status = Status::newGood();
111
112 $n = count( $performOps );
113 if ( $n > self::MAX_BATCH_SIZE ) {
114 $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
115 return $status;
116 }
117
118 $batchId = $journal->getTimestampedUUID();
119 $allowStale = !empty( $opts['allowStale'] );
120 $ignoreErrors = !empty( $opts['force'] );
121 $journaled = empty( $opts['nonJournaled'] );
122
123 $entries = array(); // file journal entries
124 $predicates = FileOp::newPredicates(); // account for previous op in prechecks
125 // Do pre-checks for each operation; abort on failure...
126 foreach ( $performOps as $index => $fileOp ) {
127 $fileOp->setBatchId( $batchId );
128 $fileOp->allowStaleReads( $allowStale );
129 $oldPredicates = $predicates;
130 $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
131 $status->merge( $subStatus );
132 if ( $subStatus->isOK() ) {
133 if ( $journaled ) { // journal log entry
134 $entries = array_merge( $entries,
135 self::getJournalEntries( $fileOp, $oldPredicates, $predicates ) );
136 }
137 } else { // operation failed?
138 $status->success[$index] = false;
139 ++$status->failCount;
140 if ( !$ignoreErrors ) {
141 return $status; // abort
142 }
143 }
144 }
145
146 // Log the operations in file journal...
147 if ( count( $entries ) ) {
148 $subStatus = $journal->logChangeBatch( $entries, $batchId );
149 if ( !$subStatus->isOK() ) {
150 return $subStatus; // abort
151 }
152 }
153
154 if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
155 $status->setResult( true, $status->value );
156 }
157
158 // Restart PHP's execution timer and set the timeout to safe amount.
159 // This handles cases where the operations take a long time or where we are
160 // already running low on time left. The old timeout is restored afterwards.
161 # @TODO: re-enable this for when the number of batches is high.
162 #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC );
163
164 // Attempt each operation...
165 foreach ( $performOps as $index => $fileOp ) {
166 if ( $fileOp->failed() ) {
167 continue; // nothing to do
168 }
169 $subStatus = $fileOp->attempt();
170 $status->merge( $subStatus );
171 if ( $subStatus->isOK() ) {
172 $status->success[$index] = true;
173 ++$status->successCount;
174 } else {
175 $status->success[$index] = false;
176 ++$status->failCount;
177 // We can't continue (even with $ignoreErrors) as $predicates is wrong.
178 // Log the remaining ops as failed for recovery...
179 for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
180 $performOps[$i]->logFailure( 'attempt_aborted' );
181 }
182 return $status; // bail out
183 }
184 }
185
186 return $status;
187 }
188
189 /**
190 * Get the file journal entries for a single file operation
191 *
192 * @param $fileOp FileOp
193 * @param $oPredicates Array Pre-op information about files
194 * @param $nPredicates Array Post-op information about files
195 * @return Array
196 */
197 final protected static function getJournalEntries(
198 FileOp $fileOp, array $oPredicates, array $nPredicates
199 ) {
200 $nullEntries = array();
201 $updateEntries = array();
202 $deleteEntries = array();
203 $pathsUsed = array_merge( $fileOp->storagePathsRead(), $fileOp->storagePathsChanged() );
204 foreach ( $pathsUsed as $path ) {
205 $nullEntries[] = array( // assertion for recovery
206 'op' => 'null',
207 'path' => $path,
208 'newSha1' => $fileOp->fileSha1( $path, $oPredicates )
209 );
210 }
211 foreach ( $fileOp->storagePathsChanged() as $path ) {
212 if ( $nPredicates['sha1'][$path] === false ) { // deleted
213 $deleteEntries[] = array(
214 'op' => 'delete',
215 'path' => $path,
216 'newSha1' => ''
217 );
218 } else { // created/updated
219 $updateEntries[] = array(
220 'op' => $fileOp->fileExists( $path, $oPredicates ) ? 'update' : 'create',
221 'path' => $path,
222 'newSha1' => $nPredicates['sha1'][$path]
223 );
224 }
225 }
226 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
227 }
228
229 /**
230 * Get the value of the parameter with the given name
231 *
232 * @param $name string
233 * @return mixed Returns null if the parameter is not set
234 */
235 final public function getParam( $name ) {
236 return isset( $this->params[$name] ) ? $this->params[$name] : null;
237 }
238
239 /**
240 * Check if this operation failed precheck() or attempt()
241 *
242 * @return bool
243 */
244 final public function failed() {
245 return $this->failed;
246 }
247
248 /**
249 * Get a new empty predicates array for precheck()
250 *
251 * @return Array
252 */
253 final public static function newPredicates() {
254 return array( 'exists' => array(), 'sha1' => array() );
255 }
256
257 /**
258 * Check preconditions of the operation without writing anything
259 *
260 * @param $predicates Array
261 * @return Status
262 */
263 final public function precheck( array &$predicates ) {
264 if ( $this->state !== self::STATE_NEW ) {
265 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
266 }
267 $this->state = self::STATE_CHECKED;
268 $status = $this->doPrecheck( $predicates );
269 if ( !$status->isOK() ) {
270 $this->failed = true;
271 }
272 return $status;
273 }
274
275 /**
276 * Attempt the operation, backing up files as needed; this must be reversible
277 *
278 * @return Status
279 */
280 final public function attempt() {
281 if ( $this->state !== self::STATE_CHECKED ) {
282 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
283 } elseif ( $this->failed ) { // failed precheck
284 return Status::newFatal( 'fileop-fail-attempt-precheck' );
285 }
286 $this->state = self::STATE_ATTEMPTED;
287 $status = $this->doAttempt();
288 if ( !$status->isOK() ) {
289 $this->failed = true;
290 $this->logFailure( 'attempt' );
291 }
292 return $status;
293 }
294
295 /**
296 * Get the file operation parameters
297 *
298 * @return Array (required params list, optional params list)
299 */
300 protected function allowedParams() {
301 return array( array(), array() );
302 }
303
304 /**
305 * Get a list of storage paths read from for this operation
306 *
307 * @return Array
308 */
309 public function storagePathsRead() {
310 return array();
311 }
312
313 /**
314 * Get a list of storage paths written to for this operation
315 *
316 * @return Array
317 */
318 public function storagePathsChanged() {
319 return array();
320 }
321
322 /**
323 * @return Status
324 */
325 protected function doPrecheck( array &$predicates ) {
326 return Status::newGood();
327 }
328
329 /**
330 * @return Status
331 */
332 protected function doAttempt() {
333 return Status::newGood();
334 }
335
336 /**
337 * Check for errors with regards to the destination file already existing.
338 * This also updates the destSameAsSource and sourceSha1 member variables.
339 * A bad status will be returned if there is no chance it can be overwritten.
340 *
341 * @param $predicates Array
342 * @return Status
343 */
344 protected function precheckDestExistence( array $predicates ) {
345 $status = Status::newGood();
346 // Get hash of source file/string and the destination file
347 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
348 if ( $this->sourceSha1 === null ) { // file in storage?
349 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
350 }
351 $this->destSameAsSource = false;
352 if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
353 if ( $this->getParam( 'overwrite' ) ) {
354 return $status; // OK
355 } elseif ( $this->getParam( 'overwriteSame' ) ) {
356 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
357 // Check if hashes are valid and match each other...
358 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
359 $status->fatal( 'backend-fail-hashes' );
360 } elseif ( $this->sourceSha1 !== $dhash ) {
361 // Give an error if the files are not identical
362 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
363 } else {
364 $this->destSameAsSource = true; // OK
365 }
366 return $status; // do nothing; either OK or bad status
367 } else {
368 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
369 return $status;
370 }
371 }
372 return $status;
373 }
374
375 /**
376 * precheckDestExistence() helper function to get the source file SHA-1.
377 * Subclasses should overwride this iff the source is not in storage.
378 *
379 * @return string|bool Returns false on failure
380 */
381 protected function getSourceSha1Base36() {
382 return null; // N/A
383 }
384
385 /**
386 * Check if a file will exist in storage when this operation is attempted
387 *
388 * @param $source string Storage path
389 * @param $predicates Array
390 * @return bool
391 */
392 final protected function fileExists( $source, array $predicates ) {
393 if ( isset( $predicates['exists'][$source] ) ) {
394 return $predicates['exists'][$source]; // previous op assures this
395 } else {
396 $params = array( 'src' => $source, 'latest' => $this->useLatest );
397 return $this->backend->fileExists( $params );
398 }
399 }
400
401 /**
402 * Get the SHA-1 of a file in storage when this operation is attempted
403 *
404 * @param $source string Storage path
405 * @param $predicates Array
406 * @return string|bool False on failure
407 */
408 final protected function fileSha1( $source, array $predicates ) {
409 if ( isset( $predicates['sha1'][$source] ) ) {
410 return $predicates['sha1'][$source]; // previous op assures this
411 } else {
412 $params = array( 'src' => $source, 'latest' => $this->useLatest );
413 return $this->backend->getFileSha1Base36( $params );
414 }
415 }
416
417 /**
418 * Log a file operation failure and preserve any temp files
419 *
420 * @param $action string
421 * @return void
422 */
423 final protected function logFailure( $action ) {
424 $params = $this->params;
425 $params['failedAction'] = $action;
426 try {
427 wfDebugLog( 'FileOperation', get_class( $this ) .
428 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
429 } catch ( Exception $e ) {
430 // bad config? debug log error?
431 }
432 }
433 }
434
435 /**
436 * FileOp helper class to expand PHP execution time for a function.
437 * On construction, set_time_limit() is called and set to $seconds.
438 * When the object goes out of scope, the timer is restarted, with
439 * the original time limit minus the time the object existed.
440 */
441 class FileOpScopedPHPTimeout {
442 protected $startTime; // float; seconds
443 protected $oldTimeout; // integer; seconds
444
445 protected static $stackDepth = 0; // integer
446 protected static $totalCalls = 0; // integer
447 protected static $totalElapsed = 0; // float; seconds
448
449 /* Prevent callers in infinite loops from running forever */
450 const MAX_TOTAL_CALLS = 1000000;
451 const MAX_TOTAL_TIME = 300; // seconds
452
453 /**
454 * @param $seconds integer
455 */
456 public function __construct( $seconds ) {
457 if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0
458 if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) {
459 trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." );
460 } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) {
461 trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." );
462 } elseif ( self::$stackDepth > 0 ) { // recursion guard
463 trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." );
464 } else {
465 $this->oldTimeout = ini_set( 'max_execution_time', $seconds );
466 $this->startTime = microtime( true );
467 ++self::$stackDepth;
468 ++self::$totalCalls; // proof against < 1us scopes
469 }
470 }
471 }
472
473 /**
474 * Restore the original timeout.
475 * This does not account for the timer value on __construct().
476 */
477 public function __destruct() {
478 if ( $this->oldTimeout ) {
479 $elapsed = microtime( true ) - $this->startTime;
480 // Note: a limit of 0 is treated as "forever"
481 set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) );
482 // If each scoped timeout is for less than one second, we end up
483 // restoring the original timeout without any decrease in value.
484 // Thus web scripts in an infinite loop can run forever unless we
485 // take some measures to prevent this. Track total time and calls.
486 self::$totalElapsed += $elapsed;
487 --self::$stackDepth;
488 }
489 }
490 }
491
492 /**
493 * Store a file into the backend from a file on the file system.
494 * Parameters similar to FileBackendStore::storeInternal(), which include:
495 * src : source path on file system
496 * dst : destination storage path
497 * overwrite : do nothing and pass if an identical file exists at destination
498 * overwriteSame : override any existing file at destination
499 */
500 class StoreFileOp extends FileOp {
501 protected function allowedParams() {
502 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
503 }
504
505 protected function doPrecheck( array &$predicates ) {
506 $status = Status::newGood();
507 // Check if the source file exists on the file system
508 if ( !is_file( $this->params['src'] ) ) {
509 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
510 return $status;
511 // Check if the source file is too big
512 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
513 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
514 return $status;
515 // Check if a file can be placed at the destination
516 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
517 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
518 return $status;
519 }
520 // Check if destination file exists
521 $status->merge( $this->precheckDestExistence( $predicates ) );
522 if ( $status->isOK() ) {
523 // Update file existence predicates
524 $predicates['exists'][$this->params['dst']] = true;
525 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
526 }
527 return $status; // safe to call attempt()
528 }
529
530 protected function doAttempt() {
531 $status = Status::newGood();
532 // Store the file at the destination
533 if ( !$this->destSameAsSource ) {
534 $status->merge( $this->backend->storeInternal( $this->params ) );
535 }
536 return $status;
537 }
538
539 protected function getSourceSha1Base36() {
540 wfSuppressWarnings();
541 $hash = sha1_file( $this->params['src'] );
542 wfRestoreWarnings();
543 if ( $hash !== false ) {
544 $hash = wfBaseConvert( $hash, 16, 36, 31 );
545 }
546 return $hash;
547 }
548
549 public function storagePathsChanged() {
550 return array( $this->params['dst'] );
551 }
552 }
553
554 /**
555 * Create a file in the backend with the given content.
556 * Parameters similar to FileBackendStore::createInternal(), which include:
557 * content : the raw file contents
558 * dst : destination storage path
559 * overwrite : do nothing and pass if an identical file exists at destination
560 * overwriteSame : override any existing file at destination
561 */
562 class CreateFileOp extends FileOp {
563 protected function allowedParams() {
564 return array( array( 'content', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
565 }
566
567 protected function doPrecheck( array &$predicates ) {
568 $status = Status::newGood();
569 // Check if the source data is too big
570 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
571 $status->fatal( 'backend-fail-create', $this->params['dst'] );
572 return $status;
573 // Check if a file can be placed at the destination
574 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
575 $status->fatal( 'backend-fail-create', $this->params['dst'] );
576 return $status;
577 }
578 // Check if destination file exists
579 $status->merge( $this->precheckDestExistence( $predicates ) );
580 if ( $status->isOK() ) {
581 // Update file existence predicates
582 $predicates['exists'][$this->params['dst']] = true;
583 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
584 }
585 return $status; // safe to call attempt()
586 }
587
588 protected function doAttempt() {
589 $status = Status::newGood();
590 // Create the file at the destination
591 if ( !$this->destSameAsSource ) {
592 $status->merge( $this->backend->createInternal( $this->params ) );
593 }
594 return $status;
595 }
596
597 protected function getSourceSha1Base36() {
598 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
599 }
600
601 public function storagePathsChanged() {
602 return array( $this->params['dst'] );
603 }
604 }
605
606 /**
607 * Copy a file from one storage path to another in the backend.
608 * Parameters similar to FileBackendStore::copyInternal(), which include:
609 * src : source storage path
610 * dst : destination storage path
611 * overwrite : do nothing and pass if an identical file exists at destination
612 * overwriteSame : override any existing file at destination
613 */
614 class CopyFileOp extends FileOp {
615 protected function allowedParams() {
616 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
617 }
618
619 protected function doPrecheck( array &$predicates ) {
620 $status = Status::newGood();
621 // Check if the source file exists
622 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
623 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
624 return $status;
625 // Check if a file can be placed at the destination
626 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
627 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
628 return $status;
629 }
630 // Check if destination file exists
631 $status->merge( $this->precheckDestExistence( $predicates ) );
632 if ( $status->isOK() ) {
633 // Update file existence predicates
634 $predicates['exists'][$this->params['dst']] = true;
635 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
636 }
637 return $status; // safe to call attempt()
638 }
639
640 protected function doAttempt() {
641 $status = Status::newGood();
642 // Do nothing if the src/dst paths are the same
643 if ( $this->params['src'] !== $this->params['dst'] ) {
644 // Copy the file into the destination
645 if ( !$this->destSameAsSource ) {
646 $status->merge( $this->backend->copyInternal( $this->params ) );
647 }
648 }
649 return $status;
650 }
651
652 public function storagePathsRead() {
653 return array( $this->params['src'] );
654 }
655
656 public function storagePathsChanged() {
657 return array( $this->params['dst'] );
658 }
659 }
660
661 /**
662 * Move a file from one storage path to another in the backend.
663 * Parameters similar to FileBackendStore::moveInternal(), which include:
664 * src : source storage path
665 * dst : destination storage path
666 * overwrite : do nothing and pass if an identical file exists at destination
667 * overwriteSame : override any existing file at destination
668 */
669 class MoveFileOp extends FileOp {
670 protected function allowedParams() {
671 return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
672 }
673
674 protected function doPrecheck( array &$predicates ) {
675 $status = Status::newGood();
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 // Check if a file can be placed at the destination
681 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
682 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
683 return $status;
684 }
685 // Check if destination file exists
686 $status->merge( $this->precheckDestExistence( $predicates ) );
687 if ( $status->isOK() ) {
688 // Update file existence predicates
689 $predicates['exists'][$this->params['src']] = false;
690 $predicates['sha1'][$this->params['src']] = false;
691 $predicates['exists'][$this->params['dst']] = true;
692 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
693 }
694 return $status; // safe to call attempt()
695 }
696
697 protected function doAttempt() {
698 $status = Status::newGood();
699 // Do nothing if the src/dst paths are the same
700 if ( $this->params['src'] !== $this->params['dst'] ) {
701 if ( !$this->destSameAsSource ) {
702 // Move the file into the destination
703 $status->merge( $this->backend->moveInternal( $this->params ) );
704 } else {
705 // Just delete source as the destination needs no changes
706 $params = array( 'src' => $this->params['src'] );
707 $status->merge( $this->backend->deleteInternal( $params ) );
708 }
709 }
710 return $status;
711 }
712
713 public function storagePathsRead() {
714 return array( $this->params['src'] );
715 }
716
717 public function storagePathsChanged() {
718 return array( $this->params['dst'] );
719 }
720 }
721
722 /**
723 * Delete a file at the given storage path from the backend.
724 * Parameters similar to FileBackendStore::deleteInternal(), which include:
725 * src : source storage path
726 * ignoreMissingSource : don't return an error if the file does not exist
727 */
728 class DeleteFileOp extends FileOp {
729 protected function allowedParams() {
730 return array( array( 'src' ), array( 'ignoreMissingSource' ) );
731 }
732
733 protected $needsDelete = true;
734
735 protected function doPrecheck( array &$predicates ) {
736 $status = Status::newGood();
737 // Check if the source file exists
738 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
739 if ( !$this->getParam( 'ignoreMissingSource' ) ) {
740 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
741 return $status;
742 }
743 $this->needsDelete = false;
744 }
745 // Update file existence predicates
746 $predicates['exists'][$this->params['src']] = false;
747 $predicates['sha1'][$this->params['src']] = false;
748 return $status; // safe to call attempt()
749 }
750
751 protected function doAttempt() {
752 $status = Status::newGood();
753 if ( $this->needsDelete ) {
754 // Delete the source file
755 $status->merge( $this->backend->deleteInternal( $this->params ) );
756 }
757 return $status;
758 }
759
760 public function storagePathsChanged() {
761 return array( $this->params['src'] );
762 }
763 }
764
765 /**
766 * Placeholder operation that has no params and does nothing
767 */
768 class NullFileOp extends FileOp {}