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 );
}
return $this->newFatal( 'directorycreateerror', $archiveDir );
}
- // Archive destination file if it exists
- if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) {
- // Check if the archive file exists
- // This is a sanity check to avoid data loss. In UNIX, the rename primitive
- // unlinks the destination file if it exists. DB-based synchronisation in
- // publishBatch's caller should prevent races. In Windows there's no
- // problem because the rename primitive fails if the destination exists.
- if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) {
- $operations[] = array( 'op' => 'null' );
- continue;
- } else {
- $operations[] = array(
- 'op' => 'move',
- 'src' => $dstPath,
- 'dst' => $archivePath
- );
- }
- $status->value[$i] = 'archived';
- } else {
- $status->value[$i] = 'new';
- }
+ // Archive destination file if it exists.
+ // This will check if the archive file also exists and fail if does.
+ // This is a sanity check to avoid data loss. On Windows and Linux,
+ // copy() will overwrite, so the existence check is vulnerable to
+ // race conditions unless an functioning LockManager is used.
+ // LocalFile also uses SELECT FOR UPDATE for synchronization.
+ $operations[] = array(
+ 'op' => 'copy',
+ 'src' => $dstPath,
+ 'dst' => $archivePath,
+ 'ignoreMissingSource' => true
+ );
+
// Copy (or move) the source file to the destination
if ( FileBackend::isStoragePath( $srcPath ) ) {
if ( $flags & self::DELETE_SOURCE ) {
$operations[] = array(
'op' => 'move',
'src' => $srcPath,
- 'dst' => $dstPath
+ 'dst' => $dstPath,
+ 'overwrite' => true // replace current
);
} else {
$operations[] = array(
'op' => 'copy',
'src' => $srcPath,
- 'dst' => $dstPath
+ 'dst' => $dstPath,
+ 'overwrite' => true // replace current
);
}
} else { // FS source path
$operations[] = array(
'op' => 'store',
'src' => $srcPath,
- 'dst' => $dstPath
+ 'dst' => $dstPath,
+ 'overwrite' => true // replace current
);
if ( $flags & self::DELETE_SOURCE ) {
$sourceFSFilesToDelete[] = $srcPath;
}
// Execute the operations for each triplet
- $opts = array( 'force' => true );
- $status->merge( $backend->doOperations( $operations, $opts ) );
+ $status->merge( $backend->doOperations( $operations ) );
+ // Find out which files were archived...
+ foreach ( $triplets as $i => $triplet ) {
+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+ $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
+ if ( $this->fileExists( $archivePath ) ) {
+ $status->value[$i] = 'archived';
+ } else {
+ $status->value[$i] = 'new';
+ }
+ }
// Cleanup for disk source files...
foreach ( $sourceFSFilesToDelete as $file ) {
wfSuppressWarnings();
protected function doPop() {
global $wgMemc;
- $uuid = wfRandomString( 32 ); // pop attempt
+ if ( $wgMemc->get( $this->getEmptinessCacheKey() ) === 'true' ) {
+ return false; // queue is empty
+ }
$dbw = $this->getMasterDB();
$dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
+ $uuid = wfRandomString( 32 ); // pop attempt
$job = false; // job popped off
$autoTrx = $dbw->getFlag( DBO_TRX ); // automatic begin() enabled?
$dbw->clearFlag( DBO_TRX ); // make each query its own transaction
'version-entrypoints-load-php' => wfScript( 'load' ),
);
+ $language = $this->getLanguage();
+ $thAttribures = array(
+ 'dir' => $language->getDir(),
+ 'lang' => $language->getCode()
+ );
$out = Html::element( 'h2', array( 'id' => 'mw-version-entrypoints' ), $this->msg( 'version-entrypoints' )->text() ) .
- Html::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'mw-version-entrypoints-table' ) ) .
+ Html::openElement( 'table',
+ array(
+ 'class' => 'wikitable plainlinks',
+ 'id' => 'mw-version-entrypoints-table',
+ 'dir' => 'ltr',
+ 'lang' => 'en'
+ )
+ ) .
Html::openElement( 'tr' ) .
- Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) .
- Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-url' )->text() ) .
+ Html::element( 'th', $thAttribures, $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) .
+ Html::element( 'th', $thAttribures, $this->msg( 'version-entrypoints-header-url' )->text() ) .
Html::closeElement( 'tr' );
foreach ( $entryPoints as $message => $value ) {
$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;
}