MediaWiki  1.34.0
FileOp.php
Go to the documentation of this file.
1 <?php
23 use Psr\Log\LoggerInterface;
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  $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
219  foreach ( array_unique( $pathsUsed ) as $path ) {
220  $nullEntries[] = [ // assertion for recovery
221  'op' => 'null',
222  'path' => $path,
223  'newSha1' => $this->fileSha1( $path, $oPredicates )
224  ];
225  }
226  foreach ( $this->storagePathsChanged() as $path ) {
227  if ( $nPredicates['sha1'][$path] === false ) { // deleted
228  $deleteEntries[] = [
229  'op' => 'delete',
230  'path' => $path,
231  'newSha1' => ''
232  ];
233  } else { // created/updated
234  $updateEntries[] = [
235  'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
236  'path' => $path,
237  'newSha1' => $nPredicates['sha1'][$path]
238  ];
239  }
240  }
241 
242  return array_merge( $nullEntries, $updateEntries, $deleteEntries );
243  }
244 
253  final public function precheck( array &$predicates ) {
254  if ( $this->state !== self::STATE_NEW ) {
255  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
256  }
257  $this->state = self::STATE_CHECKED;
258 
260  $storagePaths = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
261  foreach ( array_unique( $storagePaths ) as $storagePath ) {
262  if ( !$this->backend->isPathUsableInternal( $storagePath ) ) {
263  $status->fatal( 'backend-fail-usable', $storagePath );
264  }
265  }
266  if ( !$status->isOK() ) {
267  return $status;
268  }
269 
270  $status = $this->doPrecheck( $predicates );
271  if ( !$status->isOK() ) {
272  $this->failed = true;
273  }
274 
275  return $status;
276  }
277 
282  protected function doPrecheck( array &$predicates ) {
283  return StatusValue::newGood();
284  }
285 
291  final public function attempt() {
292  if ( $this->state !== self::STATE_CHECKED ) {
293  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
294  } elseif ( $this->failed ) { // failed precheck
295  return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
296  }
297  $this->state = self::STATE_ATTEMPTED;
298  if ( $this->doOperation ) {
299  $status = $this->doAttempt();
300  if ( !$status->isOK() ) {
301  $this->failed = true;
302  $this->logFailure( 'attempt' );
303  }
304  } else { // no-op
306  }
307 
308  return $status;
309  }
310 
314  protected function doAttempt() {
315  return StatusValue::newGood();
316  }
317 
323  final public function attemptAsync() {
324  $this->async = true;
325  $result = $this->attempt();
326  $this->async = false;
327 
328  return $result;
329  }
330 
336  protected function allowedParams() {
337  return [ [], [], [] ];
338  }
339 
346  protected function setFlags( array $params ) {
347  return [ 'async' => $this->async ] + $params;
348  }
349 
355  public function storagePathsRead() {
356  return [];
357  }
358 
364  public function storagePathsChanged() {
365  return [];
366  }
367 
376  protected function precheckDestExistence( array $predicates ) {
378  // Get hash of source file/string and the destination file
379  $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
380  if ( $this->sourceSha1 === null ) { // file in storage?
381  $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
382  }
383  $this->overwriteSameCase = false;
384  $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
385  if ( $this->destExists ) {
386  if ( $this->getParam( 'overwrite' ) ) {
387  return $status; // OK
388  } elseif ( $this->getParam( 'overwriteSame' ) ) {
389  $dhash = $this->fileSha1( $this->params['dst'], $predicates );
390  // Check if hashes are valid and match each other...
391  if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
392  $status->fatal( 'backend-fail-hashes' );
393  } elseif ( $this->sourceSha1 !== $dhash ) {
394  // Give an error if the files are not identical
395  $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
396  } else {
397  $this->overwriteSameCase = true; // OK
398  }
399 
400  return $status; // do nothing; either OK or bad status
401  } else {
402  $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
403 
404  return $status;
405  }
406  } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
407  $status->fatal( 'backend-fail-stat', $this->params['dst'] );
408  }
409 
410  return $status;
411  }
412 
419  protected function getSourceSha1Base36() {
420  return null; // N/A
421  }
422 
433  final protected function fileExists( $source, array $predicates ) {
434  if ( isset( $predicates['exists'][$source] ) ) {
435  return $predicates['exists'][$source]; // previous op assures this
436  } else {
437  $params = [ 'src' => $source, 'latest' => true ];
438 
439  return $this->backend->fileExists( $params );
440  }
441  }
442 
453  final protected function fileSha1( $source, array $predicates ) {
454  if ( isset( $predicates['sha1'][$source] ) ) {
455  return $predicates['sha1'][$source]; // previous op assures this
456  } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
457  return false; // previous op assures this
458  } else {
459  $params = [ 'src' => $source, 'latest' => true ];
460 
461  return $this->backend->getFileSha1Base36( $params );
462  }
463  }
464 
470  public function getBackend() {
471  return $this->backend;
472  }
473 
479  final public function logFailure( $action ) {
481  $params['failedAction'] = $action;
482  try {
483  $this->logger->error( static::class .
484  " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
485  } catch ( Exception $e ) {
486  // bad config? debug log error?
487  }
488  }
489 }
FileOp\$sourceSha1
string $sourceSha1
Definition: FileOp.php:61
FileOp\logFailure
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition: FileOp.php:479
FileOp\setBatchId
setBatchId( $batchId)
Set the batch UUID this operation belongs to.
Definition: FileOp.php:129
FileOp\$doOperation
bool $doOperation
Operation is not a no-op.
Definition: FileOp.php:58
FileOp\fileSha1
fileSha1( $source, array $predicates)
Get the SHA-1 hash a file in storage will have when this operation is attempted.
Definition: FileOp.php:453
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
FileOp\allowedParams
allowedParams()
Get the file operation parameters.
Definition: FileOp.php:336
FileOp
FileBackend helper class for representing operations.
Definition: FileOp.php:36
true
return true
Definition: router.php:92
FileOp\$destExists
bool $destExists
Definition: FileOp.php:67
FileOp\doAttempt
doAttempt()
Definition: FileOp.php:314
FileOp\precheck
precheck(array &$predicates)
Check preconditions of the operation without writing anything.
Definition: FileOp.php:253
FileBackend\normalizeStoragePath
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
Definition: FileBackend.php:1543
FileOp\getJournalEntries
getJournalEntries(array $oPredicates, array $nPredicates)
Get the file journal entries for this file operation.
Definition: FileOp.php:211
FileOp\doPrecheck
doPrecheck(array &$predicates)
Definition: FileOp.php:282
FileOp\$logger
LoggerInterface $logger
Definition: FileOp.php:43
FileOp\storagePathsChanged
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition: FileOp.php:364
$res
$res
Definition: testCompression.php:52
FileOp\$failed
bool $failed
Definition: FileOp.php:49
FileOp\applyDependencies
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition: FileOp.php:176
FileOp\attemptAsync
attemptAsync()
Attempt the operation in the background.
Definition: FileOp.php:323
FileOp\__construct
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition: FileOp.php:82
FileOp\$async
bool $async
Definition: FileOp.php:52
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
FileOp\getBackend
getBackend()
Get the backend this operation is for.
Definition: FileOp.php:470
FileOp\$batchId
string $batchId
Definition: FileOp.php:55
FileOp\failed
failed()
Check if this operation failed precheck() or attempt()
Definition: FileOp.php:148
FileBackend\isStoragePath
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
Definition: FileBackend.php:1508
FileOp\setFlags
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition: FileOp.php:346
FileOp\$backend
FileBackendStore $backend
Definition: FileOp.php:41
FileOp\attempt
attempt()
Attempt the operation.
Definition: FileOp.php:291
FileOp\getParam
getParam( $name)
Get the value of the parameter with the given name.
Definition: FileOp.php:139
FileOp\STATE_NEW
const STATE_NEW
Definition: FileOp.php:70
FileOp\STATE_ATTEMPTED
const STATE_ATTEMPTED
Definition: FileOp.php:72
FileOp\dependsOn
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition: FileOp.php:189
FileOp\$params
array $params
Definition: FileOp.php:38
FileOp\$state
int $state
Definition: FileOp.php:46
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
FileBackendStore
Base class for all backends using particular storage medium.
Definition: FileBackendStore.php:40
FileOp\getSourceSha1Base36
getSourceSha1Base36()
precheckDestExistence() helper function to get the source file SHA-1.
Definition: FileOp.php:419
FileOp\STATE_CHECKED
const STATE_CHECKED
Definition: FileOp.php:71
$status
return $status
Definition: SyntaxHighlight.php:347
FileOp\$overwriteSameCase
bool $overwriteSameCase
Definition: FileOp.php:64
$path
$path
Definition: NoLocalSettings.php:25
$source
$source
Definition: mwdoc-filter.php:34
FileOp\fileExists
fileExists( $source, array $predicates)
Check if a file will exist in storage when this operation is attempted.
Definition: FileOp.php:433
FileOp\precheckDestExistence
precheckDestExistence(array $predicates)
Check for errors with regards to the destination file already existing.
Definition: FileOp.php:376
FileOp\newDependencies
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition: FileOp.php:166
FileOp\normalizeIfValidStoragePath
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition: FileOp.php:114
FileOp\newPredicates
static newPredicates()
Get a new empty predicates array for precheck()
Definition: FileOp.php:157
FileOp\storagePathsRead
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition: FileOp.php:355