2011-12-20 03:52:06 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* @file
|
|
|
|
|
* @ingroup FileBackend
|
|
|
|
|
* @author Aaron Schulz
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper class for representing operations with transaction support.
|
|
|
|
|
* FileBackend::doOperations() will require these classes for supported operations.
|
2012-01-13 23:30:46 +00:00
|
|
|
* Do not use this class from places outside FileBackend.
|
2011-12-20 03:52:06 +00:00
|
|
|
*
|
2012-01-13 23:30:46 +00:00
|
|
|
* Use of large fields should be avoided as we want to support
|
2011-12-20 03:52:06 +00:00
|
|
|
* potentially many FileOp classes in large arrays in memory.
|
|
|
|
|
*
|
|
|
|
|
* @ingroup FileBackend
|
|
|
|
|
* @since 1.19
|
|
|
|
|
*/
|
|
|
|
|
abstract class FileOp {
|
|
|
|
|
/** $var Array */
|
|
|
|
|
protected $params = array();
|
|
|
|
|
/** $var FileBackendBase */
|
|
|
|
|
protected $backend;
|
|
|
|
|
/** @var TempFSFile|null */
|
|
|
|
|
protected $tmpSourceFile, $tmpDestFile;
|
|
|
|
|
|
|
|
|
|
protected $state = self::STATE_NEW; // integer
|
|
|
|
|
protected $failed = false; // boolean
|
|
|
|
|
protected $useBackups = true; // boolean
|
2012-01-05 06:18:36 +00:00
|
|
|
protected $useLatest = true; // boolean
|
2011-12-20 03:52:06 +00:00
|
|
|
protected $destSameAsSource = false; // boolean
|
|
|
|
|
protected $destAlreadyExists = false; // boolean
|
|
|
|
|
|
|
|
|
|
/* Object life-cycle */
|
|
|
|
|
const STATE_NEW = 1;
|
|
|
|
|
const STATE_CHECKED = 2;
|
|
|
|
|
const STATE_ATTEMPTED = 3;
|
|
|
|
|
const STATE_DONE = 4;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build a new file operation transaction
|
|
|
|
|
*
|
|
|
|
|
* @params $backend FileBackend
|
|
|
|
|
* @params $params Array
|
|
|
|
|
*/
|
|
|
|
|
final public function __construct( FileBackendBase $backend, array $params ) {
|
|
|
|
|
$this->backend = $backend;
|
|
|
|
|
foreach ( $this->allowedParams() as $name ) {
|
|
|
|
|
if ( isset( $params[$name] ) ) {
|
|
|
|
|
$this->params[$name] = $params[$name];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$this->params = $params;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disable file backups for this operation
|
2012-01-12 19:41:18 +00:00
|
|
|
*
|
|
|
|
|
* @return void
|
2011-12-20 03:52:06 +00:00
|
|
|
*/
|
|
|
|
|
final protected function disableBackups() {
|
|
|
|
|
$this->useBackups = false;
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-05 06:18:36 +00:00
|
|
|
/**
|
|
|
|
|
* Allow stale data for file reads and existence checks.
|
|
|
|
|
* If this is called, then disableBackups() should also be called
|
|
|
|
|
* unless the affected files are known to have not changed recently.
|
2012-01-12 19:41:18 +00:00
|
|
|
*
|
|
|
|
|
* @return void
|
2012-01-05 06:18:36 +00:00
|
|
|
*/
|
|
|
|
|
final protected function allowStaleReads() {
|
|
|
|
|
$this->useLatest = false;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
/**
|
|
|
|
|
* Attempt a series of file operations.
|
|
|
|
|
* Callers are responsible for handling file locking.
|
|
|
|
|
*
|
2012-01-09 21:04:48 +00:00
|
|
|
* $opts is an array of options, including:
|
|
|
|
|
* 'force' : Errors that would normally cause a rollback do not.
|
|
|
|
|
* The remaining operations are still attempted if any fail.
|
|
|
|
|
* 'allowStale' : Don't require the latest available data.
|
|
|
|
|
* This can increase performance for non-critical writes.
|
|
|
|
|
* This has no effect unless the 'force' flag is set.
|
|
|
|
|
*
|
2011-12-20 03:52:06 +00:00
|
|
|
* @param $performOps Array List of FileOp operations
|
|
|
|
|
* @param $opts Array Batch operation options
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
final public static function attemptBatch( array $performOps, array $opts ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
2012-01-05 06:18:36 +00:00
|
|
|
$allowStale = isset( $opts['allowStale'] ) && $opts['allowStale'];
|
2012-01-08 08:40:00 +00:00
|
|
|
$ignoreErrors = isset( $opts['force'] ) && $opts['force'];
|
2011-12-20 03:52:06 +00:00
|
|
|
$predicates = FileOp::newPredicates(); // account for previous op in prechecks
|
|
|
|
|
// Do pre-checks for each operation; abort on failure...
|
|
|
|
|
foreach ( $performOps as $index => $fileOp ) {
|
2012-01-05 06:18:36 +00:00
|
|
|
if ( $allowStale ) {
|
|
|
|
|
$fileOp->allowStaleReads(); // allow potentially stale reads
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->merge( $fileOp->precheck( $predicates ) );
|
|
|
|
|
if ( !$status->isOK() ) { // operation failed?
|
|
|
|
|
if ( $ignoreErrors ) {
|
|
|
|
|
++$status->failCount;
|
|
|
|
|
$status->success[$index] = false;
|
|
|
|
|
} else {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt each operation; abort on failure...
|
|
|
|
|
foreach ( $performOps as $index => $fileOp ) {
|
|
|
|
|
if ( $fileOp->failed() ) {
|
|
|
|
|
continue; // nothing to do
|
|
|
|
|
} elseif ( $ignoreErrors ) {
|
|
|
|
|
$fileOp->disableBackups(); // no chance of revert() calls
|
|
|
|
|
}
|
|
|
|
|
$status->merge( $fileOp->attempt() );
|
|
|
|
|
if ( !$status->isOK() ) { // operation failed?
|
|
|
|
|
if ( $ignoreErrors ) {
|
|
|
|
|
++$status->failCount;
|
|
|
|
|
$status->success[$index] = false;
|
|
|
|
|
} else {
|
|
|
|
|
// Revert everything done so far and abort.
|
|
|
|
|
// Do this by reverting each previous operation in reverse order.
|
|
|
|
|
$pos = $index - 1; // last one failed; no need to revert()
|
|
|
|
|
while ( $pos >= 0 ) {
|
|
|
|
|
if ( !$performOps[$pos]->failed() ) {
|
|
|
|
|
$status->merge( $performOps[$pos]->revert() );
|
|
|
|
|
}
|
|
|
|
|
$pos--;
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-09 21:04:48 +00:00
|
|
|
$wasOk = $status->isOK();
|
2011-12-20 03:52:06 +00:00
|
|
|
// Finish each operation...
|
|
|
|
|
foreach ( $performOps as $index => $fileOp ) {
|
|
|
|
|
if ( $fileOp->failed() ) {
|
|
|
|
|
continue; // nothing to do
|
|
|
|
|
}
|
|
|
|
|
$subStatus = $fileOp->finish();
|
|
|
|
|
if ( $subStatus->isOK() ) {
|
|
|
|
|
++$status->successCount;
|
|
|
|
|
$status->success[$index] = true;
|
|
|
|
|
} else {
|
|
|
|
|
++$status->failCount;
|
|
|
|
|
$status->success[$index] = false;
|
|
|
|
|
}
|
|
|
|
|
$status->merge( $subStatus );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure status is OK, despite any finish() fatals
|
2012-01-09 21:04:48 +00:00
|
|
|
$status->setResult( $wasOk, $status->value );
|
2011-12-20 03:52:06 +00:00
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the value of the parameter with the given name.
|
|
|
|
|
* Returns null if the parameter is not set.
|
|
|
|
|
*
|
|
|
|
|
* @param $name string
|
|
|
|
|
* @return mixed
|
|
|
|
|
*/
|
|
|
|
|
final public function getParam( $name ) {
|
|
|
|
|
return isset( $this->params[$name] ) ? $this->params[$name] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if this operation failed precheck() or attempt()
|
|
|
|
|
* @return type
|
|
|
|
|
*/
|
|
|
|
|
final public function failed() {
|
|
|
|
|
return $this->failed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a new empty predicates array for precheck()
|
|
|
|
|
*
|
|
|
|
|
* @return Array
|
|
|
|
|
*/
|
|
|
|
|
final public static function newPredicates() {
|
|
|
|
|
return array( 'exists' => array() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check preconditions of the operation without writing anything
|
|
|
|
|
*
|
|
|
|
|
* @param $predicates Array
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
final public function precheck( array &$predicates ) {
|
|
|
|
|
if ( $this->state !== self::STATE_NEW ) {
|
|
|
|
|
return Status::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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempt the operation, backing up files as needed; this must be reversible
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
final public function attempt() {
|
|
|
|
|
if ( $this->state !== self::STATE_CHECKED ) {
|
|
|
|
|
return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
|
|
|
|
|
} elseif ( $this->failed ) { // failed precheck
|
|
|
|
|
return Status::newFatal( 'fileop-fail-attempt-precheck' );
|
|
|
|
|
}
|
|
|
|
|
$this->state = self::STATE_ATTEMPTED;
|
|
|
|
|
$status = $this->doAttempt();
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
$this->failed = true;
|
|
|
|
|
$this->logFailure( 'attempt' );
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Revert the operation; affected files are restored
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
final public function revert() {
|
|
|
|
|
if ( $this->state !== self::STATE_ATTEMPTED ) {
|
|
|
|
|
return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state );
|
|
|
|
|
}
|
|
|
|
|
$this->state = self::STATE_DONE;
|
|
|
|
|
if ( $this->failed ) {
|
|
|
|
|
$status = Status::newGood(); // nothing to revert
|
|
|
|
|
} else {
|
|
|
|
|
$status = $this->doRevert();
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
$this->logFailure( 'revert' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finish the operation; this may be irreversible
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
final public function finish() {
|
|
|
|
|
if ( $this->state !== self::STATE_ATTEMPTED ) {
|
|
|
|
|
return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state );
|
|
|
|
|
}
|
|
|
|
|
$this->state = self::STATE_DONE;
|
|
|
|
|
if ( $this->failed ) {
|
|
|
|
|
$status = Status::newGood(); // nothing to finish
|
|
|
|
|
} else {
|
|
|
|
|
$status = $this->doFinish();
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of storage paths read from for this operation
|
|
|
|
|
*
|
|
|
|
|
* @return Array
|
|
|
|
|
*/
|
|
|
|
|
public function storagePathsRead() {
|
|
|
|
|
return array();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of storage paths written to for this operation
|
|
|
|
|
*
|
|
|
|
|
* @return Array
|
|
|
|
|
*/
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return Array List of allowed parameters
|
|
|
|
|
*/
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function doPrecheck( array &$predicates ) {
|
|
|
|
|
return Status::newGood();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function doAttempt();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function doRevert();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function doFinish() {
|
|
|
|
|
return Status::newGood();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the destination file exists and update the
|
|
|
|
|
* destAlreadyExists member variable. A bad status will
|
|
|
|
|
* be returned if there is no chance it can be overwritten.
|
|
|
|
|
*
|
|
|
|
|
* @param $predicates Array
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function precheckDestExistence( array $predicates ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( $this->fileExists( $this->params['dst'], $predicates ) ) {
|
|
|
|
|
$this->destAlreadyExists = true;
|
|
|
|
|
if ( !$this->getParam( 'overwriteDest' ) && !$this->getParam( 'overwriteSame' ) ) {
|
|
|
|
|
$status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$this->destAlreadyExists = false;
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Backup any file at the source to a temporary file
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function backupSource() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( $this->useBackups ) {
|
|
|
|
|
// Check if a file already exists at the source...
|
2012-01-05 06:18:36 +00:00
|
|
|
$params = array( 'src' => $this->params['src'], 'latest' => $this->useLatest );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( $this->backend->fileExists( $params ) ) {
|
|
|
|
|
// Create a temporary backup copy...
|
|
|
|
|
$this->tmpSourcePath = $this->backend->getLocalCopy( $params );
|
|
|
|
|
if ( $this->tmpSourcePath === null ) {
|
|
|
|
|
$status->fatal( 'backend-fail-backup', $this->params['src'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Backup the file at the destination to a temporary file.
|
|
|
|
|
* Don't bother backing it up unless we might overwrite the file.
|
|
|
|
|
* This assumes that the destination is in the backend and that
|
|
|
|
|
* the source is either in the backend or on the file system.
|
|
|
|
|
* This also handles the 'overwriteSame' check logic and updates
|
|
|
|
|
* the destSameAsSource member variable.
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function checkAndBackupDest() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
$this->destSameAsSource = false;
|
|
|
|
|
|
|
|
|
|
if ( $this->getParam( 'overwriteDest' ) ) {
|
|
|
|
|
if ( $this->useBackups ) {
|
|
|
|
|
// Create a temporary backup copy...
|
2012-01-05 06:18:36 +00:00
|
|
|
$params = array( 'src' => $this->params['dst'], 'latest' => $this->useLatest );
|
2011-12-20 03:52:06 +00:00
|
|
|
$this->tmpDestFile = $this->backend->getLocalCopy( $params );
|
|
|
|
|
if ( !$this->tmpDestFile ) {
|
|
|
|
|
$status->fatal( 'backend-fail-backup', $this->params['dst'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $this->getParam( 'overwriteSame' ) ) {
|
|
|
|
|
$shash = $this->getSourceSha1Base36();
|
|
|
|
|
// If there is a single source, then we can do some checks already.
|
|
|
|
|
// For things like concatenate(), we would need to build a temp file
|
|
|
|
|
// first and thus don't support 'overwriteSame' ($shash is null).
|
|
|
|
|
if ( $shash !== null ) {
|
|
|
|
|
$dhash = $this->getFileSha1Base36( $this->params['dst'] );
|
|
|
|
|
if ( !strlen( $shash ) || !strlen( $dhash ) ) {
|
|
|
|
|
$status->fatal( 'backend-fail-hashes' );
|
|
|
|
|
} elseif ( $shash !== $dhash ) {
|
|
|
|
|
// Give an error if the files are not identical
|
|
|
|
|
$status->fatal( 'backend-fail-notsame', $this->params['dst'] );
|
|
|
|
|
} else {
|
|
|
|
|
$this->destSameAsSource = true;
|
|
|
|
|
}
|
|
|
|
|
return $status; // do nothing; either OK or bad status
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* checkAndBackupDest() helper function to get the source file Sha1.
|
|
|
|
|
* Returns false on failure and null if there is no single source.
|
|
|
|
|
*
|
|
|
|
|
* @return string|false|null
|
|
|
|
|
*/
|
|
|
|
|
protected function getSourceSha1Base36() {
|
|
|
|
|
return null; // N/A
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* checkAndBackupDest() helper function to get the Sha1 of a file.
|
|
|
|
|
*
|
|
|
|
|
* @return string|false False on failure
|
|
|
|
|
*/
|
|
|
|
|
protected function getFileSha1Base36( $path ) {
|
|
|
|
|
// Source file is in backend
|
|
|
|
|
if ( FileBackend::isStoragePath( $path ) ) {
|
|
|
|
|
// For some backends (e.g. Swift, Azure) we can get
|
|
|
|
|
// standard hashes to use for this types of comparisons.
|
2012-01-05 06:18:36 +00:00
|
|
|
$params = array( 'src' => $path, 'latest' => $this->useLatest );
|
|
|
|
|
$hash = $this->backend->getFileSha1Base36( $params );
|
2011-12-20 03:52:06 +00:00
|
|
|
// Source file is on file system
|
|
|
|
|
} else {
|
|
|
|
|
wfSuppressWarnings();
|
|
|
|
|
$hash = sha1_file( $path );
|
|
|
|
|
wfRestoreWarnings();
|
|
|
|
|
if ( $hash !== false ) {
|
|
|
|
|
$hash = wfBaseConvert( $hash, 16, 36, 31 );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $hash;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Restore any temporary source backup file
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function restoreSource() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Restore any file that was at the destination
|
|
|
|
|
if ( $this->tmpSourcePath !== null ) {
|
|
|
|
|
$params = array(
|
|
|
|
|
'src' => $this->tmpSourcePath,
|
|
|
|
|
'dst' => $this->params['src'],
|
|
|
|
|
'overwriteDest' => true
|
|
|
|
|
);
|
2011-12-21 09:16:28 +00:00
|
|
|
$status = $this->backend->storeInternal( $params );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Restore any temporary destination backup file
|
|
|
|
|
*
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
|
|
|
|
protected function restoreDest() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Restore any file that was at the destination
|
|
|
|
|
if ( $this->tmpDestFile ) {
|
|
|
|
|
$params = array(
|
|
|
|
|
'src' => $this->tmpDestFile->getPath(),
|
|
|
|
|
'dst' => $this->params['dst'],
|
|
|
|
|
'overwriteDest' => true
|
|
|
|
|
);
|
2011-12-21 09:16:28 +00:00
|
|
|
$status = $this->backend->storeInternal( $params );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a file will exist in storage when this operation is attempted
|
|
|
|
|
*
|
|
|
|
|
* @param $source string Storage path
|
|
|
|
|
* @param $predicates Array
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
final protected function fileExists( $source, array $predicates ) {
|
|
|
|
|
if ( isset( $predicates['exists'][$source] ) ) {
|
|
|
|
|
return $predicates['exists'][$source]; // previous op assures this
|
|
|
|
|
} else {
|
2012-01-05 06:18:36 +00:00
|
|
|
$params = array( 'src' => $source, 'latest' => $this->useLatest );
|
|
|
|
|
return $this->backend->fileExists( $params );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Log a file operation failure and preserve any temp files
|
|
|
|
|
*
|
|
|
|
|
* @param $fileOp FileOp
|
2012-01-12 19:41:18 +00:00
|
|
|
* @return void
|
2011-12-20 03:52:06 +00:00
|
|
|
*/
|
|
|
|
|
final protected function logFailure( $action ) {
|
|
|
|
|
$params = $this->params;
|
|
|
|
|
$params['failedAction'] = $action;
|
|
|
|
|
// Preserve backup files just in case (for recovery)
|
|
|
|
|
if ( $this->tmpSourceFile ) {
|
|
|
|
|
$this->tmpSourceFile->preserve(); // don't purge
|
|
|
|
|
$params['srcBackupPath'] = $this->tmpSourceFile->getPath();
|
|
|
|
|
}
|
|
|
|
|
if ( $this->tmpDestFile ) {
|
|
|
|
|
$this->tmpDestFile->preserve(); // don't purge
|
|
|
|
|
$params['dstBackupPath'] = $this->tmpDestFile->getPath();
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
wfDebugLog( 'FileOperation',
|
|
|
|
|
get_class( $this ) . ' failed:' . serialize( $params ) );
|
|
|
|
|
} catch ( Exception $e ) {
|
|
|
|
|
// bad config? debug log error?
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store a file into the backend from a file on the file system.
|
2011-12-21 09:16:28 +00:00
|
|
|
* Parameters similar to FileBackend::storeInternal(), which include:
|
2011-12-20 03:52:06 +00:00
|
|
|
* src : source path on file system
|
|
|
|
|
* dst : destination storage path
|
|
|
|
|
* overwriteDest : do nothing and pass if an identical file exists at destination
|
|
|
|
|
* overwriteSame : override any existing file at destination
|
|
|
|
|
*/
|
|
|
|
|
class StoreFileOp extends FileOp {
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doPrecheck( array &$predicates ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Check if destination file exists
|
|
|
|
|
$status->merge( $this->precheckDestExistence( $predicates ) );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
// Update file existence predicates
|
|
|
|
|
$predicates['exists'][$this->params['dst']] = true;
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Create a destination backup copy as needed
|
|
|
|
|
if ( $this->destAlreadyExists ) {
|
|
|
|
|
$status->merge( $this->checkAndBackupDest() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Store the file at the destination
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->storeInternal( $this->params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
|
|
|
|
// Restore any file that was at the destination,
|
|
|
|
|
// overwritting what was put there in attempt()
|
|
|
|
|
$status->merge( $this->restoreDest() );
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getSourceSha1Base36() {
|
|
|
|
|
return $this->getFileSha1Base36( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array( $this->params['dst'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a file in the backend with the given content.
|
|
|
|
|
* Parameters similar to FileBackend::create(), which include:
|
|
|
|
|
* content : a string of raw file contents
|
|
|
|
|
* dst : destination storage path
|
|
|
|
|
* overwriteDest : do nothing and pass if an identical file exists at destination
|
|
|
|
|
* overwriteSame : override any existing file at destination
|
|
|
|
|
*/
|
|
|
|
|
class CreateFileOp extends FileOp {
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array( 'content', 'dst', 'overwriteDest', 'overwriteSame' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doPrecheck( array &$predicates ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Check if destination file exists
|
|
|
|
|
$status->merge( $this->precheckDestExistence( $predicates ) );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Update file existence predicates
|
|
|
|
|
$predicates['exists'][$this->params['dst']] = true;
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Create a destination backup copy as needed
|
|
|
|
|
if ( $this->destAlreadyExists ) {
|
|
|
|
|
$status->merge( $this->checkAndBackupDest() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Create the file at the destination
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->createInternal( $this->params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
|
|
|
|
// Restore any file that was at the destination,
|
|
|
|
|
// overwritting what was put there in attempt()
|
|
|
|
|
$status->merge( $this->restoreDest() );
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getSourceSha1Base36() {
|
|
|
|
|
return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array( $this->params['dst'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Copy a file from one storage path to another in the backend.
|
|
|
|
|
* Parameters similar to FileBackend::copy(), which include:
|
|
|
|
|
* src : source storage path
|
|
|
|
|
* dst : destination storage path
|
|
|
|
|
* overwriteDest : do nothing and pass if an identical file exists at destination
|
|
|
|
|
* overwriteSame : override any existing file at destination
|
|
|
|
|
*/
|
|
|
|
|
class CopyFileOp extends FileOp {
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doPrecheck( array &$predicates ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Check if destination file exists
|
|
|
|
|
$status->merge( $this->precheckDestExistence( $predicates ) );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Check if the source file exists
|
|
|
|
|
if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
|
|
|
|
|
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Update file existence predicates
|
|
|
|
|
$predicates['exists'][$this->params['dst']] = true;
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Create a destination backup copy as needed
|
|
|
|
|
if ( $this->destAlreadyExists ) {
|
|
|
|
|
$status->merge( $this->checkAndBackupDest() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Copy the file into the destination
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->copyInternal( $this->params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
|
|
|
|
// Restore any file that was at the destination,
|
|
|
|
|
// overwritting what was put there in attempt()
|
|
|
|
|
$status->merge( $this->restoreDest() );
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getSourceSha1Base36() {
|
|
|
|
|
return $this->getFileSha1Base36( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsRead() {
|
|
|
|
|
return array( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array( $this->params['dst'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move a file from one storage path to another in the backend.
|
|
|
|
|
* Parameters similar to FileBackend::move(), which include:
|
|
|
|
|
* src : source storage path
|
|
|
|
|
* dst : destination storage path
|
|
|
|
|
* overwriteDest : do nothing and pass if an identical file exists at destination
|
|
|
|
|
* overwriteSame : override any existing file at destination
|
|
|
|
|
*/
|
|
|
|
|
class MoveFileOp extends FileOp {
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array( 'src', 'dst', 'overwriteDest', 'overwriteSame' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doPrecheck( array &$predicates ) {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Check if destination file exists
|
|
|
|
|
$status->merge( $this->precheckDestExistence( $predicates ) );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Check if the source file exists
|
|
|
|
|
if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
|
|
|
|
|
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Update file existence predicates
|
|
|
|
|
$predicates['exists'][$this->params['src']] = false;
|
|
|
|
|
$predicates['exists'][$this->params['dst']] = true;
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
// Create a destination backup copy as needed
|
|
|
|
|
if ( $this->destAlreadyExists ) {
|
|
|
|
|
$status->merge( $this->checkAndBackupDest() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
|
|
|
|
// Move the file into the destination
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->moveInternal( $this->params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
} else {
|
|
|
|
|
// Create a source backup copy as needed
|
|
|
|
|
$status->merge( $this->backupSource() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Just delete source as the destination needs no changes
|
|
|
|
|
$params = array( 'src' => $this->params['src'] );
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->deleteInternal( $params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( !$this->destSameAsSource ) {
|
|
|
|
|
// Move the file back to the source
|
|
|
|
|
$params = array(
|
|
|
|
|
'src' => $this->params['dst'],
|
|
|
|
|
'dst' => $this->params['src']
|
|
|
|
|
);
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->moveInternal( $params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status; // also can't restore any dest file
|
|
|
|
|
}
|
|
|
|
|
// Restore any file that was at the destination
|
|
|
|
|
$status->merge( $this->restoreDest() );
|
|
|
|
|
} else {
|
|
|
|
|
// Restore any source file
|
|
|
|
|
return $this->restoreSource();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getSourceSha1Base36() {
|
|
|
|
|
return $this->getFileSha1Base36( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsRead() {
|
|
|
|
|
return array( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array( $this->params['dst'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a file at the storage path.
|
|
|
|
|
* Parameters similar to FileBackend::delete(), which include:
|
|
|
|
|
* src : source storage path
|
|
|
|
|
* ignoreMissingSource : don't return an error if the file does not exist
|
|
|
|
|
*/
|
|
|
|
|
class DeleteFileOp extends FileOp {
|
|
|
|
|
protected $needsDelete = true;
|
|
|
|
|
|
|
|
|
|
protected function allowedParams() {
|
|
|
|
|
return array( 'src', 'ignoreMissingSource' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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' ) ) {
|
|
|
|
|
$status->fatal( 'backend-fail-notexists', $this->params['src'] );
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
$this->needsDelete = false;
|
|
|
|
|
}
|
|
|
|
|
// Update file existence predicates
|
|
|
|
|
$predicates['exists'][$this->params['src']] = false;
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
$status = Status::newGood();
|
|
|
|
|
if ( $this->needsDelete ) {
|
|
|
|
|
// Create a source backup copy as needed
|
|
|
|
|
$status->merge( $this->backupSource() );
|
|
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
// Delete the source file
|
2011-12-21 09:16:28 +00:00
|
|
|
$status->merge( $this->backend->deleteInternal( $this->params ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
// Restore any source file that we deleted
|
|
|
|
|
return $this->restoreSource();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function storagePathsChanged() {
|
|
|
|
|
return array( $this->params['src'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Placeholder operation that has no params and does nothing
|
|
|
|
|
*/
|
|
|
|
|
class NullFileOp extends FileOp {
|
|
|
|
|
protected function doAttempt() {
|
|
|
|
|
return Status::newGood();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRevert() {
|
|
|
|
|
return Status::newGood();
|
|
|
|
|
}
|
|
|
|
|
}
|