diff options
Diffstat (limited to 'includes/filebackend/FileOp.php')
-rw-r--r-- | includes/filebackend/FileOp.php | 332 |
1 files changed, 215 insertions, 117 deletions
diff --git a/includes/filebackend/FileOp.php b/includes/filebackend/FileOp.php index 7c43c489..bb0ab578 100644 --- a/includes/filebackend/FileOp.php +++ b/includes/filebackend/FileOp.php @@ -42,11 +42,12 @@ abstract class FileOp { protected $state = self::STATE_NEW; // integer protected $failed = false; // boolean protected $async = false; // boolean - protected $useLatest = true; // boolean protected $batchId; // string + protected $doOperation = true; // boolean; operation is not a no-op protected $sourceSha1; // string protected $destSameAsSource; // boolean + protected $destExists; // boolean /* Object life-cycle */ const STATE_NEW = 1; @@ -65,37 +66,61 @@ abstract class FileOp { list( $required, $optional ) = $this->allowedParams(); foreach ( $required as $name ) { if ( isset( $params[$name] ) ) { - $this->params[$name] = $params[$name]; + $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] ); } else { throw new MWException( "File operation missing parameter '$name'." ); } } foreach ( $optional as $name ) { if ( isset( $params[$name] ) ) { - $this->params[$name] = $params[$name]; + $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] ); } } $this->params = $params; } /** - * Set the batch UUID this operation belongs to + * Normalize $item or anything in $item that is a valid storage path * - * @param $batchId string - * @return void + * @param $item string|array + * @return string|Array + */ + protected function normalizeAnyStoragePaths( $item ) { + if ( is_array( $item ) ) { + $res = array(); + foreach ( $item as $k => $v ) { + $k = self::normalizeIfValidStoragePath( $k ); + $v = self::normalizeIfValidStoragePath( $v ); + $res[$k] = $v; + } + return $res; + } else { + return self::normalizeIfValidStoragePath( $item ); + } + } + + /** + * Normalize a string if it is a valid storage path + * + * @param $path string + * @return string */ - final public function setBatchId( $batchId ) { - $this->batchId = $batchId; + protected static function normalizeIfValidStoragePath( $path ) { + if ( FileBackend::isStoragePath( $path ) ) { + $res = FileBackend::normalizeStoragePath( $path ); + return ( $res !== null ) ? $res : $path; + } + return $path; } /** - * Whether to allow stale data for file reads and stat checks + * Set the batch UUID this operation belongs to * - * @param $allowStale bool + * @param $batchId string * @return void */ - final public function allowStaleReads( $allowStale ) { - $this->useLatest = !$allowStale; + final public function setBatchId( $batchId ) { + $this->batchId = $batchId; } /** @@ -138,11 +163,11 @@ abstract class FileOp { /** * Update a dependency tracking array to account for this operation * - * @param $deps Array Prior path reads/writes; format of FileOp::newPredicates() + * @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['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); return $deps; } @@ -150,7 +175,7 @@ abstract class FileOp { /** * Check if this operation changes files listed in $paths * - * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates() + * @param array $paths Prior path reads/writes; format of FileOp::newPredicates() * @return boolean */ final public function dependsOn( array $deps ) { @@ -170,16 +195,19 @@ abstract class FileOp { /** * Get the file journal entries for this file operation * - * @param $oPredicates Array Pre-op info about files (format of FileOp::newPredicates) - * @param $nPredicates Array Post-op info about files (format of FileOp::newPredicates) + * @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 array(); // this is a no-op + } $nullEntries = array(); $updateEntries = array(); $deleteEntries = array(); $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); - foreach ( $pathsUsed as $path ) { + foreach ( array_unique( $pathsUsed ) as $path ) { $nullEntries[] = array( // assertion for recovery 'op' => 'null', 'path' => $path, @@ -205,7 +233,9 @@ abstract class FileOp { } /** - * Check preconditions of the operation without writing anything + * Check preconditions of the operation without writing anything. + * This must update $predicates for each path that the op can change + * except when a failing status object is returned. * * @param $predicates Array * @return Status @@ -241,10 +271,14 @@ abstract class FileOp { return Status::newFatal( 'fileop-fail-attempt-precheck' ); } $this->state = self::STATE_ATTEMPTED; - $status = $this->doAttempt(); - if ( !$status->isOK() ) { - $this->failed = true; - $this->logFailure( 'attempt' ); + if ( $this->doOperation ) { + $status = $this->doAttempt(); + if ( !$status->isOK() ) { + $this->failed = true; + $this->logFailure( 'attempt' ); + } + } else { // no-op + $status = Status::newGood(); } return $status; } @@ -292,15 +326,7 @@ abstract class FileOp { * * @return Array */ - final public function storagePathsRead() { - return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() ); - } - - /** - * @see FileOp::storagePathsRead() - * @return Array - */ - protected function doStoragePathsRead() { + public function storagePathsRead() { return array(); } @@ -309,21 +335,13 @@ abstract class FileOp { * * @return Array */ - final public function storagePathsChanged() { - return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() ); - } - - /** - * @see FileOp::storagePathsChanged() - * @return Array - */ - protected function doStoragePathsChanged() { + public function storagePathsChanged() { return array(); } /** * Check for errors with regards to the destination file already existing. - * This also updates the destSameAsSource and sourceSha1 member variables. + * Also set the destExists, destSameAsSource and sourceSha1 member variables. * A bad status will be returned if there is no chance it can be overwritten. * * @param $predicates Array @@ -337,7 +355,8 @@ abstract class FileOp { $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); } $this->destSameAsSource = false; - if ( $this->fileExists( $this->params['dst'], $predicates ) ) { + $this->destExists = $this->fileExists( $this->params['dst'], $predicates ); + if ( $this->destExists ) { if ( $this->getParam( 'overwrite' ) ) { return $status; // OK } elseif ( $this->getParam( 'overwriteSame' ) ) { @@ -373,7 +392,7 @@ abstract class FileOp { /** * Check if a file will exist in storage when this operation is attempted * - * @param $source string Storage path + * @param string $source Storage path * @param $predicates Array * @return bool */ @@ -381,7 +400,7 @@ abstract class FileOp { if ( isset( $predicates['exists'][$source] ) ) { return $predicates['exists'][$source]; // previous op assures this } else { - $params = array( 'src' => $source, 'latest' => $this->useLatest ); + $params = array( 'src' => $source, 'latest' => true ); return $this->backend->fileExists( $params ); } } @@ -389,15 +408,17 @@ abstract class FileOp { /** * Get the SHA-1 of a file in storage when this operation is attempted * - * @param $source string Storage path + * @param string $source Storage path * @param $predicates Array * @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 = array( 'src' => $source, 'latest' => $this->useLatest ); + $params = array( 'src' => $source, 'latest' => true ); return $this->backend->getFileSha1Base36( $params ); } } @@ -430,42 +451,32 @@ abstract class FileOp { } /** - * Store a file into the backend from a file on the file system. + * Create a file in the backend with the given content. * Parameters for this operation are outlined in FileBackend::doOperations(). */ -class StoreFileOp extends FileOp { - /** - * @return array - */ +class CreateFileOp extends FileOp { protected function allowedParams() { - return array( array( 'src', 'dst' ), - array( 'overwrite', 'overwriteSame', 'disposition' ) ); + return array( array( 'content', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) ); } - /** - * @param $predicates array - * @return Status - */ protected function doPrecheck( array &$predicates ) { $status = Status::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() ) { + // 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-store', $this->params['src'], $this->params['dst'] ); + $status->fatal( 'backend-fail-create', $this->params['dst'] ); return $status; - // Check if a file can be placed at the destination + // 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'] ); + $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; @@ -478,57 +489,66 @@ class StoreFileOp extends FileOp { * @return Status */ protected function doAttempt() { - // Store the file at the destination if ( !$this->destSameAsSource ) { - return $this->backend->storeInternal( $this->setFlags( $this->params ) ); + // Create the file at the destination + return $this->backend->createInternal( $this->setFlags( $this->params ) ); } return Status::newGood(); } /** - * @return bool|string + * @return bool|String */ protected function getSourceSha1Base36() { - wfSuppressWarnings(); - $hash = sha1_file( $this->params['src'] ); - wfRestoreWarnings(); - if ( $hash !== false ) { - $hash = wfBaseConvert( $hash, 16, 36, 31 ); - } - return $hash; + return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); } - protected function doStoragePathsChanged() { + /** + * @return array + */ + public function storagePathsChanged() { return array( $this->params['dst'] ); } } /** - * Create a file in the backend with the given content. + * Store a file into the backend from a file on the file system. * Parameters for this operation are outlined in FileBackend::doOperations(). */ -class CreateFileOp extends FileOp { +class StoreFileOp extends FileOp { + /** + * @return array + */ protected function allowedParams() { - return array( array( 'content', 'dst' ), - array( 'overwrite', 'overwriteSame', 'disposition' ) ); + return array( array( 'src', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) ); } + /** + * @param $predicates array + * @return Status + */ protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); - // Check if the source data is too big - if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { + // 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-create', $this->params['dst'] ); + $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); return $status; - // Check if a file can be placed at the destination + // 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'] ); + $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; @@ -541,24 +561,27 @@ class CreateFileOp extends FileOp { * @return Status */ protected function doAttempt() { + // Store the file at the destination if ( !$this->destSameAsSource ) { - // Create the file at the destination - return $this->backend->createInternal( $this->setFlags( $this->params ) ); + return $this->backend->storeInternal( $this->setFlags( $this->params ) ); } return Status::newGood(); } /** - * @return bool|String + * @return bool|string */ protected function getSourceSha1Base36() { - return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); + wfSuppressWarnings(); + $hash = sha1_file( $this->params['src'] ); + wfRestoreWarnings(); + if ( $hash !== false ) { + $hash = wfBaseConvert( $hash, 16, 36, 31 ); + } + return $hash; } - /** - * @return array - */ - protected function doStoragePathsChanged() { + public function storagePathsChanged() { return array( $this->params['dst'] ); } } @@ -573,7 +596,7 @@ class CopyFileOp extends FileOp { */ protected function allowedParams() { return array( array( 'src', 'dst' ), - array( 'overwrite', 'overwriteSame', 'disposition' ) ); + array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); } /** @@ -584,9 +607,17 @@ class CopyFileOp extends FileOp { $status = Status::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 at the destination + 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'] ); @@ -594,6 +625,7 @@ class CopyFileOp extends FileOp { } // 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; @@ -619,14 +651,14 @@ class CopyFileOp extends FileOp { /** * @return array */ - protected function doStoragePathsRead() { + public function storagePathsRead() { return array( $this->params['src'] ); } /** * @return array */ - protected function doStoragePathsChanged() { + public function storagePathsChanged() { return array( $this->params['dst'] ); } } @@ -641,7 +673,7 @@ class MoveFileOp extends FileOp { */ protected function allowedParams() { return array( array( 'src', 'dst' ), - array( 'overwrite', 'overwriteSame', 'disposition' ) ); + array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) ); } /** @@ -652,9 +684,17 @@ class MoveFileOp extends FileOp { $status = Status::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 at the destination + 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'] ); @@ -662,6 +702,7 @@ class MoveFileOp extends FileOp { } // 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; @@ -693,14 +734,14 @@ class MoveFileOp extends FileOp { /** * @return array */ - protected function doStoragePathsRead() { + public function storagePathsRead() { return array( $this->params['src'] ); } /** * @return array */ - protected function doStoragePathsChanged() { + public function storagePathsChanged() { return array( $this->params['src'], $this->params['dst'] ); } } @@ -717,21 +758,29 @@ class DeleteFileOp extends FileOp { return array( array( 'src' ), array( 'ignoreMissingSource' ) ); } - protected $needsDelete = true; - /** - * @param array $predicates + * @param $predicates array * @return Status */ protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source file exists if ( !$this->fileExists( $this->params['src'], $predicates ) ) { - if ( !$this->getParam( 'ignoreMissingSource' ) ) { + 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; } - $this->needsDelete = false; + // 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; @@ -743,17 +792,66 @@ class DeleteFileOp extends FileOp { * @return Status */ protected function doAttempt() { - if ( $this->needsDelete ) { - // Delete the source file - return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); + // Delete the source file + return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); + } + + /** + * @return array + */ + public function storagePathsChanged() { + return array( $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 { + /** + * @return array + */ + protected function allowedParams() { + return array( array( 'src' ), array( 'disposition', 'headers' ) ); + } + + /** + * @param $predicates array + * @return Status + */ + protected function doPrecheck( array &$predicates ) { + $status = Status::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; } - return Status::newGood(); + // 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() + } + + /** + * @return Status + */ + protected function doAttempt() { + // Update the source file's metadata + return $this->backend->describeInternal( $this->setFlags( $this->params ) ); } /** * @return array */ - protected function doStoragePathsChanged() { + public function storagePathsChanged() { return array( $this->params['src'] ); } } |