aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--autoload.php1
-rw-r--r--includes/libs/filebackend/FileOpBatch.php2
-rw-r--r--includes/libs/filebackend/fileop/CopyFileOp.php29
-rw-r--r--includes/libs/filebackend/fileop/CreateFileOp.php19
-rw-r--r--includes/libs/filebackend/fileop/DeleteFileOp.php15
-rw-r--r--includes/libs/filebackend/fileop/DescribeFileOp.php23
-rw-r--r--includes/libs/filebackend/fileop/FileOp.php172
-rw-r--r--includes/libs/filebackend/fileop/FileStatePredicates.php142
-rw-r--r--includes/libs/filebackend/fileop/MoveFileOp.php33
-rw-r--r--includes/libs/filebackend/fileop/StoreFileOp.php25
-rw-r--r--tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php8
11 files changed, 303 insertions, 166 deletions
diff --git a/autoload.php b/autoload.php
index 378f6c5b5672..a6a5504f8af1 100644
--- a/autoload.php
+++ b/autoload.php
@@ -494,6 +494,7 @@ $wgAutoloadLocalClasses = [
'FileOpBatch' => __DIR__ . '/includes/libs/filebackend/FileOpBatch.php',
'FileOpPerfTest' => __DIR__ . '/maintenance/fileOpPerfTest.php',
'FileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
+ 'FileStatePredicates' => __DIR__ . '/includes/libs/filebackend/fileop/FileStatePredicates.php',
'FindBadBlobs' => __DIR__ . '/maintenance/findBadBlobs.php',
'FindClasses' => __DIR__ . '/maintenance/findClasses.php',
'FindDeprecated' => __DIR__ . '/maintenance/findDeprecated.php',
diff --git a/includes/libs/filebackend/FileOpBatch.php b/includes/libs/filebackend/FileOpBatch.php
index d28cbdfc5359..bcabf47099d7 100644
--- a/includes/libs/filebackend/FileOpBatch.php
+++ b/includes/libs/filebackend/FileOpBatch.php
@@ -64,7 +64,7 @@ class FileOpBatch {
$ignoreErrors = !empty( $opts['force'] );
$maxConcurrency = $opts['concurrency'] ?? 1;
- $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
+ $predicates = new FileStatePredicates(); // account for previous ops in prechecks
$curBatch = []; // concurrent FileOp sub-batch accumulation
$curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
$pPerformOps = []; // ordered list of concurrent FileOp sub-batches
diff --git a/includes/libs/filebackend/fileop/CopyFileOp.php b/includes/libs/filebackend/fileop/CopyFileOp.php
index 779fece9490a..cee3e7f7ea18 100644
--- a/includes/libs/filebackend/fileop/CopyFileOp.php
+++ b/includes/libs/filebackend/fileop/CopyFileOp.php
@@ -34,18 +34,19 @@ class CopyFileOp extends FileOp {
];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
// Check source file existence
- $srcExists = $this->fileExists( $this->params['src'], $predicates );
+ $srcExists = $this->resolveFileExistence( $this->params['src'], $opPredicates );
if ( $srcExists === false ) {
if ( $this->getParam( 'ignoreMissingSource' ) ) {
$this->cancelled = true; // no-op
// Update file existence predicates (cache 404s)
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = false;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] = false;
- $predicates[self::ASSUMED_SHA1][$this->params['src']] = false;
+ $batchPredicates->assumeFileDoesNotExist( $this->params['src'] );
return $status; // nothing to do
} else {
@@ -58,15 +59,23 @@ class CopyFileOp extends FileOp {
return $status;
}
- // Check if destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
+ // Check if an incompatible destination file exists
+ $srcSize = function () use ( $opPredicates ) {
+ static $size = null;
+ $size ??= $this->resolveFileSize( $this->params['src'], $opPredicates );
+ return $size;
+ };
+ $srcSha1 = function () use ( $opPredicates ) {
+ static $sha1 = null;
+ $sha1 ??= $this->resolveFileSha1Base36( $this->params['src'], $opPredicates );
+ return $sha1;
+ };
+ $status->merge( $this->precheckDestExistence( $opPredicates, $srcSize, $srcSha1 ) );
$this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
// Update file existence predicates if the operation is expected to be allowed to run
if ( $status->isOK() ) {
- $predicates[self::ASSUMED_EXISTS][$this->params['dst']] = true;
- $predicates[self::ASSUMED_SIZE][$this->params['dst']] = $this->sourceSize;
- $predicates[self::ASSUMED_SHA1][$this->params['dst']] = $this->sourceSha1;
+ $batchPredicates->assumeFileExists( $this->params['dst'], $srcSize, $srcSha1 );
}
return $status; // safe to call attempt()
diff --git a/includes/libs/filebackend/fileop/CreateFileOp.php b/includes/libs/filebackend/fileop/CreateFileOp.php
index ef825c6318f6..d1897a13a915 100644
--- a/includes/libs/filebackend/fileop/CreateFileOp.php
+++ b/includes/libs/filebackend/fileop/CreateFileOp.php
@@ -32,25 +32,28 @@ class CreateFileOp extends FileOp {
];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
// Check if the source data is too big
- $maxBytes = $this->backend->maxFileSizeInternal();
- if ( strlen( $this->getParam( 'content' ) ) > $maxBytes ) {
- $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
+ $sourceSize = $this->getSourceSize();
+ $maxFileSize = $this->backend->maxFileSizeInternal();
+ if ( $sourceSize > $maxFileSize ) {
+ $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxFileSize );
return $status;
}
// Check if an incompatible destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
+ $sourceSha1 = $this->getSourceSha1Base36();
+ $status->merge( $this->precheckDestExistence( $opPredicates, $sourceSize, $sourceSha1 ) );
$this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
// Update file existence predicates if the operation is expected to be allowed to run
if ( $status->isOK() ) {
- $predicates[self::ASSUMED_EXISTS][$this->params['dst']] = true;
- $predicates[self::ASSUMED_SIZE][$this->params['dst']] = $this->sourceSize;
- $predicates[self::ASSUMED_SHA1][$this->params['dst']] = $this->sourceSha1;
+ $batchPredicates->assumeFileExists( $this->params['dst'], $sourceSize, $sourceSha1 );
}
return $status; // safe to call attempt()
diff --git a/includes/libs/filebackend/fileop/DeleteFileOp.php b/includes/libs/filebackend/fileop/DeleteFileOp.php
index 251b7bbc7419..7c7c39f850e0 100644
--- a/includes/libs/filebackend/fileop/DeleteFileOp.php
+++ b/includes/libs/filebackend/fileop/DeleteFileOp.php
@@ -30,18 +30,19 @@ class DeleteFileOp extends FileOp {
return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
// Check source file existence
- $srcExists = $this->fileExists( $this->params['src'], $predicates );
+ $srcExists = $this->resolveFileExistence( $this->params['src'], $opPredicates );
if ( $srcExists === false ) {
if ( $this->getParam( 'ignoreMissingSource' ) ) {
$this->cancelled = true; // no-op
// Update file existence predicates (cache 404s)
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = false;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] = false;
- $predicates[self::ASSUMED_SHA1][$this->params['src']] = false;
+ $batchPredicates->assumeFileDoesNotExist( $this->params['src'] );
return $status; // nothing to do
} else {
@@ -56,9 +57,7 @@ class DeleteFileOp extends FileOp {
}
// Update file existence predicates since the operation is expected to be allowed to run
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = false;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] = false;
- $predicates[self::ASSUMED_SHA1][$this->params['src']] = false;
+ $batchPredicates->assumeFileDoesNotExist( $this->params['src'] );
return $status; // safe to call attempt()
}
diff --git a/includes/libs/filebackend/fileop/DescribeFileOp.php b/includes/libs/filebackend/fileop/DescribeFileOp.php
index 029c45132a64..70411963e416 100644
--- a/includes/libs/filebackend/fileop/DescribeFileOp.php
+++ b/includes/libs/filebackend/fileop/DescribeFileOp.php
@@ -30,11 +30,14 @@ class DescribeFileOp extends FileOp {
return [ [ 'src' ], [ 'headers' ], [ 'src' ] ];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
// Check source file existence
- $srcExists = $this->fileExists( $this->params['src'], $predicates );
+ $srcExists = $this->resolveFileExistence( $this->params['src'], $opPredicates );
if ( $srcExists === false ) {
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
@@ -46,11 +49,17 @@ class DescribeFileOp extends FileOp {
}
// Update file existence predicates since the operation is expected to be allowed to run
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = $srcExists;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] =
- $this->fileSize( $this->params['src'], $predicates );
- $predicates[self::ASSUMED_SHA1][$this->params['src']] =
- $this->fileSha1( $this->params['src'], $predicates );
+ $srcSize = function () use ( $opPredicates ) {
+ static $size = null;
+ $size ??= $this->resolveFileSize( $this->params['src'], $opPredicates );
+ return $size;
+ };
+ $srcSha1 = function () use ( $opPredicates ) {
+ static $sha1 = null;
+ $sha1 ??= $this->resolveFileSha1Base36( $this->params['src'], $opPredicates );
+ return $sha1;
+ };
+ $batchPredicates->assumeFileExists( $this->params['src'], $srcSize, $srcSha1 );
return $status; // safe to call attempt()
}
diff --git a/includes/libs/filebackend/fileop/FileOp.php b/includes/libs/filebackend/fileop/FileOp.php
index 68b4d276c0c7..3bfd362e5e91 100644
--- a/includes/libs/filebackend/fileop/FileOp.php
+++ b/includes/libs/filebackend/fileop/FileOp.php
@@ -52,15 +52,9 @@ abstract class FileOp {
/** @var bool */
protected $cancelled = false;
- /** @var int|bool */
- protected $sourceSize;
- /** @var string|bool */
- protected $sourceSha1;
-
- /** @var bool */
+ /** @var bool|null */
protected $overwriteSameCase;
-
- /** @var bool */
+ /** @var bool|null */
protected $destExists;
/* Object life-cycle */
@@ -68,10 +62,6 @@ abstract class FileOp {
private const STATE_CHECKED = 2;
private const STATE_ATTEMPTED = 3;
- protected const ASSUMED_SHA1 = 'sha1';
- protected const ASSUMED_EXISTS = 'exists';
- protected const ASSUMED_SIZE = 'size';
-
/**
* Build a new batch file operation transaction
*
@@ -142,15 +132,6 @@ abstract class FileOp {
}
/**
- * Get a new empty predicates array for precheck()
- *
- * @return array
- */
- final public static function newPredicates() {
- return [ self::ASSUMED_EXISTS => [], self::ASSUMED_SHA1 => [], self::ASSUMED_SIZE => [] ];
- }
-
- /**
* Get a new empty dependency tracking array for paths read/written to
*
* @return array
@@ -162,7 +143,7 @@ abstract class FileOp {
/**
* Update a dependency tracking array to account for this operation
*
- * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
+ * @param array $deps Prior path reads/writes; format of FileOp::newDependencies()
* @return array
*/
final public function applyDependencies( array $deps ) {
@@ -175,7 +156,7 @@ abstract class FileOp {
/**
* Check if this operation changes files listed in $paths
*
- * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
+ * @param array $deps Prior path reads/writes; format of FileOp::newDependencies()
* @return bool
*/
final public function dependsOn( array $deps ) {
@@ -194,14 +175,14 @@ abstract class FileOp {
}
/**
- * Check preconditions of the operation without writing anything.
- * This must update $predicates for each path that the op can change
- * except when a failing StatusValue object is returned.
+ * Do a dry-run precondition check of the operation in the context of op batch
+ *
+ * Updates the batch predicates for all paths this op can change if an OK status is returned
*
- * @param array &$predicates
+ * @param FileStatePredicates $predicates Counterfactual file states for the op batch
* @return StatusValue
*/
- final public function precheck( array &$predicates ) {
+ final public function precheck( FileStatePredicates $predicates ) {
if ( $this->state !== self::STATE_NEW ) {
return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
}
@@ -217,7 +198,8 @@ abstract class FileOp {
return $status;
}
- $status = $this->doPrecheck( $predicates );
+ $opPredicates = $predicates->snapshot( $this->storagePathsReadOrChanged() );
+ $status = $this->doPrecheck( $opPredicates, $predicates );
if ( !$status->isOK() ) {
$this->failed = true;
}
@@ -226,10 +208,18 @@ abstract class FileOp {
}
/**
- * @param array &$predicates
+ * Do a dry-run precondition check of the operation in the context of op batch
+ *
+ * Updates the batch predicates for all paths this op can change if an OK status is returned
+ *
+ * @param FileStatePredicates $opPredicates Counterfactual file states for op paths at op start
+ * @param FileStatePredicates $batchPredicates Counterfactual file states for the op batch
* @return StatusValue
*/
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
return StatusValue::newGood();
}
@@ -349,25 +339,24 @@ abstract class FileOp {
}
/**
- * Check for errors with regards to the destination file already existing.
- * Also set destExists, overwriteSameCase, sourceSize, and sourceSha1 member variables.
+ * Check for errors with regards to the destination file already existing
+ *
+ * Also set the destExists and overwriteSameCase member variables.
* A bad StatusValue will be returned if there is no chance it can be overwritten.
*
- * @param array $predicates
+ * @param FileStatePredicates $opPredicates Counterfactual storage path states for this op
+ * @param int|false|Closure $sourceSize Source size or idempotent function yielding the size
+ * @param string|Closure $sourceSha1 Source hash, or, idempotent function yielding the hash
* @return StatusValue
*/
- protected function precheckDestExistence( array $predicates ) {
+ protected function precheckDestExistence(
+ FileStatePredicates $opPredicates,
+ $sourceSize,
+ $sourceSha1
+ ) {
$status = StatusValue::newGood();
- // Record the size of source file/string
- $this->sourceSize = $this->getSourceSize(); // FS file or data string
- // file in storage?
- $this->sourceSize ??= $this->fileSize( $this->params['src'], $predicates );
- // Record the hash of source file/string
- $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
- // file in storage?
- $this->sourceSha1 ??= $this->fileSha1( $this->params['src'], $predicates );
// Record the existence of destination file
- $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
+ $this->destExists = $this->resolveFileExistence( $this->params['dst'], $opPredicates );
// Check if an incompatible file exists at the destination
$this->overwriteSameCase = false;
if ( $this->destExists ) {
@@ -375,14 +364,16 @@ abstract class FileOp {
return $status; // OK, no conflict
} elseif ( $this->getParam( 'overwriteSame' ) ) {
// Operation does nothing other than return an OK or bad status
- $dhash = $this->fileSha1( $this->params['dst'], $predicates );
- $dsize = $this->fileSize( $this->params['dst'], $predicates );
+ $sourceSize = ( $sourceSize instanceof Closure ) ? $sourceSize() : $sourceSize;
+ $sourceSha1 = ( $sourceSha1 instanceof Closure ) ? $sourceSha1() : $sourceSha1;
+ $dstSha1 = $this->resolveFileSha1Base36( $this->params['dst'], $opPredicates );
+ $dstSize = $this->resolveFileSize( $this->params['dst'], $opPredicates );
// Check if hashes are valid and match each other...
- if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
+ if ( !strlen( $sourceSha1 ) || !strlen( $dstSha1 ) ) {
$status->fatal( 'backend-fail-hashes' );
- } elseif ( !is_int( $this->sourceSize ) || !is_int( $dsize ) ) {
+ } elseif ( !is_int( $sourceSize ) || !is_int( $dstSize ) ) {
$status->fatal( 'backend-fail-sizes' );
- } elseif ( $this->sourceSha1 !== $dhash || $this->sourceSize !== $dsize ) {
+ } elseif ( $sourceSha1 !== $dstSha1 || $sourceSize !== $dstSize ) {
// Give an error if the files are not identical
$status->fatal( 'backend-fail-notsame', $this->params['dst'] );
} else {
@@ -399,43 +390,22 @@ abstract class FileOp {
}
/**
- * precheckDestExistence() helper function to get the source file size.
- * Subclasses should overwrite this if the source is not in storage.
- *
- * @return int|false|null Returns false on failure
- */
- protected function getSourceSize() {
- return null; // N/A
- }
-
- /**
- * precheckDestExistence() helper function to get the source file SHA-1.
- * Subclasses should overwrite this if the source is not in storage.
- *
- * @return string|false|null Returns false on failure
- */
- protected function getSourceSha1Base36() {
- return null; // N/A
- }
-
- /**
* Check if a file will exist in storage when this operation is attempted
*
* Ideally, the file stat entry should already be preloaded via preloadFileStat().
* Otherwise, this will query the backend.
*
* @param string $source Storage path
- * @param array $predicates
+ * @param FileStatePredicates $opPredicates Counterfactual storage path states for this op
* @return bool|null Whether the file will exist or null on error
*/
- final protected function fileExists( $source, array $predicates ) {
- if ( isset( $predicates[self::ASSUMED_EXISTS][$source] ) ) {
- return $predicates[self::ASSUMED_EXISTS][$source]; // previous op assures this
- } else {
- $params = [ 'src' => $source, 'latest' => true ];
-
- return $this->backend->fileExists( $params );
- }
+ final protected function resolveFileExistence( $source, FileStatePredicates $opPredicates ) {
+ return $opPredicates->resolveFileExistence(
+ $source,
+ function ( $path ) {
+ return $this->backend->fileExists( [ 'src' => $path, 'latest' => true ] );
+ }
+ );
}
/**
@@ -446,44 +416,32 @@ abstract class FileOp {
* Get the size of a file in storage when this operation is attempted
*
* @param string $source Storage path
- * @param array $predicates
+ * @param FileStatePredicates $opPredicates Counterfactual storage path states for this op
* @return int|false False on failure
*/
- final protected function fileSize( $source, array $predicates ) {
- if ( isset( $predicates[self::ASSUMED_SIZE][$source] ) ) {
- return $predicates[self::ASSUMED_SIZE][$source]; // previous op assures this
- } elseif (
- isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
- !$predicates[self::ASSUMED_EXISTS][$source]
- ) {
- return false; // previous op assures this
- } else {
- $params = [ 'src' => $source, 'latest' => true ];
-
- return $this->backend->getFileSize( $params );
- }
+ final protected function resolveFileSize( $source, FileStatePredicates $opPredicates ) {
+ return $opPredicates->resolveFileSize(
+ $source,
+ function ( $path ) {
+ return $this->backend->getFileSize( [ 'src' => $path, 'latest' => true ] );
+ }
+ );
}
/**
* Get the SHA-1 of a file in storage when this operation is attempted
*
* @param string $source Storage path
- * @param array $predicates
- * @return string|bool The SHA-1 hash the file will have or false if non-existent or on error
+ * @param FileStatePredicates $opPredicates Counterfactual storage path states for this op
+ * @return string|false The SHA-1 hash the file will have or false if non-existent or on error
*/
- final protected function fileSha1( $source, array $predicates ) {
- if ( isset( $predicates[self::ASSUMED_SHA1][$source] ) ) {
- return $predicates[self::ASSUMED_SHA1][$source]; // previous op assures this
- } elseif (
- isset( $predicates[self::ASSUMED_EXISTS][$source] ) &&
- !$predicates[self::ASSUMED_EXISTS][$source]
- ) {
- return false; // previous op assures this
- } else {
- $params = [ 'src' => $source, 'latest' => true ];
-
- return $this->backend->getFileSha1Base36( $params );
- }
+ final protected function resolveFileSha1Base36( $source, FileStatePredicates $opPredicates ) {
+ return $opPredicates->resolveFileSha1Base36(
+ $source,
+ function ( $path ) {
+ return $this->backend->getFileSha1Base36( [ 'src' => $path, 'latest' => true ] );
+ }
+ );
}
/**
diff --git a/includes/libs/filebackend/fileop/FileStatePredicates.php b/includes/libs/filebackend/fileop/FileStatePredicates.php
new file mode 100644
index 000000000000..a9a592751a28
--- /dev/null
+++ b/includes/libs/filebackend/fileop/FileStatePredicates.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup FileBackend
+ */
+
+/**
+ * Helper class for tracking counterfactual file states when pre-checking file operation batches
+ *
+ * The file states are represented with (existence,size,sha1) triples. When combined with the
+ * current state of files in the backend, this can be used to simulate how a batch of operations
+ * would play out as a "dry run". This is used in FileBackend::doOperations() to bail out if any
+ * failure can be predicted before modifying any data. This includes file operation batches where
+ * the same file gets modified by different operations within the batch.
+ *
+ * @internal Only for use within FileBackend
+ */
+class FileStatePredicates {
+ protected const EXISTS = 'exists';
+ protected const SIZE = 'size';
+ protected const SHA1 = 'sha1';
+
+ /** @var array<string,array> Map of (storage path => file state map) */
+ private $fileStateByPath;
+
+ public function __construct() {
+ $this->fileStateByPath = [];
+ }
+
+ /**
+ * Predicate that a file exists at the path
+ *
+ * @param string $path Storage path
+ * @param int|false|Closure $size File size or idempotent function yielding the size
+ * @param string|Closure $sha1Base36 File hash, or, idempotent function yielding the hash
+ */
+ public function assumeFileExists( string $path, $size, $sha1Base36 ) {
+ $this->fileStateByPath[$path] = [
+ self::EXISTS => true,
+ self::SIZE => $size,
+ self::SHA1 => $sha1Base36
+ ];
+ }
+
+ /**
+ * Predicate that no file exists at the path
+ *
+ * @param string $path Storage path
+ */
+ public function assumeFileDoesNotExist( string $path ) {
+ $this->fileStateByPath[$path] = [
+ self::EXISTS => false,
+ self::SIZE => false,
+ self::SHA1 => false
+ ];
+ }
+
+ /**
+ * Get the hypothetical existance a file given predicated and current state of files
+ *
+ * @param string $path Storage path
+ * @param Closure $curExistenceFunc Function to compute the current existence for a given path
+ * @return bool|null Whether the file exists; null on error
+ */
+ public function resolveFileExistence( string $path, $curExistenceFunc ) {
+ return self::resolveFileProperty( $path, self::EXISTS, $curExistenceFunc );
+ }
+
+ /**
+ * Get the hypothetical size of a file given predicated and current state of files
+ *
+ * @param string $path Storage path
+ * @param Closure $curSizeFunc Function to compute the current size for a given path
+ * @return int|false Bytes; false on error
+ */
+ public function resolveFileSize( string $path, $curSizeFunc ) {
+ return self::resolveFileProperty( $path, self::SIZE, $curSizeFunc );
+ }
+
+ /**
+ * Get the hypothetical SHA-1 hash of a file given predicated and current state of files
+ *
+ * @param string $path Storage path
+ * @param Closure $curSha1Func Function to compute the current SHA-1 hash for a given path
+ * @return string|false Base 36 SHA-1 hash; false on error
+ */
+ public function resolveFileSha1Base36( string $path, $curSha1Func ) {
+ return self::resolveFileProperty( $path, self::SHA1, $curSha1Func );
+ }
+
+ /**
+ * @param string $path Storage path
+ * @param string $property One of (self::EXISTS, self::SIZE, self::SHA1)
+ * @param Closure $curValueFunc Function to compute the current value for a given path
+ * @return mixed
+ */
+ private function resolveFileProperty( $path, $property, $curValueFunc ) {
+ if ( isset( $this->fileStateByPath[$path] ) ) {
+ // File is predicated to have a counterfactual state
+ $value = $this->fileStateByPath[$path][$property];
+ if ( $value instanceof Closure ) {
+ $value = $value();
+ $this->fileStateByPath[$path][$property] = $value;
+ }
+ } else {
+ // File is not predicated to have a counterfactual state; use the current state
+ $value = $curValueFunc( $path );
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param string[] $paths List of storage paths
+ * @return self Clone predicates limited to the given paths
+ */
+ public function snapshot( array $paths ) {
+ $snapshot = new self();
+ foreach ( $paths as $path ) {
+ if ( isset( $this->fileStateByPath[$path] ) ) {
+ $snapshot->fileStateByPath[$path] = $this->fileStateByPath[$path];
+ }
+ }
+
+ return $snapshot;
+ }
+}
diff --git a/includes/libs/filebackend/fileop/MoveFileOp.php b/includes/libs/filebackend/fileop/MoveFileOp.php
index 06855777aed9..708574b18393 100644
--- a/includes/libs/filebackend/fileop/MoveFileOp.php
+++ b/includes/libs/filebackend/fileop/MoveFileOp.php
@@ -34,18 +34,19 @@ class MoveFileOp extends FileOp {
];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
// Check source file existence
- $srcExists = $this->fileExists( $this->params['src'], $predicates );
+ $srcExists = $this->resolveFileExistence( $this->params['src'], $opPredicates );
if ( $srcExists === false ) {
if ( $this->getParam( 'ignoreMissingSource' ) ) {
$this->cancelled = true; // no-op
// Update file existence predicates (cache 404s)
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = false;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] = false;
- $predicates[self::ASSUMED_SHA1][$this->params['src']] = false;
+ $batchPredicates->assumeFileDoesNotExist( $this->params['src'] );
return $status; // nothing to do
} else {
@@ -59,17 +60,25 @@ class MoveFileOp extends FileOp {
return $status;
}
// Check if an incompatible destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
+ $srcSize = function () use ( $opPredicates ) {
+ static $size = null;
+ $size ??= $this->resolveFileSize( $this->params['src'], $opPredicates );
+ return $size;
+ };
+ $srcSha1 = function () use ( $opPredicates ) {
+ static $sha1 = null;
+ $sha1 ??= $this->resolveFileSha1Base36( $this->params['src'], $opPredicates );
+ return $sha1;
+ };
+ $status->merge( $this->precheckDestExistence( $opPredicates, $srcSize, $srcSha1 ) );
$this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
// Update file existence predicates if the operation is expected to be allowed to run
if ( $status->isOK() ) {
- $predicates[self::ASSUMED_EXISTS][$this->params['src']] = false;
- $predicates[self::ASSUMED_SIZE][$this->params['src']] = false;
- $predicates[self::ASSUMED_SHA1][$this->params['src']] = false;
- $predicates[self::ASSUMED_EXISTS][$this->params['dst']] = true;
- $predicates[self::ASSUMED_SIZE][$this->params['dst']] = $this->sourceSize;
- $predicates[self::ASSUMED_SHA1][$this->params['dst']] = $this->sourceSha1;
+ $batchPredicates->assumeFileExists( $this->params['dst'], $srcSize, $srcSha1 );
+ if ( $this->params['src'] !== $this->params['dst'] ) {
+ $batchPredicates->assumeFileDoesNotExist( $this->params['src'] );
+ }
}
return $status; // safe to call attempt()
diff --git a/includes/libs/filebackend/fileop/StoreFileOp.php b/includes/libs/filebackend/fileop/StoreFileOp.php
index 76cc4b6c494e..ef4974ff9c96 100644
--- a/includes/libs/filebackend/fileop/StoreFileOp.php
+++ b/includes/libs/filebackend/fileop/StoreFileOp.php
@@ -36,31 +36,38 @@ class StoreFileOp extends FileOp {
];
}
- protected function doPrecheck( array &$predicates ) {
+ protected function doPrecheck(
+ FileStatePredicates $opPredicates,
+ FileStatePredicates $batchPredicates
+ ) {
$status = StatusValue::newGood();
- // Check if the source file exists in the file system and is not too big
+ // Check if the source file exists in the file system
if ( !is_file( $this->params['src'] ) ) {
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
return $status;
}
// Check if the source file is too big
- $maxBytes = $this->backend->maxFileSizeInternal();
- if ( filesize( $this->params['src'] ) > $maxBytes ) {
- $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
+ $sourceSize = $this->getSourceSize();
+ $maxFileSize = $this->backend->maxFileSizeInternal();
+ if ( $sourceSize > $maxFileSize ) {
+ $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxFileSize );
return $status;
}
// Check if an incompatible destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
+ $sourceSha1 = function () {
+ static $sha1 = null;
+ $sha1 ??= $this->getSourceSha1Base36();
+ return $sha1;
+ };
+ $status->merge( $this->precheckDestExistence( $opPredicates, $sourceSize, $sourceSha1 ) );
$this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
// Update file existence predicates if the operation is expected to be allowed to run
if ( $status->isOK() ) {
- $predicates[self::ASSUMED_EXISTS][$this->params['dst']] = true;
- $predicates[self::ASSUMED_SIZE][$this->params['dst']] = $this->sourceSize;
- $predicates[self::ASSUMED_SHA1][$this->params['dst']] = $this->sourceSha1;
+ $batchPredicates->assumeFileExists( $this->params['dst'], $sourceSize, $sourceSha1 );
}
return $status; // safe to call attempt()
diff --git a/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php b/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
index c292a8dd0c9f..ebb7659da5a4 100644
--- a/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
+++ b/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
@@ -1672,19 +1672,19 @@ class FileBackendIntegrationTest extends MediaWikiIntegrationTestCase {
}
}
- public function testDoOperations() {
+ public function testDoOperationsSuccessful() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
- $this->doTestDoOperations();
+ $this->doTestDoOperationsSuccessful();
$this->tearDownFiles();
$this->backend = $this->multiBackend;
$this->tearDownFiles();
- $this->doTestDoOperations();
+ $this->doTestDoOperationsSuccessful();
$this->tearDownFiles();
}
- private function doTestDoOperations() {
+ private function doTestDoOperationsSuccessful() {
$base = self::baseStorePath();
$fileA = "$base/unittest-cont1/e/a/b/fileA.txt";