MediaWiki  master
FileOp.php
Go to the documentation of this file.
1 <?php
23 use Psr\Log\LoggerInterface;
24 use Wikimedia\RequestTimeout\TimeoutException;
25 
37 abstract class FileOp {
39  protected $backend;
41  protected $logger;
42 
44  protected $params = [];
45 
47  protected $state = self::STATE_NEW;
49  protected $failed = false;
51  protected $async = false;
53  protected $cancelled = false;
54 
56  protected $sourceSize;
58  protected $sourceSha1;
59 
61  protected $overwriteSameCase;
62 
64  protected $destExists;
65 
66  /* Object life-cycle */
67  private const STATE_NEW = 1;
68  private const STATE_CHECKED = 2;
69  private const STATE_ATTEMPTED = 3;
70 
71  protected const ASSUMED_SHA1 = 'sha1';
72  protected const ASSUMED_EXISTS = 'exists';
73  protected const ASSUMED_SIZE = 'size';
74 
83  final public function __construct(
84  FileBackendStore $backend, array $params, LoggerInterface $logger
85  ) {
86  $this->backend = $backend;
87  $this->logger = $logger;
88  [ $required, $optional, $paths ] = $this->allowedParams();
89  foreach ( $required as $name ) {
90  if ( isset( $params[$name] ) ) {
91  $this->params[$name] = $params[$name];
92  } else {
93  throw new InvalidArgumentException( "File operation missing parameter '$name'." );
94  }
95  }
96  foreach ( $optional as $name ) {
97  if ( isset( $params[$name] ) ) {
98  $this->params[$name] = $params[$name];
99  }
100  }
101  foreach ( $paths as $name ) {
102  if ( isset( $this->params[$name] ) ) {
103  // Normalize paths so the paths to the same file have the same string
104  $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
105  }
106  }
107  }
108 
115  protected static function normalizeIfValidStoragePath( $path ) {
118 
119  return $res ?? $path;
120  }
121 
122  return $path;
123  }
124 
131  final public function getParam( $name ) {
132  return $this->params[$name] ?? null;
133  }
134 
140  final public function failed() {
141  return $this->failed;
142  }
143 
149  final public static function newPredicates() {
150  return [ self::ASSUMED_EXISTS => [], self::ASSUMED_SHA1 => [], self::ASSUMED_SIZE => [] ];
151  }
152 
158  final public static function newDependencies() {
159  return [ 'read' => [], 'write' => [] ];
160  }
161 
168  final public function applyDependencies( array $deps ) {
169  $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
170  $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
171 
172  return $deps;
173  }
174 
181  final public function dependsOn( array $deps ) {
182  foreach ( $this->storagePathsChanged() as $path ) {
183  if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
184  return true; // "output" or "anti" dependency
185  }
186  }
187  foreach ( $this->storagePathsRead() as $path ) {
188  if ( isset( $deps['write'][$path] ) ) {
189  return true; // "flow" dependency
190  }
191  }
192 
193  return false;
194  }
195 
204  final public function precheck( array &$predicates ) {
205  if ( $this->state !== self::STATE_NEW ) {
206  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
207  }
208  $this->state = self::STATE_CHECKED;
209 
210  $status = StatusValue::newGood();
211  foreach ( $this->storagePathsReadOrChanged() as $path ) {
212  if ( !$this->backend->isPathUsableInternal( $path ) ) {
213  $status->fatal( 'backend-fail-usable', $path );
214  }
215  }
216  if ( !$status->isOK() ) {
217  return $status;
218  }
219 
220  $status = $this->doPrecheck( $predicates );
221  if ( !$status->isOK() ) {
222  $this->failed = true;
223  }
224 
225  return $status;
226  }
227 
232  protected function doPrecheck( array &$predicates ) {
233  return StatusValue::newGood();
234  }
235 
241  final public function attempt() {
242  if ( $this->state !== self::STATE_CHECKED ) {
243  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
244  } elseif ( $this->failed ) { // failed precheck
245  return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
246  }
247  $this->state = self::STATE_ATTEMPTED;
248  if ( $this->cancelled ) {
249  $status = StatusValue::newGood(); // no-op
250  } else {
251  $status = $this->doAttempt();
252  if ( !$status->isOK() ) {
253  $this->failed = true;
254  $this->logFailure( 'attempt' );
255  }
256  }
257 
258  return $status;
259  }
260 
264  protected function doAttempt() {
265  return StatusValue::newGood();
266  }
267 
273  final public function attemptAsync() {
274  $this->async = true;
275  $result = $this->attempt();
276  $this->async = false;
277 
278  return $result;
279  }
280 
286  final public function attemptQuick() {
287  $this->state = self::STATE_CHECKED; // bypassed
288 
289  return $this->attempt();
290  }
291 
297  final public function attemptAsyncQuick() {
298  $this->state = self::STATE_CHECKED; // bypassed
299 
300  return $this->attemptAsync();
301  }
302 
308  protected function allowedParams() {
309  return [ [], [], [] ];
310  }
311 
318  protected function setFlags( array $params ) {
319  return [ 'async' => $this->async ] + $params;
320  }
321 
327  public function storagePathsRead() {
328  return [];
329  }
330 
336  public function storagePathsChanged() {
337  return [];
338  }
339 
345  final public function storagePathsReadOrChanged() {
346  return array_values( array_unique(
347  array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
348  ) );
349  }
350 
359  protected function precheckDestExistence( array $predicates ) {
360  $status = StatusValue::newGood();
361  // Record the size of source file/string
362  $this->sourceSize = $this->getSourceSize(); // FS file or data string
363  // file in storage?
364  $this->sourceSize ??= $this->fileSize( $this->params['src'], $predicates );
365  // Record the hash of source file/string
366  $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
367  // file in storage?
368  $this->sourceSha1 ??= $this->fileSha1( $this->params['src'], $predicates );
369  // Record the existence of destination file
370  $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
371  // Check if an incompatible file exists at the destination
372  $this->overwriteSameCase = false;
373  if ( $this->destExists ) {
374  if ( $this->getParam( 'overwrite' ) ) {
375  return $status; // OK, no conflict
376  } elseif ( $this->getParam( 'overwriteSame' ) ) {
377  // Operation does nothing other than return an OK or bad status
378  $dhash = $this->fileSha1( $this->params['dst'], $predicates );
379  $dsize = $this->fileSize( $this->params['dst'], $predicates );
380  // Check if hashes are valid and match each other...
381  if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
382  $status->fatal( 'backend-fail-hashes' );
383  } elseif ( !is_int( $this->sourceSize ) || !is_int( $dsize ) ) {
384  $status->fatal( 'backend-fail-sizes' );
385  } elseif ( $this->sourceSha1 !== $dhash || $this->sourceSize !== $dsize ) {
386  // Give an error if the files are not identical
387  $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
388  } else {
389  $this->overwriteSameCase = true; // OK
390  }
391  } else {
392  $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
393  }
394  } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
395  $status->fatal( 'backend-fail-stat', $this->params['dst'] );
396  }
397 
398  return $status;
399  }
400 
407  protected function getSourceSize() {
408  return null; // N/A
409  }
410 
417  protected function getSourceSha1Base36() {
418  return null; // N/A
419  }
420 
431  final protected function fileExists( $source, array $predicates ) {
432  if ( isset( $predicates[self::ASSUMED_EXISTS][$source] ) ) {
433  return $predicates[self::ASSUMED_EXISTS][$source]; // previous op assures this
434  } else {
435  $params = [ 'src' => $source, 'latest' => true ];
436 
437  return $this->backend->fileExists( $params );
438  }
439  }
440 
452  final protected function fileSize( $source, array $predicates ) {
453  if ( isset( $predicates[self::ASSUMED_SIZE][$source] ) ) {
454  return $predicates[self::ASSUMED_SIZE][$source]; // previous op assures this
455  } elseif (
456  isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
457  !$predicates[self::ASSUMED_EXISTS][$source]
458  ) {
459  return false; // previous op assures this
460  } else {
461  $params = [ 'src' => $source, 'latest' => true ];
462 
463  return $this->backend->getFileSize( $params );
464  }
465  }
466 
474  final protected function fileSha1( $source, array $predicates ) {
475  if ( isset( $predicates[self::ASSUMED_SHA1][$source] ) ) {
476  return $predicates[self::ASSUMED_SHA1][$source]; // previous op assures this
477  } elseif (
478  isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
479  !$predicates[self::ASSUMED_EXISTS][$source]
480  ) {
481  return false; // previous op assures this
482  } else {
483  $params = [ 'src' => $source, 'latest' => true ];
484 
485  return $this->backend->getFileSha1Base36( $params );
486  }
487  }
488 
494  public function getBackend() {
495  return $this->backend;
496  }
497 
503  final public function logFailure( $action ) {
505  $params['failedAction'] = $action;
506  try {
507  $this->logger->error( static::class .
508  " failed: " . FormatJson::encode( $params ) );
509  } catch ( TimeoutException $e ) {
510  throw $e;
511  } catch ( Exception $e ) {
512  // bad config? debug log error?
513  }
514  }
515 }
Base class for all backends using particular storage medium.
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
FileBackend helper class for representing operations.
Definition: FileOp.php:37
array $params
Definition: FileOp.php:44
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition: FileOp.php:115
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition: FileOp.php:158
fileSha1( $source, array $predicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition: FileOp.php:474
static newPredicates()
Get a new empty predicates array for precheck()
Definition: FileOp.php:149
fileExists( $source, array $predicates)
Check if a file will exist in storage when this operation is attempted.
Definition: FileOp.php:431
allowedParams()
Get the file operation parameters.
Definition: FileOp.php:308
const ASSUMED_EXISTS
Definition: FileOp.php:72
attemptAsync()
Attempt the operation in the background.
Definition: FileOp.php:273
doPrecheck(array &$predicates)
Definition: FileOp.php:232
string bool $sourceSha1
Definition: FileOp.php:58
getParam( $name)
Get the value of the parameter with the given name.
Definition: FileOp.php:131
bool $overwriteSameCase
Definition: FileOp.php:61
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition: FileOp.php:345
getSourceSize()
precheckDestExistence() helper function to get the source file size.
Definition: FileOp.php:407
const ASSUMED_SIZE
Definition: FileOp.php:73
const ASSUMED_SHA1
Definition: FileOp.php:71
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition: FileOp.php:336
int $state
Definition: FileOp.php:47
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition: FileOp.php:168
attempt()
Attempt the operation.
Definition: FileOp.php:241
precheck(array &$predicates)
Check preconditions of the operation without writing anything.
Definition: FileOp.php:204
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition: FileOp.php:318
bool $async
Definition: FileOp.php:51
bool $destExists
Definition: FileOp.php:64
fileSize( $source, array $predicates)
Get the size a file in storage will have when this operation is attempted.
Definition: FileOp.php:452
int bool $sourceSize
Definition: FileOp.php:56
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition: FileOp.php:503
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition: FileOp.php:327
getSourceSha1Base36()
precheckDestExistence() helper function to get the source file SHA-1.
Definition: FileOp.php:417
attemptQuick()
Attempt the operation without regards to prechecks.
Definition: FileOp.php:286
LoggerInterface $logger
Definition: FileOp.php:41
bool $cancelled
Definition: FileOp.php:53
FileBackendStore $backend
Definition: FileOp.php:39
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition: FileOp.php:181
getBackend()
Get the backend this operation is for.
Definition: FileOp.php:494
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition: FileOp.php:297
failed()
Check if this operation failed precheck() or attempt()
Definition: FileOp.php:140
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition: FileOp.php:83
doAttempt()
Definition: FileOp.php:264
precheckDestExistence(array $predicates)
Check for errors with regards to the destination file already existing.
Definition: FileOp.php:359
bool $failed
Definition: FileOp.php:49
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:98
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
$source
return true
Definition: router.php:90