MediaWiki  master
FileOp.php
Go to the documentation of this file.
1 <?php
24 
36 abstract class FileOp {
38  protected $params = [];
39 
41  protected $backend;
43  protected $logger;
44 
46  protected $state = self::STATE_NEW;
47 
49  protected $failed = false;
50 
52  protected $async = false;
53 
55  protected $batchId;
56 
58  protected $doOperation = true;
59 
61  protected $sourceSha1;
62 
64  protected $overwriteSameCase;
65 
67  protected $destExists;
68 
69  /* Object life-cycle */
70  const STATE_NEW = 1;
71  const STATE_CHECKED = 2;
72  const STATE_ATTEMPTED = 3;
73 
82  final public function __construct(
83  FileBackendStore $backend, array $params, LoggerInterface $logger
84  ) {
85  $this->backend = $backend;
86  $this->logger = $logger;
87  list( $required, $optional, $paths ) = $this->allowedParams();
88  foreach ( $required as $name ) {
89  if ( isset( $params[$name] ) ) {
90  $this->params[$name] = $params[$name];
91  } else {
92  throw new InvalidArgumentException( "File operation missing parameter '$name'." );
93  }
94  }
95  foreach ( $optional as $name ) {
96  if ( isset( $params[$name] ) ) {
97  $this->params[$name] = $params[$name];
98  }
99  }
100  foreach ( $paths as $name ) {
101  if ( isset( $this->params[$name] ) ) {
102  // Normalize paths so the paths to the same file have the same string
103  $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
104  }
105  }
106  }
107 
114  protected static function normalizeIfValidStoragePath( $path ) {
117 
118  return $res ?? $path;
119  }
120 
121  return $path;
122  }
123 
129  final public function setBatchId( $batchId ) {
130  $this->batchId = $batchId;
131  }
132 
139  final public function getParam( $name ) {
140  return $this->params[$name] ?? null;
141  }
142 
148  final public function failed() {
149  return $this->failed;
150  }
151 
157  final public static function newPredicates() {
158  return [ 'exists' => [], 'sha1' => [] ];
159  }
160 
166  final public static function newDependencies() {
167  return [ 'read' => [], 'write' => [] ];
168  }
169 
176  final public function applyDependencies( array $deps ) {
177  $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
178  $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
179 
180  return $deps;
181  }
182 
189  final public function dependsOn( array $deps ) {
190  foreach ( $this->storagePathsChanged() as $path ) {
191  if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
192  return true; // "output" or "anti" dependency
193  }
194  }
195  foreach ( $this->storagePathsRead() as $path ) {
196  if ( isset( $deps['write'][$path] ) ) {
197  return true; // "flow" dependency
198  }
199  }
200 
201  return false;
202  }
203 
211  final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
212  if ( !$this->doOperation ) {
213  return []; // this is a no-op
214  }
215  $nullEntries = [];
216  $updateEntries = [];
217  $deleteEntries = [];
218  foreach ( $this->storagePathsReadOrChanged() as $path ) {
219  $nullEntries[] = [ // assertion for recovery
220  'op' => 'null',
221  'path' => $path,
222  'newSha1' => $this->fileSha1( $path, $oPredicates )
223  ];
224  }
225  foreach ( $this->storagePathsChanged() as $path ) {
226  if ( $nPredicates['sha1'][$path] === false ) { // deleted
227  $deleteEntries[] = [
228  'op' => 'delete',
229  'path' => $path,
230  'newSha1' => ''
231  ];
232  } else { // created/updated
233  $updateEntries[] = [
234  'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
235  'path' => $path,
236  'newSha1' => $nPredicates['sha1'][$path]
237  ];
238  }
239  }
240 
241  return array_merge( $nullEntries, $updateEntries, $deleteEntries );
242  }
243 
252  final public function precheck( array &$predicates ) {
253  if ( $this->state !== self::STATE_NEW ) {
254  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
255  }
256  $this->state = self::STATE_CHECKED;
257 
258  $status = StatusValue::newGood();
259  foreach ( $this->storagePathsReadOrChanged() as $path ) {
260  if ( !$this->backend->isPathUsableInternal( $path ) ) {
261  $status->fatal( 'backend-fail-usable', $path );
262  }
263  }
264  if ( !$status->isOK() ) {
265  return $status;
266  }
267 
268  $status = $this->doPrecheck( $predicates );
269  if ( !$status->isOK() ) {
270  $this->failed = true;
271  }
272 
273  return $status;
274  }
275 
280  protected function doPrecheck( array &$predicates ) {
281  return StatusValue::newGood();
282  }
283 
289  final public function attempt() {
290  if ( $this->state !== self::STATE_CHECKED ) {
291  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
292  } elseif ( $this->failed ) { // failed precheck
293  return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
294  }
295  $this->state = self::STATE_ATTEMPTED;
296  if ( $this->doOperation ) {
297  $status = $this->doAttempt();
298  if ( !$status->isOK() ) {
299  $this->failed = true;
300  $this->logFailure( 'attempt' );
301  }
302  } else { // no-op
303  $status = StatusValue::newGood();
304  }
305 
306  return $status;
307  }
308 
312  protected function doAttempt() {
313  return StatusValue::newGood();
314  }
315 
321  final public function attemptAsync() {
322  $this->async = true;
323  $result = $this->attempt();
324  $this->async = false;
325 
326  return $result;
327  }
328 
334  final public function attemptQuick() {
335  $this->state = self::STATE_CHECKED; // bypassed
336 
337  return $this->attempt();
338  }
339 
345  final public function attemptAsyncQuick() {
346  $this->state = self::STATE_CHECKED; // bypassed
347 
348  return $this->attemptAsync();
349  }
350 
356  protected function allowedParams() {
357  return [ [], [], [] ];
358  }
359 
366  protected function setFlags( array $params ) {
367  return [ 'async' => $this->async ] + $params;
368  }
369 
375  public function storagePathsRead() {
376  return [];
377  }
378 
384  public function storagePathsChanged() {
385  return [];
386  }
387 
393  final public function storagePathsReadOrChanged() {
394  return array_values( array_unique(
395  array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
396  ) );
397  }
398 
407  protected function precheckDestExistence( array $predicates ) {
408  $status = StatusValue::newGood();
409  // Get hash of source file/string and the destination file
410  $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
411  if ( $this->sourceSha1 === null ) { // file in storage?
412  $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
413  }
414  $this->overwriteSameCase = false;
415  $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
416  if ( $this->destExists ) {
417  if ( $this->getParam( 'overwrite' ) ) {
418  return $status; // OK
419  } elseif ( $this->getParam( 'overwriteSame' ) ) {
420  $dhash = $this->fileSha1( $this->params['dst'], $predicates );
421  // Check if hashes are valid and match each other...
422  if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
423  $status->fatal( 'backend-fail-hashes' );
424  } elseif ( $this->sourceSha1 !== $dhash ) {
425  // Give an error if the files are not identical
426  $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
427  } else {
428  $this->overwriteSameCase = true; // OK
429  }
430 
431  return $status; // do nothing; either OK or bad status
432  } else {
433  $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
434 
435  return $status;
436  }
437  } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
438  $status->fatal( 'backend-fail-stat', $this->params['dst'] );
439  }
440 
441  return $status;
442  }
443 
450  protected function getSourceSha1Base36() {
451  return null; // N/A
452  }
453 
464  final protected function fileExists( $source, array $predicates ) {
465  if ( isset( $predicates['exists'][$source] ) ) {
466  return $predicates['exists'][$source]; // previous op assures this
467  } else {
468  $params = [ 'src' => $source, 'latest' => true ];
469 
470  return $this->backend->fileExists( $params );
471  }
472  }
473 
484  final protected function fileSha1( $source, array $predicates ) {
485  if ( isset( $predicates['sha1'][$source] ) ) {
486  return $predicates['sha1'][$source]; // previous op assures this
487  } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
488  return false; // previous op assures this
489  } else {
490  $params = [ 'src' => $source, 'latest' => true ];
491 
492  return $this->backend->getFileSha1Base36( $params );
493  }
494  }
495 
501  public function getBackend() {
502  return $this->backend;
503  }
504 
510  final public function logFailure( $action ) {
512  $params['failedAction'] = $action;
513  try {
514  $this->logger->error( static::class .
515  " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
516  } catch ( Exception $e ) {
517  // bad config? debug log error?
518  }
519  }
520 }
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
getJournalEntries(array $oPredicates, array $nPredicates)
Get the file journal entries for this file operation.
Definition: FileOp.php:211
bool $failed
Definition: FileOp.php:49
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition: FileOp.php:114
FileBackend helper class for representing operations.
Definition: FileOp.php:36
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition: FileOp.php:176
array $params
Definition: FileOp.php:38
$source
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition: FileOp.php:366
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
bool $doOperation
Operation is not a no-op.
Definition: FileOp.php:58
string $batchId
Definition: FileOp.php:55
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
const STATE_CHECKED
Definition: FileOp.php:71
const STATE_NEW
Definition: FileOp.php:70
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition: FileOp.php:345
attempt()
Attempt the operation.
Definition: FileOp.php:289
attemptQuick()
Attempt the operation without regards to prechecks.
Definition: FileOp.php:334
getParam( $name)
Get the value of the parameter with the given name.
Definition: FileOp.php:139
int $state
Definition: FileOp.php:46
const STATE_ATTEMPTED
Definition: FileOp.php:72
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition: FileOp.php:375
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition: FileOp.php:189
static newPredicates()
Get a new empty predicates array for precheck()
Definition: FileOp.php:157
precheckDestExistence(array $predicates)
Check for errors with regards to the destination file already existing.
Definition: FileOp.php:407
attemptAsync()
Attempt the operation in the background.
Definition: FileOp.php:321
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
getSourceSha1Base36()
precheckDestExistence() helper function to get the source file SHA-1.
Definition: FileOp.php:450
setBatchId( $batchId)
Set the batch UUID this operation belongs to.
Definition: FileOp.php:129
allowedParams()
Get the file operation parameters.
Definition: FileOp.php:356
getBackend()
Get the backend this operation is for.
Definition: FileOp.php:501
doAttempt()
Definition: FileOp.php:312
Base class for all backends using particular storage medium.
precheck(array &$predicates)
Check preconditions of the operation without writing anything.
Definition: FileOp.php:252
FileBackendStore $backend
Definition: FileOp.php:41
doPrecheck(array &$predicates)
Definition: FileOp.php:280
fileExists( $source, array $predicates)
Check if a file will exist in storage when this operation is attempted.
Definition: FileOp.php:464
bool $destExists
Definition: FileOp.php:67
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition: FileOp.php:166
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition: FileOp.php:393
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition: FileOp.php:384
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition: FileOp.php:82
LoggerInterface $logger
Definition: FileOp.php:43
bool $async
Definition: FileOp.php:52
fileSha1( $source, array $predicates)
Get the SHA-1 hash a file in storage will have when this operation is attempted.
Definition: FileOp.php:484
string $sourceSha1
Definition: FileOp.php:61
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition: FileOp.php:510
return true
Definition: router.php:92
failed()
Check if this operation failed precheck() or attempt()
Definition: FileOp.php:148
bool $overwriteSameCase
Definition: FileOp.php:64