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 $noOp = false;
54
58 protected $destExists;
59
61 private const STATE_NEW = 1;
63 private const STATE_CHECKED = 2;
65 private const STATE_ATTEMPTED = 3;
66
75 final public function __construct(
76 FileBackendStore $backend, array $params, LoggerInterface $logger
77 ) {
78 $this->backend = $backend;
79 $this->logger = $logger;
80 [ $required, $optional, $paths ] = $this->allowedParams();
81 foreach ( $required as $name ) {
82 if ( isset( $params[$name] ) ) {
83 $this->params[$name] = $params[$name];
84 } else {
85 throw new InvalidArgumentException( "File operation missing parameter '$name'." );
86 }
87 }
88 foreach ( $optional as $name ) {
89 if ( isset( $params[$name] ) ) {
90 $this->params[$name] = $params[$name];
91 }
92 }
93 foreach ( $paths as $name ) {
94 if ( isset( $this->params[$name] ) ) {
95 // Normalize paths so the paths to the same file have the same string
96 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
97 }
98 }
99 }
100
107 protected static function normalizeIfValidStoragePath( $path ) {
110
111 return $res ?? $path;
112 }
113
114 return $path;
115 }
116
123 final public function getParam( $name ) {
124 return $this->params[$name] ?? null;
125 }
126
132 final public function failed() {
133 return $this->failed;
134 }
135
141 final public static function newDependencies() {
142 return [ 'read' => [], 'write' => [] ];
143 }
144
151 final public function applyDependencies( array $deps ) {
152 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
153 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
154
155 return $deps;
156 }
157
164 final public function dependsOn( array $deps ) {
165 foreach ( $this->storagePathsChanged() as $path ) {
166 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
167 return true; // "output" or "anti" dependency
168 }
169 }
170 foreach ( $this->storagePathsRead() as $path ) {
171 if ( isset( $deps['write'][$path] ) ) {
172 return true; // "flow" dependency
173 }
174 }
175
176 return false;
177 }
178
187 final public function precheck( FileStatePredicates $predicates ) {
188 if ( $this->state !== self::STATE_NEW ) {
189 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
190 }
191 $this->state = self::STATE_CHECKED;
192
193 $status = StatusValue::newGood();
194 foreach ( $this->storagePathsReadOrChanged() as $path ) {
195 if ( !$this->backend->isPathUsableInternal( $path ) ) {
196 $status->fatal( 'backend-fail-usable', $path );
197 }
198 }
199 if ( !$status->isOK() ) {
200 return $status;
201 }
202
203 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
204 $status = $this->doPrecheck( $opPredicates, $predicates );
205 if ( !$status->isOK() ) {
206 $this->failed = true;
207 }
208
209 return $status;
210 }
211
221 protected function doPrecheck(
222 FileStatePredicates $opPredicates,
223 FileStatePredicates $batchPredicates
224 ) {
225 return StatusValue::newGood();
226 }
227
233 final public function attempt() {
234 if ( $this->state !== self::STATE_CHECKED ) {
235 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
236 } elseif ( $this->failed ) { // failed precheck
237 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
238 }
239 $this->state = self::STATE_ATTEMPTED;
240 if ( $this->noOp ) {
241 $status = StatusValue::newGood(); // no-op
242 } else {
243 $status = $this->doAttempt();
244 if ( !$status->isOK() ) {
245 $this->failed = true;
246 $this->logFailure( 'attempt' );
247 }
248 }
249
250 return $status;
251 }
252
256 protected function doAttempt() {
257 return StatusValue::newGood();
258 }
259
265 final public function attemptAsync() {
266 $this->async = true;
267 $result = $this->attempt();
268 $this->async = false;
269
270 return $result;
271 }
272
278 final public function attemptQuick() {
279 $this->state = self::STATE_CHECKED; // bypassed
280
281 return $this->attempt();
282 }
283
289 final public function attemptAsyncQuick() {
290 $this->state = self::STATE_CHECKED; // bypassed
291
292 return $this->attemptAsync();
293 }
294
300 protected function allowedParams() {
301 return [ [], [], [] ];
302 }
303
310 protected function setFlags( array $params ) {
311 return [ 'async' => $this->async ] + $params;
312 }
313
319 public function storagePathsRead() {
320 return [];
321 }
322
328 public function storagePathsChanged() {
329 return [];
330 }
331
337 final public function storagePathsReadOrChanged() {
338 return array_values( array_unique(
339 array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
340 ) );
341 }
342
354 protected function precheckDestExistence(
355 FileStatePredicates $opPredicates,
356 $sourceSize,
357 $sourceSha1
358 ) {
359 $status = StatusValue::newGood();
360 // Record the existence of destination file
361 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
362 // Check if an incompatible file exists at the destination
363 $this->overwriteSameCase = false;
364 if ( $this->destExists ) {
365 if ( $this->getParam( 'overwrite' ) ) {
366 return $status; // OK, no conflict
367 } elseif ( $this->getParam( 'overwriteSame' ) ) {
368 // Operation does nothing other than return an OK or bad status
369 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
370 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
371 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
372 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
373 // Check if hashes are valid and match each other...
374 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
375 $status->fatal( 'backend-fail-hashes' );
376 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
377 $status->fatal( 'backend-fail-sizes' );
378 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
379 // Give an error if the files are not identical
380 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
381 } else {
382 $this->overwriteSameCase = true; // OK
383 }
384 } else {
385 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
386 }
387 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
388 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
389 }
390
391 return $status;
392 }
393
404 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
405 return $opPredicates->resolveFileExistence(
406 $source,
407 function ( $path ) {
408 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
409 }
410 );
411 }
412
424 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
425 return $opPredicates->resolveFileSize(
426 $source,
427 function ( $path ) {
428 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
429 }
430 );
431 }
432
440 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
441 return $opPredicates->resolveFileSha1Base36(
442 $source,
443 function ( $path ) {
444 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
445 }
446 );
447 }
448
454 public function getBackend() {
455 return $this->backend;
456 }
457
463 final public function logFailure( $action ) {
465 $params['failedAction'] = $action;
466 try {
467 $this->logger->error( static::class .
468 " failed: " . FormatJson::encode( $params ) );
469 } catch ( TimeoutException $e ) {
470 throw $e;
471 } catch ( Exception $e ) {
472 // bad config? debug log error?
473 }
474 }
475}
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
bool null $overwriteSameCase
Definition FileOp.php:56
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:107
bool null $destExists
Definition FileOp.php:58
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:141
resolveFileExistence( $source, FileStatePredicates $opPredicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:404
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:354
allowedParams()
Get the file operation parameters.
Definition FileOp.php:300
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:265
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:123
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:337
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:328
int $state
Stage in the operation life-cycle.
Definition FileOp.php:47
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:151
attempt()
Attempt the operation.
Definition FileOp.php:233
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:310
bool $async
Whether the operation is part of a concurrent sub-batch of operation.
Definition FileOp.php:51
doPrecheck(FileStatePredicates $opPredicates, FileStatePredicates $batchPredicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:221
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:463
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:319
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:278
LoggerInterface $logger
Definition FileOp.php:41
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:440
FileBackendStore $backend
Definition FileOp.php:39
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:164
getBackend()
Get the backend this operation is for.
Definition FileOp.php:454
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:289
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:132
resolveFileSize( $source, FileStatePredicates $opPredicates)
Get the size a file in storage will have when this operation is attempted.
Definition FileOp.php:424
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:75
doAttempt()
Definition FileOp.php:256
bool $noOp
Whether the operation pre-check stage marked the attempt stage as a no-op.
Definition FileOp.php:53
bool $failed
Whether the operation pre-check or attempt stage failed.
Definition FileOp.php:49
precheck(FileStatePredicates $predicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:187
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