jenkins-bot has submitted this change and it was merged. Change subject: Move FileBackendStore and FileOp classes to /libs ......................................................................
Move FileBackendStore and FileOp classes to /libs Change-Id: If490f64bec282e5dfcdaf7feb1cbf46d3dce1064 --- M autoload.php D includes/filebackend/FileOp.php R includes/libs/filebackend/FileBackendStore.php R includes/libs/filebackend/FileOpBatch.php A includes/libs/filebackend/fileop/CopyFileOp.php A includes/libs/filebackend/fileop/CreateFileOp.php A includes/libs/filebackend/fileop/DeleteFileOp.php A includes/libs/filebackend/fileop/DescribeFileOp.php A includes/libs/filebackend/fileop/FileOp.php A includes/libs/filebackend/fileop/MoveFileOp.php A includes/libs/filebackend/fileop/NullFileOp.php A includes/libs/filebackend/fileop/StoreFileOp.php 12 files changed, 1,028 insertions(+), 869 deletions(-) Approvals: Krinkle: Looks good to me, approved jenkins-bot: Verified diff --git a/autoload.php b/autoload.php index 9bd6c26..0c59b4a 100644 --- a/autoload.php +++ b/autoload.php @@ -286,13 +286,13 @@ 'Cookie' => __DIR__ . '/includes/libs/Cookie.php', 'CookieJar' => __DIR__ . '/includes/libs/CookieJar.php', 'CopyFileBackend' => __DIR__ . '/maintenance/copyFileBackend.php', - 'CopyFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'CopyFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CopyFileOp.php', 'CopyJobQueue' => __DIR__ . '/maintenance/copyJobQueue.php', 'CoreParserFunctions' => __DIR__ . '/includes/parser/CoreParserFunctions.php', 'CoreTagHooks' => __DIR__ . '/includes/parser/CoreTagHooks.php', 'CoreVersionChecker' => __DIR__ . '/includes/registration/CoreVersionChecker.php', 'CreateAndPromote' => __DIR__ . '/maintenance/createAndPromote.php', - 'CreateFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'CreateFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CreateFileOp.php', 'CreditsAction' => __DIR__ . '/includes/actions/CreditsAction.php', 'CssContent' => __DIR__ . '/includes/content/CssContent.php', 'CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php', @@ -343,7 +343,7 @@ 'DeleteBatch' => __DIR__ . '/maintenance/deleteBatch.php', 'DeleteDefaultMessages' => __DIR__ . '/maintenance/deleteDefaultMessages.php', 'DeleteEqualMessages' => __DIR__ . '/maintenance/deleteEqualMessages.php', - 'DeleteFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'DeleteFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DeleteFileOp.php', 'DeleteLinksJob' => __DIR__ . '/includes/jobqueue/jobs/DeleteLinksJob.php', 'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php', 'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php', @@ -358,7 +358,7 @@ 'DerivativeContext' => __DIR__ . '/includes/context/DerivativeContext.php', 'DerivativeRequest' => __DIR__ . '/includes/DerivativeRequest.php', 'DerivativeResourceLoaderContext' => __DIR__ . '/includes/resourceloader/DerivativeResourceLoaderContext.php', - 'DescribeFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'DescribeFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DescribeFileOp.php', 'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php', 'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php', 'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php', @@ -459,11 +459,11 @@ 'FileBackendError' => __DIR__ . '/includes/libs/filebackend/FileBackendError.php', 'FileBackendGroup' => __DIR__ . '/includes/filebackend/FileBackendGroup.php', 'FileBackendMultiWrite' => __DIR__ . '/includes/libs/filebackend/FileBackendMultiWrite.php', - 'FileBackendStore' => __DIR__ . '/includes/filebackend/FileBackendStore.php', - 'FileBackendStoreOpHandle' => __DIR__ . '/includes/filebackend/FileBackendStore.php', - 'FileBackendStoreShardDirIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php', - 'FileBackendStoreShardFileIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php', - 'FileBackendStoreShardListIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php', + 'FileBackendStore' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php', + 'FileBackendStoreOpHandle' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php', + 'FileBackendStoreShardDirIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php', + 'FileBackendStoreShardFileIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php', + 'FileBackendStoreShardListIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php', 'FileBasedSiteLookup' => __DIR__ . '/includes/site/FileBasedSiteLookup.php', 'FileCacheBase' => __DIR__ . '/includes/cache/FileCacheBase.php', 'FileContentsHasher' => __DIR__ . '/includes/utils/FileContentsHasher.php', @@ -471,8 +471,8 @@ 'FileDependency' => __DIR__ . '/includes/cache/CacheDependency.php', 'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php', 'FileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/FileJournal.php', - 'FileOp' => __DIR__ . '/includes/filebackend/FileOp.php', - 'FileOpBatch' => __DIR__ . '/includes/filebackend/FileOpBatch.php', + 'FileOp' => __DIR__ . '/includes/libs/filebackend/fileop/FileOp.php', + 'FileOpBatch' => __DIR__ . '/includes/libs/filebackend/FileOpBatch.php', 'FileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php', 'FileRepoStatus' => __DIR__ . '/includes/filerepo/FileRepoStatus.php', 'FindDeprecated' => __DIR__ . '/maintenance/findDeprecated.php', @@ -947,7 +947,7 @@ 'MostlinkedTemplatesPage' => __DIR__ . '/includes/specials/SpecialMostlinkedtemplates.php', 'MostrevisionsPage' => __DIR__ . '/includes/specials/SpecialMostrevisions.php', 'MoveBatch' => __DIR__ . '/maintenance/moveBatch.php', - 'MoveFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'MoveFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/MoveFileOp.php', 'MoveLogFormatter' => __DIR__ . '/includes/logging/MoveLogFormatter.php', 'MovePage' => __DIR__ . '/includes/MovePage.php', 'MovePageForm' => __DIR__ . '/includes/specials/SpecialMovepage.php', @@ -980,7 +980,7 @@ 'NukeNS' => __DIR__ . '/maintenance/nukeNS.php', 'NukePage' => __DIR__ . '/maintenance/nukePage.php', 'NullFileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/NullFileJournal.php', - 'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'NullFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/NullFileOp.php', 'NullIndexField' => __DIR__ . '/includes/search/NullIndexField.php', 'NullJob' => __DIR__ . '/includes/jobqueue/jobs/NullJob.php', 'NullLockManager' => __DIR__ . '/includes/libs/lockmanager/NullLockManager.php', @@ -1386,7 +1386,7 @@ 'Status' => __DIR__ . '/includes/Status.php', 'StatusValue' => __DIR__ . '/includes/libs/StatusValue.php', 'StorageTypeStats' => __DIR__ . '/maintenance/storage/storageTypeStats.php', - 'StoreFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', + 'StoreFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/StoreFileOp.php', 'StreamFile' => __DIR__ . '/includes/StreamFile.php', 'StringPrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', 'StringUtils' => __DIR__ . '/includes/libs/StringUtils.php', diff --git a/includes/filebackend/FileOp.php b/includes/filebackend/FileOp.php deleted file mode 100644 index 8207b12..0000000 --- a/includes/filebackend/FileOp.php +++ /dev/null @@ -1,855 +0,0 @@ -<?php -/** - * Helper class for representing operations with transaction support. - * - * 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 - * @author Aaron Schulz - */ -use Psr\Log\LoggerInterface; - -/** - * FileBackend helper class for representing operations. - * Do not use this class from places outside FileBackend. - * - * Methods called from FileOpBatch::attempt() should avoid throwing - * exceptions at all costs. FileOp objects should be lightweight in order - * to support large arrays in memory and serialization. - * - * @ingroup FileBackend - * @since 1.19 - */ -abstract class FileOp { - /** @var array */ - protected $params = []; - - /** @var FileBackendStore */ - protected $backend; - /** @var LoggerInterface */ - protected $logger; - - /** @var int */ - protected $state = self::STATE_NEW; - - /** @var bool */ - protected $failed = false; - - /** @var bool */ - protected $async = false; - - /** @var string */ - protected $batchId; - - /** @var bool Operation is not a no-op */ - protected $doOperation = true; - - /** @var string */ - protected $sourceSha1; - - /** @var bool */ - protected $overwriteSameCase; - - /** @var bool */ - protected $destExists; - - /* Object life-cycle */ - const STATE_NEW = 1; - const STATE_CHECKED = 2; - const STATE_ATTEMPTED = 3; - - /** - * Build a new batch file operation transaction - * - * @param FileBackendStore $backend - * @param array $params - * @param LoggerInterface $logger PSR logger instance - * @throws FileBackendError - */ - final public function __construct( - FileBackendStore $backend, array $params, LoggerInterface $logger - ) { - $this->backend = $backend; - $this->logger = $logger; - list( $required, $optional, $paths ) = $this->allowedParams(); - foreach ( $required as $name ) { - if ( isset( $params[$name] ) ) { - $this->params[$name] = $params[$name]; - } else { - throw new InvalidArgumentException( "File operation missing parameter '$name'." ); - } - } - foreach ( $optional as $name ) { - if ( isset( $params[$name] ) ) { - $this->params[$name] = $params[$name]; - } - } - foreach ( $paths as $name ) { - if ( isset( $this->params[$name] ) ) { - // Normalize paths so the paths to the same file have the same string - $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] ); - } - } - } - - /** - * Normalize a string if it is a valid storage path - * - * @param string $path - * @return string - */ - protected static function normalizeIfValidStoragePath( $path ) { - if ( FileBackend::isStoragePath( $path ) ) { - $res = FileBackend::normalizeStoragePath( $path ); - - return ( $res !== null ) ? $res : $path; - } - - return $path; - } - - /** - * Set the batch UUID this operation belongs to - * - * @param string $batchId - */ - final public function setBatchId( $batchId ) { - $this->batchId = $batchId; - } - - /** - * Get the value of the parameter with the given name - * - * @param string $name - * @return mixed Returns null if the parameter is not set - */ - final public function getParam( $name ) { - return isset( $this->params[$name] ) ? $this->params[$name] : null; - } - - /** - * Check if this operation failed precheck() or attempt() - * - * @return bool - */ - final public function failed() { - return $this->failed; - } - - /** - * Get a new empty predicates array for precheck() - * - * @return array - */ - final public static function newPredicates() { - return [ 'exists' => [], 'sha1' => [] ]; - } - - /** - * Get a new empty dependency tracking array for paths read/written to - * - * @return array - */ - final public static function newDependencies() { - return [ 'read' => [], 'write' => [] ]; - } - - /** - * Update a dependency tracking array to account for this operation - * - * @param array $deps Prior path reads/writes; format of FileOp::newPredicates() - * @return array - */ - final public function applyDependencies( array $deps ) { - $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); - $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); - - return $deps; - } - - /** - * Check if this operation changes files listed in $paths - * - * @param array $deps Prior path reads/writes; format of FileOp::newPredicates() - * @return bool - */ - final public function dependsOn( array $deps ) { - foreach ( $this->storagePathsChanged() as $path ) { - if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { - return true; // "output" or "anti" dependency - } - } - foreach ( $this->storagePathsRead() as $path ) { - if ( isset( $deps['write'][$path] ) ) { - return true; // "flow" dependency - } - } - - return false; - } - - /** - * Get the file journal entries for this file operation - * - * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates) - * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates) - * @return array - */ - final public function getJournalEntries( array $oPredicates, array $nPredicates ) { - if ( !$this->doOperation ) { - return []; // this is a no-op - } - $nullEntries = []; - $updateEntries = []; - $deleteEntries = []; - $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); - foreach ( array_unique( $pathsUsed ) as $path ) { - $nullEntries[] = [ // assertion for recovery - 'op' => 'null', - 'path' => $path, - 'newSha1' => $this->fileSha1( $path, $oPredicates ) - ]; - } - foreach ( $this->storagePathsChanged() as $path ) { - if ( $nPredicates['sha1'][$path] === false ) { // deleted - $deleteEntries[] = [ - 'op' => 'delete', - 'path' => $path, - 'newSha1' => '' - ]; - } else { // created/updated - $updateEntries[] = [ - 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', - 'path' => $path, - 'newSha1' => $nPredicates['sha1'][$path] - ]; - } - } - - return array_merge( $nullEntries, $updateEntries, $deleteEntries ); - } - - /** - * 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. - * - * @param array $predicates - * @return StatusValue - */ - final public function precheck( array &$predicates ) { - if ( $this->state !== self::STATE_NEW ) { - return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); - } - $this->state = self::STATE_CHECKED; - $status = $this->doPrecheck( $predicates ); - if ( !$status->isOK() ) { - $this->failed = true; - } - - return $status; - } - - /** - * @param array $predicates - * @return StatusValue - */ - protected function doPrecheck( array &$predicates ) { - return StatusValue::newGood(); - } - - /** - * Attempt the operation - * - * @return StatusValue - */ - final public function attempt() { - if ( $this->state !== self::STATE_CHECKED ) { - return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); - } elseif ( $this->failed ) { // failed precheck - return StatusValue::newFatal( 'fileop-fail-attempt-precheck' ); - } - $this->state = self::STATE_ATTEMPTED; - if ( $this->doOperation ) { - $status = $this->doAttempt(); - if ( !$status->isOK() ) { - $this->failed = true; - $this->logFailure( 'attempt' ); - } - } else { // no-op - $status = StatusValue::newGood(); - } - - return $status; - } - - /** - * @return StatusValue - */ - protected function doAttempt() { - return StatusValue::newGood(); - } - - /** - * Attempt the operation in the background - * - * @return StatusValue - */ - final public function attemptAsync() { - $this->async = true; - $result = $this->attempt(); - $this->async = false; - - return $result; - } - - /** - * Get the file operation parameters - * - * @return array (required params list, optional params list, list of params that are paths) - */ - protected function allowedParams() { - return [ [], [], [] ]; - } - - /** - * Adjust params to FileBackendStore internal file calls - * - * @param array $params - * @return array (required params list, optional params list) - */ - protected function setFlags( array $params ) { - return [ 'async' => $this->async ] + $params; - } - - /** - * Get a list of storage paths read from for this operation - * - * @return array - */ - public function storagePathsRead() { - return []; - } - - /** - * Get a list of storage paths written to for this operation - * - * @return array - */ - public function storagePathsChanged() { - return []; - } - - /** - * Check for errors with regards to the destination file already existing. - * Also set the destExists, overwriteSameCase and sourceSha1 member variables. - * A bad StatusValue will be returned if there is no chance it can be overwritten. - * - * @param array $predicates - * @return StatusValue - */ - protected function precheckDestExistence( array $predicates ) { - $status = StatusValue::newGood(); - // Get hash of source file/string and the destination file - $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string - if ( $this->sourceSha1 === null ) { // file in storage? - $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); - } - $this->overwriteSameCase = false; - $this->destExists = $this->fileExists( $this->params['dst'], $predicates ); - if ( $this->destExists ) { - if ( $this->getParam( 'overwrite' ) ) { - return $status; // OK - } elseif ( $this->getParam( 'overwriteSame' ) ) { - $dhash = $this->fileSha1( $this->params['dst'], $predicates ); - // Check if hashes are valid and match each other... - if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { - $status->fatal( 'backend-fail-hashes' ); - } elseif ( $this->sourceSha1 !== $dhash ) { - // Give an error if the files are not identical - $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); - } else { - $this->overwriteSameCase = true; // OK - } - - return $status; // do nothing; either OK or bad status - } else { - $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); - - return $status; - } - } - - return $status; - } - - /** - * precheckDestExistence() helper function to get the source file SHA-1. - * Subclasses should overwride this if the source is not in storage. - * - * @return string|bool Returns false on failure - */ - protected function getSourceSha1Base36() { - return null; // N/A - } - - /** - * Check if a file will exist in storage when this operation is attempted - * - * @param string $source Storage path - * @param array $predicates - * @return bool - */ - final protected function fileExists( $source, array $predicates ) { - if ( isset( $predicates['exists'][$source] ) ) { - return $predicates['exists'][$source]; // previous op assures this - } else { - $params = [ 'src' => $source, 'latest' => true ]; - - return $this->backend->fileExists( $params ); - } - } - - /** - * 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 False on failure - */ - final protected function fileSha1( $source, array $predicates ) { - if ( isset( $predicates['sha1'][$source] ) ) { - return $predicates['sha1'][$source]; // previous op assures this - } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) { - return false; // previous op assures this - } else { - $params = [ 'src' => $source, 'latest' => true ]; - - return $this->backend->getFileSha1Base36( $params ); - } - } - - /** - * Get the backend this operation is for - * - * @return FileBackendStore - */ - public function getBackend() { - return $this->backend; - } - - /** - * Log a file operation failure and preserve any temp files - * - * @param string $action - */ - final public function logFailure( $action ) { - $params = $this->params; - $params['failedAction'] = $action; - try { - $this->logger->error( get_class( $this ) . - " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); - } catch ( Exception $e ) { - // bad config? debug log error? - } - } -} - -/** - * Create a file in the backend with the given content. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class CreateFileOp extends FileOp { - protected function allowedParams() { - return [ - [ 'content', 'dst' ], - [ 'overwrite', 'overwriteSame', 'headers' ], - [ 'dst' ] - ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source data is too big - if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { - $status->fatal( 'backend-fail-maxsize', - $this->params['dst'], $this->backend->maxFileSizeInternal() ); - $status->fatal( 'backend-fail-create', $this->params['dst'] ); - - return $status; - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-create', $this->params['dst'] ); - - return $status; - } - // Check if destination file exists - $status->merge( $this->precheckDestExistence( $predicates ) ); - $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() - if ( $status->isOK() ) { - // Update file existence predicates - $predicates['exists'][$this->params['dst']] = true; - $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; - } - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - if ( !$this->overwriteSameCase ) { - // Create the file at the destination - return $this->backend->createInternal( $this->setFlags( $this->params ) ); - } - - return StatusValue::newGood(); - } - - protected function getSourceSha1Base36() { - return Wikimedia\base_convert( sha1( $this->params['content'] ), 16, 36, 31 ); - } - - public function storagePathsChanged() { - return [ $this->params['dst'] ]; - } -} - -/** - * Store a file into the backend from a file on the file system. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class StoreFileOp extends FileOp { - protected function allowedParams() { - return [ - [ 'src', 'dst' ], - [ 'overwrite', 'overwriteSame', 'headers' ], - [ 'src', 'dst' ] - ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source file exists on 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 - } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { - $status->fatal( 'backend-fail-maxsize', - $this->params['dst'], $this->backend->maxFileSizeInternal() ); - $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); - - return $status; - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); - - return $status; - } - // Check if destination file exists - $status->merge( $this->precheckDestExistence( $predicates ) ); - $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() - if ( $status->isOK() ) { - // Update file existence predicates - $predicates['exists'][$this->params['dst']] = true; - $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; - } - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - if ( !$this->overwriteSameCase ) { - // Store the file at the destination - return $this->backend->storeInternal( $this->setFlags( $this->params ) ); - } - - return StatusValue::newGood(); - } - - protected function getSourceSha1Base36() { - MediaWiki\suppressWarnings(); - $hash = sha1_file( $this->params['src'] ); - MediaWiki\restoreWarnings(); - if ( $hash !== false ) { - $hash = Wikimedia\base_convert( $hash, 16, 36, 31 ); - } - - return $hash; - } - - public function storagePathsChanged() { - return [ $this->params['dst'] ]; - } -} - -/** - * Copy a file from one storage path to another in the backend. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class CopyFileOp extends FileOp { - protected function allowedParams() { - return [ - [ 'src', 'dst' ], - [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ], - [ 'src', 'dst' ] - ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { - if ( $this->getParam( 'ignoreMissingSource' ) ) { - $this->doOperation = false; // no-op - // Update file existence predicates (cache 404s) - $predicates['exists'][$this->params['src']] = false; - $predicates['sha1'][$this->params['src']] = false; - - return $status; // nothing to do - } else { - $status->fatal( 'backend-fail-notexists', $this->params['src'] ); - - return $status; - } - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); - - return $status; - } - // Check if destination file exists - $status->merge( $this->precheckDestExistence( $predicates ) ); - $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() - if ( $status->isOK() ) { - // Update file existence predicates - $predicates['exists'][$this->params['dst']] = true; - $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; - } - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - if ( $this->overwriteSameCase ) { - $status = StatusValue::newGood(); // nothing to do - } elseif ( $this->params['src'] === $this->params['dst'] ) { - // Just update the destination file headers - $headers = $this->getParam( 'headers' ) ?: []; - $status = $this->backend->describeInternal( $this->setFlags( [ - 'src' => $this->params['dst'], 'headers' => $headers - ] ) ); - } else { - // Copy the file to the destination - $status = $this->backend->copyInternal( $this->setFlags( $this->params ) ); - } - - return $status; - } - - public function storagePathsRead() { - return [ $this->params['src'] ]; - } - - public function storagePathsChanged() { - return [ $this->params['dst'] ]; - } -} - -/** - * Move a file from one storage path to another in the backend. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class MoveFileOp extends FileOp { - protected function allowedParams() { - return [ - [ 'src', 'dst' ], - [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ], - [ 'src', 'dst' ] - ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { - if ( $this->getParam( 'ignoreMissingSource' ) ) { - $this->doOperation = false; // no-op - // Update file existence predicates (cache 404s) - $predicates['exists'][$this->params['src']] = false; - $predicates['sha1'][$this->params['src']] = false; - - return $status; // nothing to do - } else { - $status->fatal( 'backend-fail-notexists', $this->params['src'] ); - - return $status; - } - // Check if a file can be placed/changed at the destination - } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['dst'] ); - $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); - - return $status; - } - // Check if destination file exists - $status->merge( $this->precheckDestExistence( $predicates ) ); - $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() - if ( $status->isOK() ) { - // Update file existence predicates - $predicates['exists'][$this->params['src']] = false; - $predicates['sha1'][$this->params['src']] = false; - $predicates['exists'][$this->params['dst']] = true; - $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; - } - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - if ( $this->overwriteSameCase ) { - if ( $this->params['src'] === $this->params['dst'] ) { - // Do nothing to the destination (which is also the source) - $status = StatusValue::newGood(); - } else { - // Just delete the source as the destination file needs no changes - $status = $this->backend->deleteInternal( $this->setFlags( - [ 'src' => $this->params['src'] ] - ) ); - } - } elseif ( $this->params['src'] === $this->params['dst'] ) { - // Just update the destination file headers - $headers = $this->getParam( 'headers' ) ?: []; - $status = $this->backend->describeInternal( $this->setFlags( - [ 'src' => $this->params['dst'], 'headers' => $headers ] - ) ); - } else { - // Move the file to the destination - $status = $this->backend->moveInternal( $this->setFlags( $this->params ) ); - } - - return $status; - } - - public function storagePathsRead() { - return [ $this->params['src'] ]; - } - - public function storagePathsChanged() { - return [ $this->params['src'], $this->params['dst'] ]; - } -} - -/** - * Delete a file at the given storage path from the backend. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class DeleteFileOp extends FileOp { - protected function allowedParams() { - return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { - if ( $this->getParam( 'ignoreMissingSource' ) ) { - $this->doOperation = false; // no-op - // Update file existence predicates (cache 404s) - $predicates['exists'][$this->params['src']] = false; - $predicates['sha1'][$this->params['src']] = false; - - return $status; // nothing to do - } else { - $status->fatal( 'backend-fail-notexists', $this->params['src'] ); - - return $status; - } - // Check if a file can be placed/changed at the source - } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['src'] ); - $status->fatal( 'backend-fail-delete', $this->params['src'] ); - - return $status; - } - // Update file existence predicates - $predicates['exists'][$this->params['src']] = false; - $predicates['sha1'][$this->params['src']] = false; - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - // Delete the source file - return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); - } - - public function storagePathsChanged() { - return [ $this->params['src'] ]; - } -} - -/** - * Change metadata for a file at the given storage path in the backend. - * Parameters for this operation are outlined in FileBackend::doOperations(). - */ -class DescribeFileOp extends FileOp { - protected function allowedParams() { - return [ [ 'src' ], [ 'headers' ], [ 'src' ] ]; - } - - protected function doPrecheck( array &$predicates ) { - $status = StatusValue::newGood(); - // Check if the source file exists - if ( !$this->fileExists( $this->params['src'], $predicates ) ) { - $status->fatal( 'backend-fail-notexists', $this->params['src'] ); - - return $status; - // Check if a file can be placed/changed at the source - } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { - $status->fatal( 'backend-fail-usable', $this->params['src'] ); - $status->fatal( 'backend-fail-describe', $this->params['src'] ); - - return $status; - } - // Update file existence predicates - $predicates['exists'][$this->params['src']] = - $this->fileExists( $this->params['src'], $predicates ); - $predicates['sha1'][$this->params['src']] = - $this->fileSha1( $this->params['src'], $predicates ); - - return $status; // safe to call attempt() - } - - protected function doAttempt() { - // Update the source file's metadata - return $this->backend->describeInternal( $this->setFlags( $this->params ) ); - } - - public function storagePathsChanged() { - return [ $this->params['src'] ]; - } -} - -/** - * Placeholder operation that has no params and does nothing - */ -class NullFileOp extends FileOp { -} diff --git a/includes/filebackend/FileBackendStore.php b/includes/libs/filebackend/FileBackendStore.php similarity index 100% rename from includes/filebackend/FileBackendStore.php rename to includes/libs/filebackend/FileBackendStore.php diff --git a/includes/filebackend/FileOpBatch.php b/includes/libs/filebackend/FileOpBatch.php similarity index 100% rename from includes/filebackend/FileOpBatch.php rename to includes/libs/filebackend/FileOpBatch.php diff --git a/includes/libs/filebackend/fileop/CopyFileOp.php b/includes/libs/filebackend/fileop/CopyFileOp.php new file mode 100644 index 0000000..e3b8c51 --- /dev/null +++ b/includes/libs/filebackend/fileop/CopyFileOp.php @@ -0,0 +1,97 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ + +/** + * Copy a file from one storage path to another in the backend. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class CopyFileOp extends FileOp { + protected function allowedParams() { + return [ + [ 'src', 'dst' ], + [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ], + [ 'src', 'dst' ] + ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source file exists + if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + if ( $this->getParam( 'ignoreMissingSource' ) ) { + $this->doOperation = false; // no-op + // Update file existence predicates (cache 404s) + $predicates['exists'][$this->params['src']] = false; + $predicates['sha1'][$this->params['src']] = false; + + return $status; // nothing to do + } else { + $status->fatal( 'backend-fail-notexists', $this->params['src'] ); + + return $status; + } + // Check if a file can be placed/changed at the destination + } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); + $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); + + return $status; + } + // Check if destination file exists + $status->merge( $this->precheckDestExistence( $predicates ) ); + $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() + if ( $status->isOK() ) { + // Update file existence predicates + $predicates['exists'][$this->params['dst']] = true; + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; + } + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + if ( $this->overwriteSameCase ) { + $status = StatusValue::newGood(); // nothing to do + } elseif ( $this->params['src'] === $this->params['dst'] ) { + // Just update the destination file headers + $headers = $this->getParam( 'headers' ) ?: []; + $status = $this->backend->describeInternal( $this->setFlags( [ + 'src' => $this->params['dst'], 'headers' => $headers + ] ) ); + } else { + // Copy the file to the destination + $status = $this->backend->copyInternal( $this->setFlags( $this->params ) ); + } + + return $status; + } + + public function storagePathsRead() { + return [ $this->params['src'] ]; + } + + public function storagePathsChanged() { + return [ $this->params['dst'] ]; + } +} diff --git a/includes/libs/filebackend/fileop/CreateFileOp.php b/includes/libs/filebackend/fileop/CreateFileOp.php new file mode 100644 index 0000000..120ca2b --- /dev/null +++ b/includes/libs/filebackend/fileop/CreateFileOp.php @@ -0,0 +1,80 @@ +<?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 + * @author Aaron Schulz + */ + +/** + * Create a file in the backend with the given content. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class CreateFileOp extends FileOp { + protected function allowedParams() { + return [ + [ 'content', 'dst' ], + [ 'overwrite', 'overwriteSame', 'headers' ], + [ 'dst' ] + ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source data is too big + if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { + $status->fatal( 'backend-fail-maxsize', + $this->params['dst'], $this->backend->maxFileSizeInternal() ); + $status->fatal( 'backend-fail-create', $this->params['dst'] ); + + return $status; + // Check if a file can be placed/changed at the destination + } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); + $status->fatal( 'backend-fail-create', $this->params['dst'] ); + + return $status; + } + // Check if destination file exists + $status->merge( $this->precheckDestExistence( $predicates ) ); + $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() + if ( $status->isOK() ) { + // Update file existence predicates + $predicates['exists'][$this->params['dst']] = true; + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; + } + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + if ( !$this->overwriteSameCase ) { + // Create the file at the destination + return $this->backend->createInternal( $this->setFlags( $this->params ) ); + } + + return StatusValue::newGood(); + } + + protected function getSourceSha1Base36() { + return Wikimedia\base_convert( sha1( $this->params['content'] ), 16, 36, 31 ); + } + + public function storagePathsChanged() { + return [ $this->params['dst'] ]; + } +} diff --git a/includes/libs/filebackend/fileop/DeleteFileOp.php b/includes/libs/filebackend/fileop/DeleteFileOp.php new file mode 100644 index 0000000..0ccb1e3 --- /dev/null +++ b/includes/libs/filebackend/fileop/DeleteFileOp.php @@ -0,0 +1,72 @@ +<?php +/** +* Helper class for representing operations with transaction support. +* +* 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 +* @author Aaron Schulz +*/ + +/** + * Delete a file at the given storage path from the backend. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class DeleteFileOp extends FileOp { + protected function allowedParams() { + return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source file exists + if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + if ( $this->getParam( 'ignoreMissingSource' ) ) { + $this->doOperation = false; // no-op + // Update file existence predicates (cache 404s) + $predicates['exists'][$this->params['src']] = false; + $predicates['sha1'][$this->params['src']] = false; + + return $status; // nothing to do + } else { + $status->fatal( 'backend-fail-notexists', $this->params['src'] ); + + return $status; + } + // Check if a file can be placed/changed at the source + } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['src'] ); + $status->fatal( 'backend-fail-delete', $this->params['src'] ); + + return $status; + } + // Update file existence predicates + $predicates['exists'][$this->params['src']] = false; + $predicates['sha1'][$this->params['src']] = false; + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + // Delete the source file + return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); + } + + public function storagePathsChanged() { + return [ $this->params['src'] ]; + } +} diff --git a/includes/libs/filebackend/fileop/DescribeFileOp.php b/includes/libs/filebackend/fileop/DescribeFileOp.php new file mode 100644 index 0000000..9b53222 --- /dev/null +++ b/includes/libs/filebackend/fileop/DescribeFileOp.php @@ -0,0 +1,65 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ + +/** + * Change metadata for a file at the given storage path in the backend. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class DescribeFileOp extends FileOp { + protected function allowedParams() { + return [ [ 'src' ], [ 'headers' ], [ 'src' ] ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source file exists + if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + $status->fatal( 'backend-fail-notexists', $this->params['src'] ); + + return $status; + // Check if a file can be placed/changed at the source + } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['src'] ); + $status->fatal( 'backend-fail-describe', $this->params['src'] ); + + return $status; + } + // Update file existence predicates + $predicates['exists'][$this->params['src']] = + $this->fileExists( $this->params['src'], $predicates ); + $predicates['sha1'][$this->params['src']] = + $this->fileSha1( $this->params['src'], $predicates ); + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + // Update the source file's metadata + return $this->backend->describeInternal( $this->setFlags( $this->params ) ); + } + + public function storagePathsChanged() { + return [ $this->params['src'] ]; + } +} diff --git a/includes/libs/filebackend/fileop/FileOp.php b/includes/libs/filebackend/fileop/FileOp.php new file mode 100644 index 0000000..fab5a37 --- /dev/null +++ b/includes/libs/filebackend/fileop/FileOp.php @@ -0,0 +1,470 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ +use Psr\Log\LoggerInterface; + +/** + * FileBackend helper class for representing operations. + * Do not use this class from places outside FileBackend. + * + * Methods called from FileOpBatch::attempt() should avoid throwing + * exceptions at all costs. FileOp objects should be lightweight in order + * to support large arrays in memory and serialization. + * + * @ingroup FileBackend + * @since 1.19 + */ +abstract class FileOp { + /** @var array */ + protected $params = []; + + /** @var FileBackendStore */ + protected $backend; + /** @var LoggerInterface */ + protected $logger; + + /** @var int */ + protected $state = self::STATE_NEW; + + /** @var bool */ + protected $failed = false; + + /** @var bool */ + protected $async = false; + + /** @var string */ + protected $batchId; + + /** @var bool Operation is not a no-op */ + protected $doOperation = true; + + /** @var string */ + protected $sourceSha1; + + /** @var bool */ + protected $overwriteSameCase; + + /** @var bool */ + protected $destExists; + + /* Object life-cycle */ + const STATE_NEW = 1; + const STATE_CHECKED = 2; + const STATE_ATTEMPTED = 3; + + /** + * Build a new batch file operation transaction + * + * @param FileBackendStore $backend + * @param array $params + * @param LoggerInterface $logger PSR logger instance + * @throws FileBackendError + */ + final public function __construct( + FileBackendStore $backend, array $params, LoggerInterface $logger + ) { + $this->backend = $backend; + $this->logger = $logger; + list( $required, $optional, $paths ) = $this->allowedParams(); + foreach ( $required as $name ) { + if ( isset( $params[$name] ) ) { + $this->params[$name] = $params[$name]; + } else { + throw new InvalidArgumentException( "File operation missing parameter '$name'." ); + } + } + foreach ( $optional as $name ) { + if ( isset( $params[$name] ) ) { + $this->params[$name] = $params[$name]; + } + } + foreach ( $paths as $name ) { + if ( isset( $this->params[$name] ) ) { + // Normalize paths so the paths to the same file have the same string + $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] ); + } + } + } + + /** + * Normalize a string if it is a valid storage path + * + * @param string $path + * @return string + */ + protected static function normalizeIfValidStoragePath( $path ) { + if ( FileBackend::isStoragePath( $path ) ) { + $res = FileBackend::normalizeStoragePath( $path ); + + return ( $res !== null ) ? $res : $path; + } + + return $path; + } + + /** + * Set the batch UUID this operation belongs to + * + * @param string $batchId + */ + final public function setBatchId( $batchId ) { + $this->batchId = $batchId; + } + + /** + * Get the value of the parameter with the given name + * + * @param string $name + * @return mixed Returns null if the parameter is not set + */ + final public function getParam( $name ) { + return isset( $this->params[$name] ) ? $this->params[$name] : null; + } + + /** + * Check if this operation failed precheck() or attempt() + * + * @return bool + */ + final public function failed() { + return $this->failed; + } + + /** + * Get a new empty predicates array for precheck() + * + * @return array + */ + final public static function newPredicates() { + return [ 'exists' => [], 'sha1' => [] ]; + } + + /** + * Get a new empty dependency tracking array for paths read/written to + * + * @return array + */ + final public static function newDependencies() { + return [ 'read' => [], 'write' => [] ]; + } + + /** + * Update a dependency tracking array to account for this operation + * + * @param array $deps Prior path reads/writes; format of FileOp::newPredicates() + * @return array + */ + final public function applyDependencies( array $deps ) { + $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); + $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); + + return $deps; + } + + /** + * Check if this operation changes files listed in $paths + * + * @param array $deps Prior path reads/writes; format of FileOp::newPredicates() + * @return bool + */ + final public function dependsOn( array $deps ) { + foreach ( $this->storagePathsChanged() as $path ) { + if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { + return true; // "output" or "anti" dependency + } + } + foreach ( $this->storagePathsRead() as $path ) { + if ( isset( $deps['write'][$path] ) ) { + return true; // "flow" dependency + } + } + + return false; + } + + /** + * Get the file journal entries for this file operation + * + * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates) + * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates) + * @return array + */ + final public function getJournalEntries( array $oPredicates, array $nPredicates ) { + if ( !$this->doOperation ) { + return []; // this is a no-op + } + $nullEntries = []; + $updateEntries = []; + $deleteEntries = []; + $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); + foreach ( array_unique( $pathsUsed ) as $path ) { + $nullEntries[] = [ // assertion for recovery + 'op' => 'null', + 'path' => $path, + 'newSha1' => $this->fileSha1( $path, $oPredicates ) + ]; + } + foreach ( $this->storagePathsChanged() as $path ) { + if ( $nPredicates['sha1'][$path] === false ) { // deleted + $deleteEntries[] = [ + 'op' => 'delete', + 'path' => $path, + 'newSha1' => '' + ]; + } else { // created/updated + $updateEntries[] = [ + 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', + 'path' => $path, + 'newSha1' => $nPredicates['sha1'][$path] + ]; + } + } + + return array_merge( $nullEntries, $updateEntries, $deleteEntries ); + } + + /** + * 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. + * + * @param array $predicates + * @return StatusValue + */ + final public function precheck( array &$predicates ) { + if ( $this->state !== self::STATE_NEW ) { + return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); + } + $this->state = self::STATE_CHECKED; + $status = $this->doPrecheck( $predicates ); + if ( !$status->isOK() ) { + $this->failed = true; + } + + return $status; + } + + /** + * @param array $predicates + * @return StatusValue + */ + protected function doPrecheck( array &$predicates ) { + return StatusValue::newGood(); + } + + /** + * Attempt the operation + * + * @return StatusValue + */ + final public function attempt() { + if ( $this->state !== self::STATE_CHECKED ) { + return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); + } elseif ( $this->failed ) { // failed precheck + return StatusValue::newFatal( 'fileop-fail-attempt-precheck' ); + } + $this->state = self::STATE_ATTEMPTED; + if ( $this->doOperation ) { + $status = $this->doAttempt(); + if ( !$status->isOK() ) { + $this->failed = true; + $this->logFailure( 'attempt' ); + } + } else { // no-op + $status = StatusValue::newGood(); + } + + return $status; + } + + /** + * @return StatusValue + */ + protected function doAttempt() { + return StatusValue::newGood(); + } + + /** + * Attempt the operation in the background + * + * @return StatusValue + */ + final public function attemptAsync() { + $this->async = true; + $result = $this->attempt(); + $this->async = false; + + return $result; + } + + /** + * Get the file operation parameters + * + * @return array (required params list, optional params list, list of params that are paths) + */ + protected function allowedParams() { + return [ [], [], [] ]; + } + + /** + * Adjust params to FileBackendStore internal file calls + * + * @param array $params + * @return array (required params list, optional params list) + */ + protected function setFlags( array $params ) { + return [ 'async' => $this->async ] + $params; + } + + /** + * Get a list of storage paths read from for this operation + * + * @return array + */ + public function storagePathsRead() { + return []; + } + + /** + * Get a list of storage paths written to for this operation + * + * @return array + */ + public function storagePathsChanged() { + return []; + } + + /** + * Check for errors with regards to the destination file already existing. + * Also set the destExists, overwriteSameCase and sourceSha1 member variables. + * A bad StatusValue will be returned if there is no chance it can be overwritten. + * + * @param array $predicates + * @return StatusValue + */ + protected function precheckDestExistence( array $predicates ) { + $status = StatusValue::newGood(); + // Get hash of source file/string and the destination file + $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string + if ( $this->sourceSha1 === null ) { // file in storage? + $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); + } + $this->overwriteSameCase = false; + $this->destExists = $this->fileExists( $this->params['dst'], $predicates ); + if ( $this->destExists ) { + if ( $this->getParam( 'overwrite' ) ) { + return $status; // OK + } elseif ( $this->getParam( 'overwriteSame' ) ) { + $dhash = $this->fileSha1( $this->params['dst'], $predicates ); + // Check if hashes are valid and match each other... + if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { + $status->fatal( 'backend-fail-hashes' ); + } elseif ( $this->sourceSha1 !== $dhash ) { + // Give an error if the files are not identical + $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); + } else { + $this->overwriteSameCase = true; // OK + } + + return $status; // do nothing; either OK or bad status + } else { + $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); + + return $status; + } + } + + return $status; + } + + /** + * precheckDestExistence() helper function to get the source file SHA-1. + * Subclasses should overwride this if the source is not in storage. + * + * @return string|bool Returns false on failure + */ + protected function getSourceSha1Base36() { + return null; // N/A + } + + /** + * Check if a file will exist in storage when this operation is attempted + * + * @param string $source Storage path + * @param array $predicates + * @return bool + */ + final protected function fileExists( $source, array $predicates ) { + if ( isset( $predicates['exists'][$source] ) ) { + return $predicates['exists'][$source]; // previous op assures this + } else { + $params = [ 'src' => $source, 'latest' => true ]; + + return $this->backend->fileExists( $params ); + } + } + + /** + * 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 False on failure + */ + final protected function fileSha1( $source, array $predicates ) { + if ( isset( $predicates['sha1'][$source] ) ) { + return $predicates['sha1'][$source]; // previous op assures this + } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) { + return false; // previous op assures this + } else { + $params = [ 'src' => $source, 'latest' => true ]; + + return $this->backend->getFileSha1Base36( $params ); + } + } + + /** + * Get the backend this operation is for + * + * @return FileBackendStore + */ + public function getBackend() { + return $this->backend; + } + + /** + * Log a file operation failure and preserve any temp files + * + * @param string $action + */ + final public function logFailure( $action ) { + $params = $this->params; + $params['failedAction'] = $action; + try { + $this->logger->error( get_class( $this ) . + " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); + } catch ( Exception $e ) { + // bad config? debug log error? + } + } +} diff --git a/includes/libs/filebackend/fileop/MoveFileOp.php b/includes/libs/filebackend/fileop/MoveFileOp.php new file mode 100644 index 0000000..fee3f4a --- /dev/null +++ b/includes/libs/filebackend/fileop/MoveFileOp.php @@ -0,0 +1,107 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ + +/** + * Move a file from one storage path to another in the backend. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class MoveFileOp extends FileOp { + protected function allowedParams() { + return [ + [ 'src', 'dst' ], + [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ], + [ 'src', 'dst' ] + ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source file exists + if ( !$this->fileExists( $this->params['src'], $predicates ) ) { + if ( $this->getParam( 'ignoreMissingSource' ) ) { + $this->doOperation = false; // no-op + // Update file existence predicates (cache 404s) + $predicates['exists'][$this->params['src']] = false; + $predicates['sha1'][$this->params['src']] = false; + + return $status; // nothing to do + } else { + $status->fatal( 'backend-fail-notexists', $this->params['src'] ); + + return $status; + } + // Check if a file can be placed/changed at the destination + } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); + $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); + + return $status; + } + // Check if destination file exists + $status->merge( $this->precheckDestExistence( $predicates ) ); + $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() + if ( $status->isOK() ) { + // Update file existence predicates + $predicates['exists'][$this->params['src']] = false; + $predicates['sha1'][$this->params['src']] = false; + $predicates['exists'][$this->params['dst']] = true; + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; + } + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + if ( $this->overwriteSameCase ) { + if ( $this->params['src'] === $this->params['dst'] ) { + // Do nothing to the destination (which is also the source) + $status = StatusValue::newGood(); + } else { + // Just delete the source as the destination file needs no changes + $status = $this->backend->deleteInternal( $this->setFlags( + [ 'src' => $this->params['src'] ] + ) ); + } + } elseif ( $this->params['src'] === $this->params['dst'] ) { + // Just update the destination file headers + $headers = $this->getParam( 'headers' ) ?: []; + $status = $this->backend->describeInternal( $this->setFlags( + [ 'src' => $this->params['dst'], 'headers' => $headers ] + ) ); + } else { + // Move the file to the destination + $status = $this->backend->moveInternal( $this->setFlags( $this->params ) ); + } + + return $status; + } + + public function storagePathsRead() { + return [ $this->params['src'] ]; + } + + public function storagePathsChanged() { + return [ $this->params['src'], $this->params['dst'] ]; + } +} diff --git a/includes/libs/filebackend/fileop/NullFileOp.php b/includes/libs/filebackend/fileop/NullFileOp.php new file mode 100644 index 0000000..ed23e81 --- /dev/null +++ b/includes/libs/filebackend/fileop/NullFileOp.php @@ -0,0 +1,29 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ + +/** + * Placeholder operation that has no params and does nothing + */ +class NullFileOp extends FileOp { +} diff --git a/includes/libs/filebackend/fileop/StoreFileOp.php b/includes/libs/filebackend/fileop/StoreFileOp.php new file mode 100644 index 0000000..b97b410 --- /dev/null +++ b/includes/libs/filebackend/fileop/StoreFileOp.php @@ -0,0 +1,94 @@ +<?php +/** + * Helper class for representing operations with transaction support. + * + * 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 + * @author Aaron Schulz + */ + +/** + * Store a file into the backend from a file on the file system. + * Parameters for this operation are outlined in FileBackend::doOperations(). + */ +class StoreFileOp extends FileOp { + protected function allowedParams() { + return [ + [ 'src', 'dst' ], + [ 'overwrite', 'overwriteSame', 'headers' ], + [ 'src', 'dst' ] + ]; + } + + protected function doPrecheck( array &$predicates ) { + $status = StatusValue::newGood(); + // Check if the source file exists on 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 + } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { + $status->fatal( 'backend-fail-maxsize', + $this->params['dst'], $this->backend->maxFileSizeInternal() ); + $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); + + return $status; + // Check if a file can be placed/changed at the destination + } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); + $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); + + return $status; + } + // Check if destination file exists + $status->merge( $this->precheckDestExistence( $predicates ) ); + $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache() + if ( $status->isOK() ) { + // Update file existence predicates + $predicates['exists'][$this->params['dst']] = true; + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; + } + + return $status; // safe to call attempt() + } + + protected function doAttempt() { + if ( !$this->overwriteSameCase ) { + // Store the file at the destination + return $this->backend->storeInternal( $this->setFlags( $this->params ) ); + } + + return StatusValue::newGood(); + } + + protected function getSourceSha1Base36() { + MediaWiki\suppressWarnings(); + $hash = sha1_file( $this->params['src'] ); + MediaWiki\restoreWarnings(); + if ( $hash !== false ) { + $hash = Wikimedia\base_convert( $hash, 16, 36, 31 ); + } + + return $hash; + } + + public function storagePathsChanged() { + return [ $this->params['dst'] ]; + } +} -- To view, visit https://gerrit.wikimedia.org/r/311359 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: If490f64bec282e5dfcdaf7feb1cbf46d3dce1064 Gerrit-PatchSet: 5 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Aaron Schulz <asch...@wikimedia.org> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits