MediaWiki master
FileOp.php
Go to the documentation of this file.
1<?php
25
26use Closure;
27use Exception;
28use InvalidArgumentException;
29use Psr\Log\LoggerInterface;
30use StatusValue;
33use Wikimedia\RequestTimeout\TimeoutException;
34
46abstract class FileOp {
48 protected $backend;
50 protected $logger;
51
53 protected $params = [];
54
56 protected $state = self::STATE_NEW;
58 protected $failed = false;
60 protected $async = false;
62 protected $noOp = false;
63
67 protected $destExists;
68
70 private const STATE_NEW = 1;
72 private const STATE_CHECKED = 2;
74 private const STATE_ATTEMPTED = 3;
75
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 newDependencies() {
150 return [ 'read' => [], 'write' => [] ];
151 }
152
159 final public function applyDependencies( array $deps ) {
160 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
161 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
162
163 return $deps;
164 }
165
172 final public function dependsOn( array $deps ) {
173 foreach ( $this->storagePathsChanged() as $path ) {
174 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
175 return true; // "output" or "anti" dependency
176 }
177 }
178 foreach ( $this->storagePathsRead() as $path ) {
179 if ( isset( $deps['write'][$path] ) ) {
180 return true; // "flow" dependency
181 }
182 }
183
184 return false;
185 }
186
195 final public function precheck( FileStatePredicates $predicates ) {
196 if ( $this->state !== self::STATE_NEW ) {
197 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
198 }
199 $this->state = self::STATE_CHECKED;
200
201 $status = StatusValue::newGood();
202 foreach ( $this->storagePathsReadOrChanged() as $path ) {
203 if ( !$this->backend->isPathUsableInternal( $path ) ) {
204 $status->fatal( 'backend-fail-usable', $path );
205 }
206 }
207 if ( !$status->isOK() ) {
208 return $status;
209 }
210
211 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
212 $status = $this->doPrecheck( $opPredicates, $predicates );
213 if ( !$status->isOK() ) {
214 $this->failed = true;
215 }
216
217 return $status;
218 }
219
229 protected function doPrecheck(
230 FileStatePredicates $opPredicates,
231 FileStatePredicates $batchPredicates
232 ) {
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->noOp ) {
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
362 protected function precheckDestExistence(
363 FileStatePredicates $opPredicates,
364 $sourceSize,
365 $sourceSha1
366 ) {
367 $status = StatusValue::newGood();
368 // Record the existence of destination file
369 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
370 // Check if an incompatible file exists at the destination
371 $this->overwriteSameCase = false;
372 if ( $this->destExists ) {
373 if ( $this->getParam( 'overwrite' ) ) {
374 return $status; // OK, no conflict
375 } elseif ( $this->getParam( 'overwriteSame' ) ) {
376 // Operation does nothing other than return an OK or bad status
377 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
378 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
379 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
380 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
381 // Check if hashes are valid and match each other...
382 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
383 $status->fatal( 'backend-fail-hashes' );
384 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
385 $status->fatal( 'backend-fail-sizes' );
386 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
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
412 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
413 return $opPredicates->resolveFileExistence(
414 $source,
415 function ( $path ) {
416 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
417 }
418 );
419 }
420
432 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
433 return $opPredicates->resolveFileSize(
434 $source,
435 function ( $path ) {
436 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
437 }
438 );
439 }
440
448 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
449 return $opPredicates->resolveFileSha1Base36(
450 $source,
451 function ( $path ) {
452 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
453 }
454 );
455 }
456
462 public function getBackend() {
463 return $this->backend;
464 }
465
471 final public function logFailure( $action ) {
472 try {
473 $this->logger->error( static::class . ' failed: ' . $action,
474 [ 'params' => $this->params ]
475 );
476 } catch ( TimeoutException $e ) {
477 throw $e;
478 } catch ( Exception $e ) {
479 // bad config? debug log error?
480 }
481 }
482}
483
485class_alias( FileOp::class, 'FileOp' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Base class for all backends using particular storage medium.
Base class for all file backend classes (including multi-write backends).
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:46
resolveFileSize( $source, FileStatePredicates $opPredicates)
Get the size a file in storage will have when this operation is attempted.
Definition FileOp.php:432
precheck(FileStatePredicates $predicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:195
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:140
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:149
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:131
allowedParams()
Get the file operation parameters.
Definition FileOp.php:308
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:345
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:448
int $state
Stage in the operation life-cycle.
Definition FileOp.php:56
bool $noOp
Whether the operation pre-check stage marked the attempt stage as a no-op.
Definition FileOp.php:62
resolveFileExistence( $source, FileStatePredicates $opPredicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:412
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:286
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:297
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:273
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:115
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:159
bool $async
Whether the operation is part of a concurrent sub-batch of operation.
Definition FileOp.php:60
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:172
doPrecheck(FileStatePredicates $opPredicates, FileStatePredicates $batchPredicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:229
attempt()
Attempt the operation.
Definition FileOp.php:241
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:327
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:471
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:362
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:336
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:318
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:83
getBackend()
Get the backend this operation is for.
Definition FileOp.php:462
bool $failed
Whether the operation pre-check or attempt stage failed.
Definition FileOp.php:58
Helper class for tracking counterfactual file states when pre-checking file operation batches.
resolveFileSha1Base36(string $path, $curSha1Func)
Get the hypothetical SHA-1 hash of a file given predicated and current state of files.
resolveFileSize(string $path, $curSizeFunc)
Get the hypothetical size of a file given predicated and current state of files.
resolveFileExistence(string $path, $curExistenceFunc)
Get the hypothetical existance a file given predicated and current state of files.
$source