MediaWiki master
FileOp.php
Go to the documentation of this file.
1<?php
25use Psr\Log\LoggerInterface;
27use Wikimedia\RequestTimeout\TimeoutException;
28
40abstract class FileOp {
42 protected $backend;
44 protected $logger;
45
47 protected $params = [];
48
50 protected $state = self::STATE_NEW;
52 protected $failed = false;
54 protected $async = false;
56 protected $noOp = false;
57
61 protected $destExists;
62
64 private const STATE_NEW = 1;
66 private const STATE_CHECKED = 2;
68 private const STATE_ATTEMPTED = 3;
69
78 final public function __construct(
79 FileBackendStore $backend, array $params, LoggerInterface $logger
80 ) {
81 $this->backend = $backend;
82 $this->logger = $logger;
83 [ $required, $optional, $paths ] = $this->allowedParams();
84 foreach ( $required as $name ) {
85 if ( isset( $params[$name] ) ) {
86 $this->params[$name] = $params[$name];
87 } else {
88 throw new InvalidArgumentException( "File operation missing parameter '$name'." );
89 }
90 }
91 foreach ( $optional as $name ) {
92 if ( isset( $params[$name] ) ) {
93 $this->params[$name] = $params[$name];
94 }
95 }
96 foreach ( $paths as $name ) {
97 if ( isset( $this->params[$name] ) ) {
98 // Normalize paths so the paths to the same file have the same string
99 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
100 }
101 }
102 }
103
110 protected static function normalizeIfValidStoragePath( $path ) {
111 if ( FileBackend::isStoragePath( $path ) ) {
112 $res = FileBackend::normalizeStoragePath( $path );
113
114 return $res ?? $path;
115 }
116
117 return $path;
118 }
119
126 final public function getParam( $name ) {
127 return $this->params[$name] ?? null;
128 }
129
135 final public function failed() {
136 return $this->failed;
137 }
138
144 final public static function newDependencies() {
145 return [ 'read' => [], 'write' => [] ];
146 }
147
154 final public function applyDependencies( array $deps ) {
155 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
156 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
157
158 return $deps;
159 }
160
167 final public function dependsOn( array $deps ) {
168 foreach ( $this->storagePathsChanged() as $path ) {
169 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
170 return true; // "output" or "anti" dependency
171 }
172 }
173 foreach ( $this->storagePathsRead() as $path ) {
174 if ( isset( $deps['write'][$path] ) ) {
175 return true; // "flow" dependency
176 }
177 }
178
179 return false;
180 }
181
190 final public function precheck( FileStatePredicates $predicates ) {
191 if ( $this->state !== self::STATE_NEW ) {
192 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
193 }
194 $this->state = self::STATE_CHECKED;
195
196 $status = StatusValue::newGood();
197 foreach ( $this->storagePathsReadOrChanged() as $path ) {
198 if ( !$this->backend->isPathUsableInternal( $path ) ) {
199 $status->fatal( 'backend-fail-usable', $path );
200 }
201 }
202 if ( !$status->isOK() ) {
203 return $status;
204 }
205
206 $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
207 $status = $this->doPrecheck( $opPredicates, $predicates );
208 if ( !$status->isOK() ) {
209 $this->failed = true;
210 }
211
212 return $status;
213 }
214
224 protected function doPrecheck(
225 FileStatePredicates $opPredicates,
226 FileStatePredicates $batchPredicates
227 ) {
228 return StatusValue::newGood();
229 }
230
236 final public function attempt() {
237 if ( $this->state !== self::STATE_CHECKED ) {
238 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
239 } elseif ( $this->failed ) { // failed precheck
240 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
241 }
242 $this->state = self::STATE_ATTEMPTED;
243 if ( $this->noOp ) {
244 $status = StatusValue::newGood(); // no-op
245 } else {
246 $status = $this->doAttempt();
247 if ( !$status->isOK() ) {
248 $this->failed = true;
249 $this->logFailure( 'attempt' );
250 }
251 }
252
253 return $status;
254 }
255
259 protected function doAttempt() {
260 return StatusValue::newGood();
261 }
262
268 final public function attemptAsync() {
269 $this->async = true;
270 $result = $this->attempt();
271 $this->async = false;
272
273 return $result;
274 }
275
281 final public function attemptQuick() {
282 $this->state = self::STATE_CHECKED; // bypassed
283
284 return $this->attempt();
285 }
286
292 final public function attemptAsyncQuick() {
293 $this->state = self::STATE_CHECKED; // bypassed
294
295 return $this->attemptAsync();
296 }
297
303 protected function allowedParams() {
304 return [ [], [], [] ];
305 }
306
313 protected function setFlags( array $params ) {
314 return [ 'async' => $this->async ] + $params;
315 }
316
322 public function storagePathsRead() {
323 return [];
324 }
325
331 public function storagePathsChanged() {
332 return [];
333 }
334
340 final public function storagePathsReadOrChanged() {
341 return array_values( array_unique(
342 array_merge( $this->storagePathsRead(), $this->storagePathsChanged() )
343 ) );
344 }
345
357 protected function precheckDestExistence(
358 FileStatePredicates $opPredicates,
359 $sourceSize,
360 $sourceSha1
361 ) {
362 $status = StatusValue::newGood();
363 // Record the existence of destination file
364 $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
365 // Check if an incompatible file exists at the destination
366 $this->overwriteSameCase = false;
367 if ( $this->destExists ) {
368 if ( $this->getParam( 'overwrite' ) ) {
369 return $status; // OK, no conflict
370 } elseif ( $this->getParam( 'overwriteSame' ) ) {
371 // Operation does nothing other than return an OK or bad status
372 $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
373 $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
374 $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
375 $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
376 // Check if hashes are valid and match each other...
377 if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
378 $status->fatal( 'backend-fail-hashes' );
379 } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
380 $status->fatal( 'backend-fail-sizes' );
381 } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
382 // Give an error if the files are not identical
383 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
384 } else {
385 $this->overwriteSameCase = true; // OK
386 }
387 } else {
388 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
389 }
390 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
391 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
392 }
393
394 return $status;
395 }
396
407 final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
408 return $opPredicates->resolveFileExistence(
409 $source,
410 function ( $path ) {
411 return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
412 }
413 );
414 }
415
427 final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
428 return $opPredicates->resolveFileSize(
429 $source,
430 function ( $path ) {
431 return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
432 }
433 );
434 }
435
443 final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
444 return $opPredicates->resolveFileSha1Base36(
445 $source,
446 function ( $path ) {
447 return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
448 }
449 );
450 }
451
457 public function getBackend() {
458 return $this->backend;
459 }
460
466 final public function logFailure( $action ) {
468 $params['failedAction'] = $action;
469 try {
470 $this->logger->error( static::class .
471 " failed: " . FormatJson::encode( $params ) );
472 } catch ( TimeoutException $e ) {
473 throw $e;
474 } catch ( Exception $e ) {
475 // bad config? debug log error?
476 }
477 }
478}
Base class for all backends using particular storage medium.
FileBackend helper class for representing operations.
Definition FileOp.php:40
array $params
Definition FileOp.php:47
bool null $overwriteSameCase
Definition FileOp.php:59
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:110
bool null $destExists
Definition FileOp.php:61
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:144
resolveFileExistence( $source, FileStatePredicates $opPredicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:407
precheckDestExistence(FileStatePredicates $opPredicates, $sourceSize, $sourceSha1)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:357
allowedParams()
Get the file operation parameters.
Definition FileOp.php:303
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:268
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:126
storagePathsReadOrChanged()
Get a list of storage paths read from or written to for this operation.
Definition FileOp.php:340
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:331
int $state
Stage in the operation life-cycle.
Definition FileOp.php:50
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:154
attempt()
Attempt the operation.
Definition FileOp.php:236
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:313
bool $async
Whether the operation is part of a concurrent sub-batch of operation.
Definition FileOp.php:54
doPrecheck(FileStatePredicates $opPredicates, FileStatePredicates $batchPredicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:224
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:466
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:322
attemptQuick()
Attempt the operation without regards to prechecks.
Definition FileOp.php:281
LoggerInterface $logger
Definition FileOp.php:44
resolveFileSha1Base36( $source, FileStatePredicates $opPredicates)
Get the SHA-1 of a file in storage when this operation is attempted.
Definition FileOp.php:443
FileBackendStore $backend
Definition FileOp.php:42
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:167
getBackend()
Get the backend this operation is for.
Definition FileOp.php:457
attemptAsyncQuick()
Attempt the operation in the background without regards to prechecks.
Definition FileOp.php:292
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:135
resolveFileSize( $source, FileStatePredicates $opPredicates)
Get the size a file in storage will have when this operation is attempted.
Definition FileOp.php:427
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:78
doAttempt()
Definition FileOp.php:259
bool $noOp
Whether the operation pre-check stage marked the attempt stage as a no-op.
Definition FileOp.php:56
bool $failed
Whether the operation pre-check or attempt stage failed.
Definition FileOp.php:52
precheck(FileStatePredicates $predicates)
Do a dry-run precondition check of the operation in the context of op batch.
Definition FileOp.php:190
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.
JSON formatter wrapper class.
Base class for all file backend classes (including multi-write backends).
$source