* This lets callers use "copy if exist" semantics more easily and avoids extra stat
queries to the backend (since the cache is cleared before doOperations()).
* Tweaked FileOp::fileSha1() to reduce backend stat requests as 404s are not cached.
Change-Id: Icb5ca14b3316f273d53593f48979d14e113990e1
return $status;
}
+ if ( !is_file( $source ) ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'] );
+ }
+ return $status; // do nothing; either OK or bad status
+ }
+
if ( file_exists( $dest ) ) {
$ok = unlink( $dest );
if ( !$ok ) {
return $status;
}
+ if ( !is_file( $source ) ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'] );
+ }
+ return $status; // do nothing; either OK or bad status
+ }
+
if ( file_exists( $dest ) ) {
// Windows does not support moving over existing files
if ( wfIsWindows() ) {
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
* 'disposition' => <Content-Disposition header value>
* )
* @endcode
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
* 'disposition' => <Content-Disposition header value>
* )
* @endcode
* 'op' => 'copy',
* 'src' => <storage path>,
* 'dst' => <storage path>,
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
* 'disposition' => <Content-Disposition header value>
* )
* @endcode
* 'op' => 'move',
* 'src' => <storage path>,
* 'dst' => <storage path>,
+ * 'ignoreMissingSource' => <boolean>, # since 1.21
* 'disposition' => <Content-Disposition header value>
* )
* @endcode
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - src : source storage path
+ * - dst : destination storage path
+ * - ignoreMissingSource : do nothing if the source file does not exist
+ * - disposition : Content-Disposition header value for the destination
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
* Do not call this function from places outside FileBackend and FileOp.
*
* $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - disposition : Content-Disposition header value for the destination
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
+ * - src : source storage path
+ * - dst : destination storage path
+ * - ignoreMissingSource : do nothing if the source file does not exist
+ * - disposition : Content-Disposition header value for the destination
+ * - async : Status will be returned immediately if supported.
+ * If the status is OK, then its value field will be
+ * set to a FileBackendStoreOpHandle object.
*
* @param $params Array
* @return Status
protected $useLatest = true; // boolean
protected $batchId; // string
+ protected $doOperation = true; // boolean; operation is not a no-op
protected $sourceSha1; // string
protected $destSameAsSource; // boolean
* @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();
}
/**
- * 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
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;
}
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 );
return $this->backend->getFileSha1Base36( $params );
*/
protected function allowedParams() {
return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
}
/**
$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;
+ 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 at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
*/
protected function allowedParams() {
return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
}
/**
$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;
+ 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 at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
$status->fatal( 'backend-fail-usable', $this->params['dst'] );
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;
}
// Update file existence predicates
$predicates['exists'][$this->params['src']] = false;
* @return Status
*/
protected function doAttempt() {
- if ( $this->needsDelete ) {
- // Delete the source file
- return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
- }
- return Status::newGood();
+ // Delete the source file
+ return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
}
/**
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
} catch ( NoSuchObjectException $e ) { // source object does not exist
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
} catch ( NoSuchObjectException $e ) { // source object does not exist
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ }
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, $status, __METHOD__, $params );
}
$this->prepare( array( 'dir' => dirname( $source ) ) );
$this->prepare( array( 'dir' => dirname( $dest ) ) );
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+ return; // done
+ }
+
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
$this->assertGoodStatus( $status,
$dest, // dest
);
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
return $cases;
}
$this->prepare( array( 'dir' => dirname( $source ) ) );
$this->prepare( array( 'dir' => dirname( $dest ) ) );
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+ return; // done
+ }
+
$status = $this->backend->doOperation(
array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
$this->assertGoodStatus( $status,
$dest, // dest
);
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
return $cases;
}