MediaWiki master
FileOp.php
Go to the documentation of this file.
1<?php
11
12use Closure;
13use Exception;
14use InvalidArgumentException;
15use Psr\Log\LoggerInterface;
16use StatusValue;
19use Wikimedia\RequestTimeout\TimeoutException;
20
32abstract class FileOp {
34 protected $backend;
36 protected $logger;
37
39 protected $params = [];
40
42 protected $state = self::STATE_NEW;
44 protected $failed = false;
46 protected $async = false;
48 protected $noOp = false;
49
53 protected $destExists;
54
56 private const STATE_NEW = 1;
58 private const STATE_CHECKED = 2;
60 private const STATE_ATTEMPTED = 3;
61
69 final public function __construct(
70 FileBackendStore $backend, array $params, LoggerInterface $logger
71 ) {
72 $this->backend = $backend;
73 $this->logger = $logger;
74 [ $required, $optional, $paths ] = $this->allowedParams();
75 foreach ( $required as $name ) {
76 if ( isset( $params[$name] ) ) {
77 $this->params[$name] = $params[$name];
78 } else {
79 throw new InvalidArgumentException( "File operation missing parameter '$name'." );
80 }
81 }
82 foreach ( $optional as $name ) {
83 if ( isset( $params[$name] ) ) {
84 $this->params[$name] = $params[$name];
85 }
86 }
87 foreach ( $paths as $name ) {
88 if ( isset( $this->params[$name] ) ) {
89 // Normalize paths so the paths to the same file have the same string
90 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
91 }
92 }
93 }
94
101 protected static function normalizeIfValidStoragePath( $path ) {
104
105 return $res ?? $path;
106 }
107
108 return $path;
109 }
110
117 final public function getParam( $name ) {
118 return $this->params[$name] ?? null;
119 }
120
126 final public function failed() {
127 return $this->failed;
128 }
129
135 final public static function newDependencies() {
136 return [ 'read' => [], 'write' => [] ];
137 }
138
145 final public function applyDependencies( array $deps ) {
146 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
147 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
148
149 return $deps;
150 }
151
158 final public function dependsOn( array $deps ) {
159 foreach ( $this->storagePathsChanged() as $path ) {
160 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
161 return true; // "output" or "anti" dependency
162 }
163 }
164 foreach ( $this->storagePathsRead() as $path ) {
165 if ( isset( $deps['write'][$path] ) ) {
166 return true; // "flow" dependency
167 }
168 }
169
170 return false;
171 }
172
181 final public function precheck( FileStatePredicates $predicates ) {
182 if ( $this->state !== self::STATE_NEW ) {
183 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
184 }
185 $this->state = self::STATE_CHECKED;
186
187 $status = StatusValue::newGood();
188 foreach ( $this->storagePathsReadOrChanged() as $path ) {
189 if ( !$this->backend->isPathUsableInternal( $path ) ) {
190 $status->fatal( 'backend-fail-usable', $path );
191 }
192 }
193 if ( !$status->isOK() ) {
194 return $status;
195 }
196
197 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
198 $status = $this->doPrecheck( $opPredicates, $predicates );
199 if ( !$status->isOK() ) {
200 $this->failed = true;
201 }
202
203 return $status;
204 }
205
215 protected function doPrecheck(
216 FileStatePredicates $opPredicates,
217 FileStatePredicates $batchPredicates
218 ) {
219 return StatusValue::newGood();
220 }
221
227 final public function attempt() {
228 if ( $this->state !== self::STATE_CHECKED ) {
229 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
230 } elseif ( $this->failed ) { // failed precheck
231 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
232 }
233 $this->state = self::STATE_ATTEMPTED;
234 if ( $this->noOp ) {
235 $status = StatusValue::newGood(); // no-op
236 } else {
237 $status = $this->doAttempt();
238 if ( !$status->isOK() ) {
239 $this->failed = true;
240 $this->logFailure( 'attempt' );
241 }
242 }
243
244 return $status;
245 }
246
250 protected function doAttempt() {
251 return StatusValue::newGood();
252 }
253
259 final public function attemptAsync() {
260 $this->async = true;
261 $result = $this->attempt();
262 $this->async = false;
263
264 return $result;
265 }
266
272 final public function attemptQuick() {
273 $this->state = self::STATE_CHECKED; // bypassed
274
275 return $this->attempt();
276 }
277
283 final public function attemptAsyncQuick() {
284 $this->state = self::STATE_CHECKED; // bypassed
285
286 return $this->attemptAsync();
287 }
288
294 protected function allowedParams() {
295 return [ [], [], [] ];
296 }
297
304 protected function setFlags( array $params ) {
305 return [ 'async' => $this->async ] + $params;
306 }
307
313 public function storagePathsRead() {
314 return [];
315 }
316
322 public function storagePathsChanged() {
323 return [];
324 }
325
331 final public function storagePathsReadOrChanged() {
332 return array_values( array_unique(
333 array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
334 ) );
335 }
336
348 protected function precheckDestExistence(
349 FileStatePredicates $opPredicates,
350 $sourceSize,
351 $sourceSha1
352 ) {
353 $status = StatusValue::newGood();
354 // Record the existence of destination file
355 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
356 // Check if an incompatible file exists at the destination
357 $this->overwriteSameCase = false;
358 if ( $this->destExists ) {
359 if ( $this->getParam( 'overwrite' ) ) {
360 return $status; // OK, no conflict
361 } elseif ( $this->getParam( 'overwriteSame' ) ) {
362 // Operation does nothing other than return an OK or bad status
363 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
364 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
365 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
366 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
367 // Check if hashes are valid and match each other...
368 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
369 $status->fatal( 'backend-fail-hashes' );
370 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
371 $status->fatal( 'backend-fail-sizes' );
372 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
373 // Give an error if the files are not identical
374 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
375 } else {
376 $this->overwriteSameCase = true; // OK
377 }
378 } else {
379 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
380 }
381 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
382 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
383 }
384
385 return $status;
386 }
387
398 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
399 return $opPredicates->resolveFileExistence(
400 $source,
401 function ( $path ) {
402 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
403 }
404 );
405 }
406
418 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
419 return $opPredicates->resolveFileSize(
420 $source,
421 function ( $path ) {
422 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
423 }
424 );
425 }
426
434 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
435 return $opPredicates->resolveFileSha1Base36(
436 $source,
437 function ( $path ) {
438 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
439 }
440 );
441 }
442
448 public function getBackend() {
449 return $this->backend;
450 }
451
457 final public function logFailure( $action ) {
458 try {
459 $this->logger->error( static::class . ' failed: ' . $action,
460 [ 'params' => $this->params ]
461 );
462 } catch ( TimeoutException $e ) {
463 throw $e;
464 } catch ( Exception ) {
465 // bad config? debug log error?
466 }
467 }
468}
469
471class_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:32
resolveFileSize( $source, FileStatePredicates $opPredicates)
Get the size a file in storage will have when this operation is attempted.
Definition FileOp.php:418
precheck(FileStatePredicates $predicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:181
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:126
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:135
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:117
allowedParams()
Get the file operation parameters.
Definition FileOp.php:294
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:331
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:434
int $state
Stage in the operation life-cycle.
Definition FileOp.php:42
bool $noOp
Whether the operation pre-check stage marked the attempt stage as a no-op.
Definition FileOp.php:48
resolveFileExistence( $source, FileStatePredicates $opPredicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:398
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:272
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:283
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:259
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:101
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:145
bool $async
Whether the operation is part of a concurrent sub-batch of operation.
Definition FileOp.php:46
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:158
doPrecheck(FileStatePredicates $opPredicates, FileStatePredicates $batchPredicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:215
attempt()
Attempt the operation.
Definition FileOp.php:227
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:313
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:457
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:348
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:322
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:304
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:69
getBackend()
Get the backend this operation is for.
Definition FileOp.php:448
bool $failed
Whether the operation pre-check or attempt stage failed.
Definition FileOp.php:44
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