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 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
188 $status = $this->doPrecheck( $opPredicates, $predicates );
189 if ( !$status->isOK() ) {
190 $this->failed = true;
191 }
192
193 return $status;
194 }
195
205 protected function doPrecheck(
206 FileStatePredicates $opPredicates,
207 FileStatePredicates $batchPredicates
208 ) {
209 return StatusValue::newGood();
210 }
211
217 final public function attempt() {
218 if ( $this->state !== self::STATE_CHECKED ) {
219 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
220 } elseif ( $this->failed ) { // failed precheck
221 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
222 }
223 $this->state = self::STATE_ATTEMPTED;
224 if ( $this->noOp ) {
225 $status = StatusValue::newGood(); // no-op
226 } else {
227 $status = $this->doAttempt();
228 if ( !$status->isOK() ) {
229 $this->failed = true;
230 $this->logFailure( 'attempt' );
231 }
232 }
233
234 return $status;
235 }
236
240 protected function doAttempt() {
241 return StatusValue::newGood();
242 }
243
249 final public function attemptAsync() {
250 $this->async = true;
251 $result = $this->attempt();
252 $this->async = false;
253
254 return $result;
255 }
256
262 final public function attemptQuick() {
263 $this->state = self::STATE_CHECKED; // bypassed
264
265 return $this->attempt();
266 }
267
273 final public function attemptAsyncQuick() {
274 $this->state = self::STATE_CHECKED; // bypassed
275
276 return $this->attemptAsync();
277 }
278
284 protected function allowedParams() {
285 return [ [], [], [] ];
286 }
287
294 protected function setFlags( array $params ) {
295 return [ 'async' => $this->async ] + $params;
296 }
297
303 public function storagePathsRead() {
304 return [];
305 }
306
312 public function storagePathsChanged() {
313 return [];
314 }
315
321 final public function storagePathsReadOrChanged() {
322 return array_values( array_unique(
323 array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
324 ) );
325 }
326
338 protected function precheckDestExistence(
339 FileStatePredicates $opPredicates,
340 $sourceSize,
341 $sourceSha1
342 ) {
343 $status = StatusValue::newGood();
344 // Record the existence of destination file
345 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
346 // Check if an incompatible file exists at the destination
347 $this->overwriteSameCase = false;
348 if ( $this->destExists ) {
349 if ( $this->getParam( 'overwrite' ) ) {
350 return $status; // OK, no conflict
351 } elseif ( $this->getParam( 'overwriteSame' ) ) {
352 // Operation does nothing other than return an OK or bad status
353 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
354 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
355 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
356 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
357 // Check if hashes are valid and match each other...
358 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
359 $status->fatal( 'backend-fail-hashes' );
360 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
361 $status->fatal( 'backend-fail-sizes' );
362 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
363 // Give an error if the files are not identical
364 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
365 } else {
366 $this->overwriteSameCase = true; // OK
367 }
368 } else {
369 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
370 }
371 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
372 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
373 }
374
375 return $status;
376 }
377
388 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
389 return $opPredicates->resolveFileExistence(
390 $source,
391 function ( $path ) {
392 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
393 }
394 );
395 }
396
408 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
409 return $opPredicates->resolveFileSize(
410 $source,
411 function ( $path ) {
412 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
413 }
414 );
415 }
416
424 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
425 return $opPredicates->resolveFileSha1Base36(
426 $source,
427 function ( $path ) {
428 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
429 }
430 );
431 }
432
438 public function getBackend() {
439 return $this->backend;
440 }
441
447 final public function logFailure( $action ) {
448 try {
449 $this->logger->error( static::class . ' failed: ' . $action,
450 [ 'params' => $this->params ]
451 );
452 } catch ( TimeoutException $e ) {
453 throw $e;
454 } catch ( Exception ) {
455 // bad config? debug log error?
456 }
457 }
458}
459
461class_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:408
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:284
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:321
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:424
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:388
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:262
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:273
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:249
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:205
attempt()
Attempt the operation.
Definition FileOp.php:217
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:303
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:447
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:338
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:312
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:294
__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:438
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 existence a file given predicated and current state of files.
$source