MediaWiki master
FileOp.php
Go to the documentation of this file.
1<?php
25
26use Closure;
27use Exception;
28use InvalidArgumentException;
30use Psr\Log\LoggerInterface;
31use StatusValue;
34use Wikimedia\RequestTimeout\TimeoutException;
35
47abstract class FileOp {
49 protected $backend;
51 protected $logger;
52
54 protected $params = [];
55
57 protected $state = self::STATE_NEW;
59 protected $failed = false;
61 protected $async = false;
63 protected $noOp = false;
64
68 protected $destExists;
69
71 private const STATE_NEW = 1;
73 private const STATE_CHECKED = 2;
75 private const STATE_ATTEMPTED = 3;
76
84 final public function __construct(
85 FileBackendStore $backend, array $params, LoggerInterface $logger
86 ) {
87 $this->backend = $backend;
88 $this->logger = $logger;
89 [ $required, $optional, $paths ] = $this->allowedParams();
90 foreach ( $required as $name ) {
91 if ( isset( $params[$name] ) ) {
92 $this->params[$name] = $params[$name];
93 } else {
94 throw new InvalidArgumentException( "File operation missing parameter '$name'." );
95 }
96 }
97 foreach ( $optional as $name ) {
98 if ( isset( $params[$name] ) ) {
99 $this->params[$name] = $params[$name];
100 }
101 }
102 foreach ( $paths as $name ) {
103 if ( isset( $this->params[$name] ) ) {
104 // Normalize paths so the paths to the same file have the same string
105 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
106 }
107 }
108 }
109
116 protected static function normalizeIfValidStoragePath( $path ) {
119
120 return $res ?? $path;
121 }
122
123 return $path;
124 }
125
132 final public function getParam( $name ) {
133 return $this->params[$name] ?? null;
134 }
135
141 final public function failed() {
142 return $this->failed;
143 }
144
150 final public static function newDependencies() {
151 return [ 'read' => [], 'write' => [] ];
152 }
153
160 final public function applyDependencies( array $deps ) {
161 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
162 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
163
164 return $deps;
165 }
166
173 final public function dependsOn( array $deps ) {
174 foreach ( $this->storagePathsChanged() as $path ) {
175 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
176 return true; // "output" or "anti" dependency
177 }
178 }
179 foreach ( $this->storagePathsRead() as $path ) {
180 if ( isset( $deps['write'][$path] ) ) {
181 return true; // "flow" dependency
182 }
183 }
184
185 return false;
186 }
187
196 final public function precheck( FileStatePredicates $predicates ) {
197 if ( $this->state !== self::STATE_NEW ) {
198 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
199 }
200 $this->state = self::STATE_CHECKED;
201
202 $status = StatusValue::newGood();
203 foreach ( $this->storagePathsReadOrChanged() as $path ) {
204 if ( !$this->backend->isPathUsableInternal( $path ) ) {
205 $status->fatal( 'backend-fail-usable', $path );
206 }
207 }
208 if ( !$status->isOK() ) {
209 return $status;
210 }
211
212 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
213 $status = $this->doPrecheck( $opPredicates, $predicates );
214 if ( !$status->isOK() ) {
215 $this->failed = true;
216 }
217
218 return $status;
219 }
220
230 protected function doPrecheck(
231 FileStatePredicates $opPredicates,
232 FileStatePredicates $batchPredicates
233 ) {
234 return StatusValue::newGood();
235 }
236
242 final public function attempt() {
243 if ( $this->state !== self::STATE_CHECKED ) {
244 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
245 } elseif ( $this->failed ) { // failed precheck
246 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
247 }
248 $this->state = self::STATE_ATTEMPTED;
249 if ( $this->noOp ) {
250 $status = StatusValue::newGood(); // no-op
251 } else {
252 $status = $this->doAttempt();
253 if ( !$status->isOK() ) {
254 $this->failed = true;
255 $this->logFailure( 'attempt' );
256 }
257 }
258
259 return $status;
260 }
261
265 protected function doAttempt() {
266 return StatusValue::newGood();
267 }
268
274 final public function attemptAsync() {
275 $this->async = true;
276 $result = $this->attempt();
277 $this->async = false;
278
279 return $result;
280 }
281
287 final public function attemptQuick() {
288 $this->state = self::STATE_CHECKED; // bypassed
289
290 return $this->attempt();
291 }
292
298 final public function attemptAsyncQuick() {
299 $this->state = self::STATE_CHECKED; // bypassed
300
301 return $this->attemptAsync();
302 }
303
309 protected function allowedParams() {
310 return [ [], [], [] ];
311 }
312
319 protected function setFlags( array $params ) {
320 return [ 'async' => $this->async ] + $params;
321 }
322
328 public function storagePathsRead() {
329 return [];
330 }
331
337 public function storagePathsChanged() {
338 return [];
339 }
340
346 final public function storagePathsReadOrChanged() {
347 return array_values( array_unique(
348 array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
349 ) );
350 }
351
363 protected function precheckDestExistence(
364 FileStatePredicates $opPredicates,
365 $sourceSize,
366 $sourceSha1
367 ) {
368 $status = StatusValue::newGood();
369 // Record the existence of destination file
370 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
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 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
379 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
380 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
381 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
382 // Check if hashes are valid and match each other...
383 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
384 $status->fatal( 'backend-fail-hashes' );
385 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
386 $status->fatal( 'backend-fail-sizes' );
387 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
388 // Give an error if the files are not identical
389 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
390 } else {
391 $this->overwriteSameCase = true; // OK
392 }
393 } else {
394 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
395 }
396 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
397 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
398 }
399
400 return $status;
401 }
402
413 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
414 return $opPredicates->resolveFileExistence(
415 $source,
416 function ( $path ) {
417 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
418 }
419 );
420 }
421
433 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
434 return $opPredicates->resolveFileSize(
435 $source,
436 function ( $path ) {
437 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
438 }
439 );
440 }
441
449 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
450 return $opPredicates->resolveFileSha1Base36(
451 $source,
452 function ( $path ) {
453 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
454 }
455 );
456 }
457
463 public function getBackend() {
464 return $this->backend;
465 }
466
472 final public function logFailure( $action ) {
474 $params['failedAction'] = $action;
475 try {
476 $this->logger->error( static::class .
477 " failed: " . FormatJson::encode( $params ) );
478 } catch ( TimeoutException $e ) {
479 throw $e;
480 } catch ( Exception $e ) {
481 // bad config? debug log error?
482 }
483 }
484}
485
487class_alias( FileOp::class, 'FileOp' );
JSON formatter wrapper class.
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:47
resolveFileSize( $source, FileStatePredicates $opPredicates)
Get the size a file in storage will have when this operation is attempted.
Definition FileOp.php:433
precheck(FileStatePredicates $predicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:196
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:141
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:150
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:132
allowedParams()
Get the file operation parameters.
Definition FileOp.php:309
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:346
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:449
int $state
Stage in the operation life-cycle.
Definition FileOp.php:57
bool $noOp
Whether the operation pre-check stage marked the attempt stage as a no-op.
Definition FileOp.php:63
resolveFileExistence( $source, FileStatePredicates $opPredicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:413
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:287
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:298
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:274
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:116
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:160
bool $async
Whether the operation is part of a concurrent sub-batch of operation.
Definition FileOp.php:61
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:173
doPrecheck(FileStatePredicates $opPredicates, FileStatePredicates $batchPredicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:230
attempt()
Attempt the operation.
Definition FileOp.php:242
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:328
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:472
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:363
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:337
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:319
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:84
getBackend()
Get the backend this operation is for.
Definition FileOp.php:463
bool $failed
Whether the operation pre-check or attempt stage failed.
Definition FileOp.php:59
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