MediaWiki master
FileOp.php
Go to the documentation of this file.
1<?php
23use Psr\Log\LoggerInterface;
24use Wikimedia\RequestTimeout\TimeoutException;
25
37abstract 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
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
$source