MediaWiki REL1_34
FileOp.php
Go to the documentation of this file.
1<?php
23use Psr\Log\LoggerInterface;
24
36abstract class FileOp {
38 protected $params = [];
39
41 protected $backend;
43 protected $logger;
44
47
49 protected $failed = false;
50
52 protected $async = false;
53
55 protected $batchId;
56
58 protected $doOperation = true;
59
61 protected $sourceSha1;
62
65
67 protected $destExists;
68
69 /* Object life-cycle */
70 const STATE_NEW = 1;
71 const STATE_CHECKED = 2;
72 const STATE_ATTEMPTED = 3;
73
82 final public function __construct(
83 FileBackendStore $backend, array $params, LoggerInterface $logger
84 ) {
85 $this->backend = $backend;
86 $this->logger = $logger;
87 list( $required, $optional, $paths ) = $this->allowedParams();
88 foreach ( $required as $name ) {
89 if ( isset( $params[$name] ) ) {
90 $this->params[$name] = $params[$name];
91 } else {
92 throw new InvalidArgumentException( "File operation missing parameter '$name'." );
93 }
94 }
95 foreach ( $optional as $name ) {
96 if ( isset( $params[$name] ) ) {
97 $this->params[$name] = $params[$name];
98 }
99 }
100 foreach ( $paths as $name ) {
101 if ( isset( $this->params[$name] ) ) {
102 // Normalize paths so the paths to the same file have the same string
103 $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
104 }
105 }
106 }
107
114 protected static function normalizeIfValidStoragePath( $path ) {
117
118 return $res ?? $path;
119 }
120
121 return $path;
122 }
123
129 final public function setBatchId( $batchId ) {
130 $this->batchId = $batchId;
131 }
132
139 final public function getParam( $name ) {
140 return $this->params[$name] ?? null;
141 }
142
148 final public function failed() {
149 return $this->failed;
150 }
151
157 final public static function newPredicates() {
158 return [ 'exists' => [], 'sha1' => [] ];
159 }
160
166 final public static function newDependencies() {
167 return [ 'read' => [], 'write' => [] ];
168 }
169
176 final public function applyDependencies( array $deps ) {
177 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
178 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
179
180 return $deps;
181 }
182
189 final public function dependsOn( array $deps ) {
190 foreach ( $this->storagePathsChanged() as $path ) {
191 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
192 return true; // "output" or "anti" dependency
193 }
194 }
195 foreach ( $this->storagePathsRead() as $path ) {
196 if ( isset( $deps['write'][$path] ) ) {
197 return true; // "flow" dependency
198 }
199 }
200
201 return false;
202 }
203
211 final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
212 if ( !$this->doOperation ) {
213 return []; // this is a no-op
214 }
215 $nullEntries = [];
216 $updateEntries = [];
217 $deleteEntries = [];
218 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
219 foreach ( array_unique( $pathsUsed ) as $path ) {
220 $nullEntries[] = [ // assertion for recovery
221 'op' => 'null',
222 'path' => $path,
223 'newSha1' => $this->fileSha1( $path, $oPredicates )
224 ];
225 }
226 foreach ( $this->storagePathsChanged() as $path ) {
227 if ( $nPredicates['sha1'][$path] === false ) { // deleted
228 $deleteEntries[] = [
229 'op' => 'delete',
230 'path' => $path,
231 'newSha1' => ''
232 ];
233 } else { // created/updated
234 $updateEntries[] = [
235 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
236 'path' => $path,
237 'newSha1' => $nPredicates['sha1'][$path]
238 ];
239 }
240 }
241
242 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
243 }
244
253 final public function precheck( array &$predicates ) {
254 if ( $this->state !== self::STATE_NEW ) {
255 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
256 }
257 $this->state = self::STATE_CHECKED;
258
259 $status = StatusValue::newGood();
260 $storagePaths = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
261 foreach ( array_unique( $storagePaths ) as $storagePath ) {
262 if ( !$this->backend->isPathUsableInternal( $storagePath ) ) {
263 $status->fatal( 'backend-fail-usable', $storagePath );
264 }
265 }
266 if ( !$status->isOK() ) {
267 return $status;
268 }
269
270 $status = $this->doPrecheck( $predicates );
271 if ( !$status->isOK() ) {
272 $this->failed = true;
273 }
274
275 return $status;
276 }
277
282 protected function doPrecheck( array &$predicates ) {
283 return StatusValue::newGood();
284 }
285
291 final public function attempt() {
292 if ( $this->state !== self::STATE_CHECKED ) {
293 return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
294 } elseif ( $this->failed ) { // failed precheck
295 return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
296 }
297 $this->state = self::STATE_ATTEMPTED;
298 if ( $this->doOperation ) {
299 $status = $this->doAttempt();
300 if ( !$status->isOK() ) {
301 $this->failed = true;
302 $this->logFailure( 'attempt' );
303 }
304 } else { // no-op
305 $status = StatusValue::newGood();
306 }
307
308 return $status;
309 }
310
314 protected function doAttempt() {
315 return StatusValue::newGood();
316 }
317
323 final public function attemptAsync() {
324 $this->async = true;
325 $result = $this->attempt();
326 $this->async = false;
327
328 return $result;
329 }
330
336 protected function allowedParams() {
337 return [ [], [], [] ];
338 }
339
346 protected function setFlags( array $params ) {
347 return [ 'async' => $this->async ] + $params;
348 }
349
355 public function storagePathsRead() {
356 return [];
357 }
358
364 public function storagePathsChanged() {
365 return [];
366 }
367
376 protected function precheckDestExistence( array $predicates ) {
377 $status = StatusValue::newGood();
378 // Get hash of source file/string and the destination file
379 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
380 if ( $this->sourceSha1 === null ) { // file in storage?
381 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
382 }
383 $this->overwriteSameCase = false;
384 $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
385 if ( $this->destExists ) {
386 if ( $this->getParam( 'overwrite' ) ) {
387 return $status; // OK
388 } elseif ( $this->getParam( 'overwriteSame' ) ) {
389 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
390 // Check if hashes are valid and match each other...
391 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
392 $status->fatal( 'backend-fail-hashes' );
393 } elseif ( $this->sourceSha1 !== $dhash ) {
394 // Give an error if the files are not identical
395 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
396 } else {
397 $this->overwriteSameCase = true; // OK
398 }
399
400 return $status; // do nothing; either OK or bad status
401 } else {
402 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
403
404 return $status;
405 }
406 } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
407 $status->fatal( 'backend-fail-stat', $this->params['dst'] );
408 }
409
410 return $status;
411 }
412
419 protected function getSourceSha1Base36() {
420 return null; // N/A
421 }
422
433 final protected function fileExists( $source, array $predicates ) {
434 if ( isset( $predicates['exists'][$source] ) ) {
435 return $predicates['exists'][$source]; // previous op assures this
436 } else {
437 $params = [ 'src' => $source, 'latest' => true ];
438
439 return $this->backend->fileExists( $params );
440 }
441 }
442
453 final protected function fileSha1( $source, array $predicates ) {
454 if ( isset( $predicates['sha1'][$source] ) ) {
455 return $predicates['sha1'][$source]; // previous op assures this
456 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
457 return false; // previous op assures this
458 } else {
459 $params = [ 'src' => $source, 'latest' => true ];
460
461 return $this->backend->getFileSha1Base36( $params );
462 }
463 }
464
470 public function getBackend() {
471 return $this->backend;
472 }
473
479 final public function logFailure( $action ) {
481 $params['failedAction'] = $action;
482 try {
483 $this->logger->error( static::class .
484 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
485 } catch ( Exception $e ) {
486 // bad config? debug log error?
487 }
488 }
489}
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:36
setBatchId( $batchId)
Set the batch UUID this operation belongs to.
Definition FileOp.php:129
array $params
Definition FileOp.php:38
string $sourceSha1
Definition FileOp.php:61
static normalizeIfValidStoragePath( $path)
Normalize a string if it is a valid storage path.
Definition FileOp.php:114
static newDependencies()
Get a new empty dependency tracking array for paths read/written to.
Definition FileOp.php:166
fileSha1( $source, array $predicates)
Get the SHA-1 hash a file in storage will have when this operation is attempted.
Definition FileOp.php:453
static newPredicates()
Get a new empty predicates array for precheck()
Definition FileOp.php:157
fileExists( $source, array $predicates)
Check if a file will exist in storage when this operation is attempted.
Definition FileOp.php:433
allowedParams()
Get the file operation parameters.
Definition FileOp.php:336
bool $doOperation
Operation is not a no-op.
Definition FileOp.php:58
attemptAsync()
Attempt the operation in the background.
Definition FileOp.php:323
doPrecheck(array &$predicates)
Definition FileOp.php:282
getParam( $name)
Get the value of the parameter with the given name.
Definition FileOp.php:139
const STATE_NEW
Definition FileOp.php:70
bool $overwriteSameCase
Definition FileOp.php:64
const STATE_ATTEMPTED
Definition FileOp.php:72
storagePathsChanged()
Get a list of storage paths written to for this operation.
Definition FileOp.php:364
int $state
Definition FileOp.php:46
applyDependencies(array $deps)
Update a dependency tracking array to account for this operation.
Definition FileOp.php:176
attempt()
Attempt the operation.
Definition FileOp.php:291
precheck(array &$predicates)
Check preconditions of the operation without writing anything.
Definition FileOp.php:253
setFlags(array $params)
Adjust params to FileBackendStore internal file calls.
Definition FileOp.php:346
bool $async
Definition FileOp.php:52
string $batchId
Definition FileOp.php:55
bool $destExists
Definition FileOp.php:67
getJournalEntries(array $oPredicates, array $nPredicates)
Get the file journal entries for this file operation.
Definition FileOp.php:211
logFailure( $action)
Log a file operation failure and preserve any temp files.
Definition FileOp.php:479
storagePathsRead()
Get a list of storage paths read from for this operation.
Definition FileOp.php:355
getSourceSha1Base36()
precheckDestExistence() helper function to get the source file SHA-1.
Definition FileOp.php:419
LoggerInterface $logger
Definition FileOp.php:43
FileBackendStore $backend
Definition FileOp.php:41
dependsOn(array $deps)
Check if this operation changes files listed in $paths.
Definition FileOp.php:189
getBackend()
Get the backend this operation is for.
Definition FileOp.php:470
const STATE_CHECKED
Definition FileOp.php:71
failed()
Check if this operation failed precheck() or attempt()
Definition FileOp.php:148
__construct(FileBackendStore $backend, array $params, LoggerInterface $logger)
Build a new batch file operation transaction.
Definition FileOp.php:82
doAttempt()
Definition FileOp.php:314
precheckDestExistence(array $predicates)
Check for errors with regards to the destination file already existing.
Definition FileOp.php:376
bool $failed
Definition FileOp.php:49
$source
return true
Definition router.php:94