MediaWiki  master
FileOp.php
Go to the documentation of this file.
1 <?php
23 use Psr\Log\LoggerInterface;
24 
36 abstract class FileOp {
38  protected $backend;
40  protected $logger;
41 
43  protected $params = [];
44 
46  protected $state = self::STATE_NEW;
48  protected $failed = false;
50  protected $async = false;
52  protected $cancelled = false;
53 
55  protected $sourceSize;
57  protected $sourceSha1;
58 
60  protected $overwriteSameCase;
61 
63  protected $destExists;
64 
65  /* Object life-cycle */
66  private const STATE_NEW = 1;
67  private const STATE_CHECKED = 2;
68  private const STATE_ATTEMPTED = 3;
69 
70  protected const ASSUMED_SHA1 = 'sha1';
71  protected const ASSUMED_EXISTS = 'exists';
72  protected const ASSUMED_SIZE = 'size';
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 
130  final public function getParam( $name ) {
131  return $this->params[$name] ?? null;
132  }
133 
139  final public function failed() {
140  return $this->failed;
141  }
142 
148  final public static function newPredicates() {
149  return [ self::ASSUMED_EXISTS => [], self::ASSUMED_SHA1 => [], self::ASSUMED_SIZE => [] ];
150  }
151 
157  final public static function newDependencies() {
158  return [ 'read' => [], 'write' => [] ];
159  }
160 
167  final public function applyDependencies( array $deps ) {
168  $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
169  $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
170 
171  return $deps;
172  }
173 
180  final public function dependsOn( array $deps ) {
181  foreach ( $this->storagePathsChanged() as $path ) {
182  if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
183  return true; // "output" or "anti" dependency
184  }
185  }
186  foreach ( $this->storagePathsRead() as $path ) {
187  if ( isset( $deps['write'][$path] ) ) {
188  return true; // "flow" dependency
189  }
190  }
191 
192  return false;
193  }
194 
203  final public function precheck( array &$predicates ) {
204  if ( $this->state !== self::STATE_NEW ) {
205  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
206  }
207  $this->state = self::STATE_CHECKED;
208 
209  $status = StatusValue::newGood();
210  foreach ( $this->storagePathsReadOrChanged() as $path ) {
211  if ( !$this->backend->isPathUsableInternal( $path ) ) {
212  $status->fatal( 'backend-fail-usable', $path );
213  }
214  }
215  if ( !$status->isOK() ) {
216  return $status;
217  }
218 
219  $status = $this->doPrecheck( $predicates );
220  if ( !$status->isOK() ) {
221  $this->failed = true;
222  }
223 
224  return $status;
225  }
226 
231  protected function doPrecheck( array &$predicates ) {
232  return StatusValue::newGood();
233  }
234 
240  final public function attempt() {
241  if ( $this->state !== self::STATE_CHECKED ) {
242  return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
243  } elseif ( $this->failed ) { // failed precheck
244  return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
245  }
246  $this->state = self::STATE_ATTEMPTED;
247  if ( $this->cancelled ) {
248  $status = StatusValue::newGood(); // no-op
249  } else {
250  $status = $this->doAttempt();
251  if ( !$status->isOK() ) {
252  $this->failed = true;
253  $this->logFailure( 'attempt' );
254  }
255  }
256 
257  return $status;
258  }
259 
263  protected function doAttempt() {
264  return StatusValue::newGood();
265  }
266 
272  final public function attemptAsync() {
273  $this->async = true;
274  $result = $this->attempt();
275  $this->async = false;
276 
277  return $result;
278  }
279 
285  final public function attemptQuick() {
286  $this->state = self::STATE_CHECKED; // bypassed
287 
288  return $this->attempt();
289  }
290 
296  final public function attemptAsyncQuick() {
297  $this->state = self::STATE_CHECKED; // bypassed
298 
299  return $this->attemptAsync();
300  }
301 
307  protected function allowedParams() {
308  return [ [], [], [] ];
309  }
310 
317  protected function setFlags( array $params ) {
318  return [ 'async' => $this->async ] + $params;
319  }
320 
326  public function storagePathsRead() {
327  return [];
328  }
329 
335  public function storagePathsChanged() {
336  return [];
337  }
338 
344  final public function storagePathsReadOrChanged() {
345  return array_values( array_unique(
346  array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
347  ) );
348  }
349 
358  protected function precheckDestExistence( array $predicates ) {
359  $status = StatusValue::newGood();
360  // Record the size of source file/string
361  $this->sourceSize = $this->getSourceSize(); // FS file or data string
362  if ( $this->sourceSize === null ) { // file in storage?
363  $this->sourceSize = $this->fileSize( $this->params['src'], $predicates );
364  }
365  // Record the hash of source file/string
366  $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
367  if ( $this->sourceSha1 === null ) { // file in storage?
368  $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
369  }
370  // Record the existence of destination file
371  $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
372  // Check if an incompatible file exists at the destination
373  $this->overwriteSameCase = false;
374  if ( $this->destExists ) {
375  if ( $this->getParam( 'overwrite' ) ) {
376  return $status; // OK, no conflict
377  } elseif ( $this->getParam( 'overwriteSame' ) ) {
378  // Operation does nothing other than return an OK or bad status
379  $dhash = $this->fileSha1( $this->params['dst'], $predicates );
380  $dsize = $this->fileSize( $this->params['dst'], $predicates );
381  // Check if hashes are valid and match each other...
382  if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
383  $status->fatal( 'backend-fail-hashes' );
384  } elseif ( !is_int( $this->sourceSize ) || !is_int( $dsize ) ) {
385  $status->fatal( 'backend-fail-sizes' );
386  } elseif ( $this->sourceSha1 !== $dhash || $this->sourceSize !== $dsize ) {
387  // Give an error if the files are not identical
388  $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
389  } else {
390  $this->overwriteSameCase = true; // OK
391  }
392  } else {
393  $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
394  }
395  } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
396  $status->fatal( 'backend-fail-stat', $this->params['dst'] );
397  }
398 
399  return $status;
400  }
401 
408  protected function getSourceSize() {
409  return null; // N/A
410  }
411 
418  protected function getSourceSha1Base36() {
419  return null; // N/A
420  }
421 
432  final protected function fileExists( $source, array $predicates ) {
433  if ( isset( $predicates[self::ASSUMED_EXISTS][$source] ) ) {
434  return $predicates[self::ASSUMED_EXISTS][$source]; // previous op assures this
435  } else {
436  $params = [ 'src' => $source, 'latest' => true ];
437 
438  return $this->backend->fileExists( $params );
439  }
440  }
441 
453  final protected function fileSize( $source, array $predicates ) {
454  if ( isset( $predicates[self::ASSUMED_SIZE][$source] ) ) {
455  return $predicates[self::ASSUMED_SIZE][$source]; // previous op assures this
456  } elseif (
457  isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
458  !$predicates[self::ASSUMED_EXISTS][$source]
459  ) {
460  return false; // previous op assures this
461  } else {
462  $params = [ 'src' => $source, 'latest' => true ];
463 
464  return $this->backend->getFileSize( $params );
465  }
466  }
467 
475  final protected function fileSha1( $source, array $predicates ) {
476  if ( isset( $predicates[self::ASSUMED_SHA1][$source] ) ) {
477  return $predicates[self::ASSUMED_SHA1][$source]; // previous op assures this
478  } elseif (
479  isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
480  !$predicates[self::ASSUMED_EXISTS][$source]
481  ) {
482  return false; // previous op assures this
483  } else {
484  $params = [ 'src' => $source, 'latest' => true ];
485 
486  return $this->backend->getFileSha1Base36( $params );
487  }
488  }
489 
495  public function getBackend() {
496  return $this->backend;
497  }
498 
504  final public function logFailure( $action ) {
506  $params['failedAction'] = $action;
507  try {
508  $this->logger->error( static::class .
509  " failed: " . FormatJson::encode( $params ) );
510  } catch ( Exception $e ) {
511  // bad config? debug log error?
512  }
513  }
514 }
FileOp\logFailure
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition: FileOp.php:504
FileOp\fileSha1
fileSha1( $source, array $predicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition: FileOp.php:475
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
FileOp\allowedParams
allowedParams()
Get the file operation parameters.
Definition: FileOp.php:307
FileOp\attemptAsyncQuick
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition: FileOp.php:296
FileOp
FileBackend helper class for representing operations.
Definition: FileOp.php:36
true
return true
Definition: router.php:90
FileOp\$destExists
bool $destExists
Definition: FileOp.php:63
FileOp\doAttempt
doAttempt()
Definition: FileOp.php:263
FileOp\attemptQuick
attemptQuick()
Attempt the operation without regards to prechecks.
Definition: FileOp.php:285
FileOp\precheck
precheck(array &$predicates)
Check preconditions of the operation without writing anything.
Definition: FileOp.php:203
FileBackend\normalizeStoragePath
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
Definition: FileBackend.php:1545
FileOp\doPrecheck
doPrecheck(array &$predicates)
Definition: FileOp.php:231
FileOp\$logger
LoggerInterface $logger
Definition: FileOp.php:40
FileOp\storagePathsChanged
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition: FileOp.php:335
FileOp\ASSUMED_EXISTS
const ASSUMED_EXISTS
Definition: FileOp.php:71
$res
$res
Definition: testCompression.php:57
FileOp\$failed
bool $failed
Definition: FileOp.php:48
FileOp\applyDependencies
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition: FileOp.php:167
FileOp\attemptAsync
attemptAsync()
Attempt the operation in the background.
Definition: FileOp.php:272
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:50
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
FileOp\getBackend
getBackend()
Get the backend this operation is for.
Definition: FileOp.php:495
FileOp\getSourceSize
getSourceSize()
precheckDestExistence() helper function to get the source file size.
Definition: FileOp.php:408
FileOp\failed
failed()
Check if this operation failed precheck() or attempt()
Definition: FileOp.php:139
FileOp\ASSUMED_SHA1
const ASSUMED_SHA1
Definition: FileOp.php:70
FileBackend\isStoragePath
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
Definition: FileBackend.php:1510
FileOp\setFlags
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition: FileOp.php:317
FileOp\fileSize
fileSize( $source, array $predicates)
Get the size a file in storage will have when this operation is attempted.
Definition: FileOp.php:453
FileOp\$backend
FileBackendStore $backend
Definition: FileOp.php:38
FileOp\attempt
attempt()
Attempt the operation.
Definition: FileOp.php:240
FileOp\getParam
getParam( $name)
Get the value of the parameter with the given name.
Definition: FileOp.php:130
FileOp\STATE_NEW
const STATE_NEW
Definition: FileOp.php:66
FileOp\STATE_ATTEMPTED
const STATE_ATTEMPTED
Definition: FileOp.php:68
FileOp\dependsOn
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition: FileOp.php:180
FileOp\$sourceSha1
string bool $sourceSha1
Definition: FileOp.php:57
FileOp\$params
array $params
Definition: FileOp.php:43
FileOp\$state
int $state
Definition: FileOp.php:46
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
FileOp\$cancelled
bool $cancelled
Definition: FileOp.php:52
FileBackendStore
Base class for all backends using particular storage medium.
Definition: FileBackendStore.php:41
FileOp\getSourceSha1Base36
getSourceSha1Base36()
precheckDestExistence() helper function to get the source file SHA-1.
Definition: FileOp.php:418
FileOp\STATE_CHECKED
const STATE_CHECKED
Definition: FileOp.php:67
FileOp\$overwriteSameCase
bool $overwriteSameCase
Definition: FileOp.php:60
$path
$path
Definition: NoLocalSettings.php:25
FileOp\ASSUMED_SIZE
const ASSUMED_SIZE
Definition: FileOp.php:72
FileOp\$sourceSize
int bool $sourceSize
Definition: FileOp.php:55
$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:432
FileOp\precheckDestExistence
precheckDestExistence(array $predicates)
Check for errors with regards to the destination file already existing.
Definition: FileOp.php:358
FileOp\newDependencies
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition: FileOp.php:157
FileOp\normalizeIfValidStoragePath
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition: FileOp.php:114
FileOp\storagePathsReadOrChanged
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition: FileOp.php:344
FileOp\newPredicates
static newPredicates()
Get a new empty predicates array for precheck()
Definition: FileOp.php:148
FileOp\storagePathsRead
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition: FileOp.php:326