/** @var bool Whether the row was upgraded on load */
private $upgraded;
+ /** @var bool Whether the row was scheduled to upgrade on load */
+ private $upgrading;
+
/** @var bool True if the image row is locked */
private $locked;
*/
function maybeUpgradeRow() {
global $wgUpdateCompatibleMetadata;
- if ( wfReadOnly() ) {
+
+ if ( wfReadOnly() || $this->upgrading ) {
return;
}
$upgrade = false;
- if ( is_null( $this->media_type ) ||
- $this->mime == 'image/svg'
- ) {
+ if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
$upgrade = true;
} else {
$handler = $this->getHandler();
if ( $handler ) {
$validity = $handler->isMetadataValid( $this, $this->getMetadata() );
- if ( $validity === MediaHandler::METADATA_BAD
- || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
- ) {
+ if ( $validity === MediaHandler::METADATA_BAD ) {
$upgrade = true;
+ } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
+ $upgrade = $wgUpdateCompatibleMetadata;
}
}
}
if ( $upgrade ) {
- try {
- $this->upgradeRow();
- } catch ( LocalFileLockError $e ) {
- // let the other process handle it (or do it next time)
- }
- $this->upgraded = true; // avoid rework/retries
+ $this->upgrading = true;
+ // Defer updates unless in auto-commit CLI mode
+ DeferredUpdates::addCallableUpdate( function() {
+ $this->upgrading = false; // avoid duplicate updates
+ try {
+ $this->upgradeRow();
+ } catch ( LocalFileLockError $e ) {
+ // let the other process handle it (or do it next time)
+ }
+ } );
}
}
+ /**
+ * @return bool Whether upgradeRow() ran for this object
+ */
function getUpgraded() {
return $this->upgraded;
}
$this->invalidateCache();
$this->unlock(); // done
-
+ $this->upgraded = true; // avoid rework/retries
}
/**
DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
}
+ /**
+ * Prerenders a configurable set of thumbnails
+ *
+ * @since 1.28
+ */
+ public function prerenderThumbnails() {
+ global $wgUploadThumbnailRenderMap;
+
+ $jobs = [];
+
+ $sizes = $wgUploadThumbnailRenderMap;
+ rsort( $sizes );
+
+ foreach ( $sizes as $size ) {
+ if ( $this->isVectorized() || $this->getWidth() > $size ) {
+ $jobs[] = new ThumbnailRenderJob(
+ $this->getTitle(),
+ [ 'transformParams' => [ 'width' => $size ] ]
+ );
+ }
+ }
+
+ if ( $jobs ) {
+ JobQueueGroup::singleton()->lazyPush( $jobs );
+ }
+ }
+
/**
* Delete a list of thumbnails visible at urls
* @param string $dir Base dir of the files.
# Do some cache purges after final commit so that:
# a) Changes are more likely to be seen post-purge
# b) They won't cause rollback of the log publish/update above
- $that = $this;
- $dbw->onTransactionIdle( function () use (
- $that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags
- ) {
- # Update memcache after the commit
- $that->invalidateCache();
-
- $updateLogPage = false;
- if ( $newPageContent ) {
- # New file page; create the description page.
- # There's already a log entry, so don't make a second RC entry
- # CDN and file cache for the description page are purged by doEditContent.
- $status = $wikiPage->doEditContent(
- $newPageContent,
- $comment,
- EDIT_NEW | EDIT_SUPPRESS_RC,
- false,
- $user
- );
-
- if ( isset( $status->value['revision'] ) ) {
- // Associate new page revision id
- $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
- }
- // This relies on the resetArticleID() call in WikiPage::insertOn(),
- // which is triggered on $descTitle by doEditContent() above.
- if ( isset( $status->value['revision'] ) ) {
- /** @var $rev Revision */
- $rev = $status->value['revision'];
- $updateLogPage = $rev->getPage();
- }
- } else {
- # Existing file page: invalidate description page cache
- $wikiPage->getTitle()->invalidateCache();
- $wikiPage->getTitle()->purgeSquid();
- # Allow the new file version to be patrolled from the page footer
- Article::purgePatrolFooterCache( $descId );
- }
+ DeferredUpdates::addUpdate(
+ new AutoCommitUpdate(
+ $dbw,
+ __METHOD__,
+ function () use (
+ $reupload, $wikiPage, $newPageContent, $comment, $user,
+ $logEntry, $logId, $descId, $tags
+ ) {
+ # Update memcache after the commit
+ $this->invalidateCache();
+
+ $updateLogPage = false;
+ if ( $newPageContent ) {
+ # New file page; create the description page.
+ # There's already a log entry, so don't make a second RC entry
+ # CDN and file cache for the description page are purged by doEditContent.
+ $status = $wikiPage->doEditContent(
+ $newPageContent,
+ $comment,
+ EDIT_NEW | EDIT_SUPPRESS_RC,
+ false,
+ $user
+ );
+
+ if ( isset( $status->value['revision'] ) ) {
+ // Associate new page revision id
+ $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
+ }
+ // This relies on the resetArticleID() call in WikiPage::insertOn(),
+ // which is triggered on $descTitle by doEditContent() above.
+ if ( isset( $status->value['revision'] ) ) {
+ /** @var $rev Revision */
+ $rev = $status->value['revision'];
+ $updateLogPage = $rev->getPage();
+ }
+ } else {
+ # Existing file page: invalidate description page cache
+ $wikiPage->getTitle()->invalidateCache();
+ $wikiPage->getTitle()->purgeSquid();
+ # Allow the new file version to be patrolled from the page footer
+ Article::purgePatrolFooterCache( $descId );
+ }
- # Update associated rev id. This should be done by $logEntry->insert() earlier,
- # but setAssociatedRevId() wasn't called at that point yet...
- $logParams = $logEntry->getParameters();
- $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
- $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
- if ( $updateLogPage ) {
- # Also log page, in case where we just created it above
- $update['log_page'] = $updateLogPage;
- }
- $that->getRepo()->getMasterDB()->update(
- 'logging',
- $update,
- [ 'log_id' => $logId ],
- __METHOD__
- );
- $that->getRepo()->getMasterDB()->insert(
- 'log_search',
- [
- 'ls_field' => 'associated_rev_id',
- 'ls_value' => $logEntry->getAssociatedRevId(),
- 'ls_log_id' => $logId,
- ],
- __METHOD__
- );
+ # Update associated rev id. This should be done by $logEntry->insert() earlier,
+ # but setAssociatedRevId() wasn't called at that point yet...
+ $logParams = $logEntry->getParameters();
+ $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
+ $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
+ if ( $updateLogPage ) {
+ # Also log page, in case where we just created it above
+ $update['log_page'] = $updateLogPage;
+ }
+ $this->getRepo()->getMasterDB()->update(
+ 'logging',
+ $update,
+ [ 'log_id' => $logId ],
+ __METHOD__
+ );
+ $this->getRepo()->getMasterDB()->insert(
+ 'log_search',
+ [
+ 'ls_field' => 'associated_rev_id',
+ 'ls_value' => $logEntry->getAssociatedRevId(),
+ 'ls_log_id' => $logId,
+ ],
+ __METHOD__
+ );
+
+ # Add change tags, if any
+ if ( $tags ) {
+ $logEntry->setTags( $tags );
+ }
- # Add change tags, if any
- if ( $tags ) {
- $logEntry->setTags( $tags );
- }
+ # Uploads can be patrolled
+ $logEntry->setIsPatrollable( true );
- # Uploads can be patrolled
- $logEntry->setIsPatrollable( true );
+ # Now that the log entry is up-to-date, make an RC entry.
+ $logEntry->publish( $logId );
- # Now that the log entry is up-to-date, make an RC entry.
- $logEntry->publish( $logId );
+ # Run hook for other updates (typically more cache purging)
+ Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
- # Run hook for other updates (typically more cache purging)
- Hooks::run( 'FileUpload', [ $that, $reupload, !$newPageContent ] );
+ if ( $reupload ) {
+ # Delete old thumbnails
+ $this->purgeThumbnails();
+ # Remove the old file from the CDN cache
+ DeferredUpdates::addUpdate(
+ new CdnCacheUpdate( [ $this->getUrl() ] ),
+ DeferredUpdates::PRESEND
+ );
+ } else {
+ # Update backlink pages pointing to this title if created
+ LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
+ }
- if ( $reupload ) {
- # Delete old thumbnails
- $that->purgeThumbnails();
- # Remove the old file from the CDN cache
- DeferredUpdates::addUpdate(
- new CdnCacheUpdate( [ $that->getUrl() ] ),
- DeferredUpdates::PRESEND
- );
- } else {
- # Update backlink pages pointing to this title if created
- LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
- }
- } );
+ $this->prerenderThumbnails();
+ }
+ ),
+ DeferredUpdates::PRESEND
+ );
if ( !$reupload ) {
# This is a new file, so update the image count
&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
}
+ /**
+ * @return Status
+ * @since 1.28
+ */
+ public function acquireFileLock() {
+ return $this->getRepo()->getBackend()->lockFiles(
+ [ $this->getPath() ], LockManager::LOCK_EX, 10
+ );
+ }
+
+ /**
+ * @return Status
+ * @since 1.28
+ */
+ public function releaseFileLock() {
+ return $this->getRepo()->getBackend()->unlockFiles(
+ [ $this->getPath() ], LockManager::LOCK_EX
+ );
+ }
+
/**
* Start an atomic DB section and lock the image for update
* or increments a reference counter if the lock is already held
*
+ * This method should not be used outside of LocalFile/LocalFile*Batch
+ *
* @throws LocalFileLockError Throws an error if the lock was not acquired
* @return bool Whether the file lock owns/spawned the DB transaction
*/
- function lock() {
+ public function lock() {
if ( !$this->locked ) {
$logger = LoggerFactory::getInstance( 'LocalFile' );
// Bug 54736: use simple lock to handle when the file does not exist.
// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
// Also, that would cause contention on INSERT of similarly named rows.
- $backend = $this->getRepo()->getBackend();
- $lockPaths = [ $this->getPath() ]; // represents all versions of the file
- $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 10 );
+ $status = $this->acquireFileLock(); // represents all versions of the file
if ( !$status->isGood() ) {
$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
$logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
}
// Release the lock *after* commit to avoid row-level contention.
// Make sure it triggers on rollback() as well as commit() (T132921).
- $dbw->onTransactionResolution( function () use ( $backend, $lockPaths, $logger ) {
- $status = $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX );
+ $dbw->onTransactionResolution( function () use ( $logger ) {
+ $status = $this->releaseFileLock();
if ( !$status->isGood() ) {
$logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
}
/**
* Decrement the lock reference count and end the atomic section if it reaches zero
*
+ * This method should not be used outside of LocalFile/LocalFile*Batch
+ *
* The commit and loc release will happen when no atomic sections are active, which
* may happen immediately or at some point after calling this
*/
- function unlock() {
+ public function unlock() {
if ( $this->locked ) {
--$this->locked;
if ( !$this->locked ) {
}
}
- /**
- * Roll back the DB transaction and mark the image unlocked
- */
- function unlockAndRollback() {
- $this->locked = false;
- $dbw = $this->repo->getMasterDB();
- $dbw->rollback( __METHOD__ );
- $this->lockedOwnTrx = false;
- }
-
/**
* @return Status
*/
}
protected function doDBInserts() {
+ $now = time();
$dbw = $this->file->repo->getMasterDB();
- $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
$encUserId = $dbw->addQuotes( $this->user->getId() );
$encReason = $dbw->addQuotes( $this->reason );
$encGroup = $dbw->addQuotes( 'deleted' );
}
if ( $deleteCurrent ) {
- $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
- $where = [ 'img_name' => $this->file->getName() ];
- $dbw->insertSelect( 'filearchive', 'image',
+ $dbw->insertSelect(
+ 'filearchive',
+ 'image',
[
'fa_storage_group' => $encGroup,
'fa_storage_key' => $dbw->conditional(
[ 'img_sha1' => '' ],
$dbw->addQuotes( '' ),
- $concat
+ $dbw->buildConcat( [ "img_sha1", $encExt ] )
),
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_user' => 'img_user',
'fa_user_text' => 'img_user_text',
'fa_timestamp' => 'img_timestamp',
- 'fa_sha1' => 'img_sha1',
- ], $where, __METHOD__ );
+ 'fa_sha1' => 'img_sha1'
+ ],
+ [ 'img_name' => $this->file->getName() ],
+ __METHOD__
+ );
}
if ( count( $oldRels ) ) {
- $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
- $where = [
- 'oi_name' => $this->file->getName(),
- 'oi_archive_name' => array_keys( $oldRels ) ];
- $dbw->insertSelect( 'filearchive', 'oldimage',
+ $res = $dbw->select(
+ 'oldimage',
+ OldLocalFile::selectFields(),
[
- 'fa_storage_group' => $encGroup,
- 'fa_storage_key' => $dbw->conditional(
- [ 'oi_sha1' => '' ],
- $dbw->addQuotes( '' ),
- $concat
- ),
- 'fa_deleted_user' => $encUserId,
- 'fa_deleted_timestamp' => $encTimestamp,
- 'fa_deleted_reason' => $encReason,
- 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
-
- 'fa_name' => 'oi_name',
- 'fa_archive_name' => 'oi_archive_name',
- 'fa_size' => 'oi_size',
- 'fa_width' => 'oi_width',
- 'fa_height' => 'oi_height',
- 'fa_metadata' => 'oi_metadata',
- 'fa_bits' => 'oi_bits',
- 'fa_media_type' => 'oi_media_type',
- 'fa_major_mime' => 'oi_major_mime',
- 'fa_minor_mime' => 'oi_minor_mime',
- 'fa_description' => 'oi_description',
- 'fa_user' => 'oi_user',
- 'fa_user_text' => 'oi_user_text',
- 'fa_timestamp' => 'oi_timestamp',
- 'fa_sha1' => 'oi_sha1',
- ], $where, __METHOD__ );
+ 'oi_name' => $this->file->getName(),
+ 'oi_archive_name' => array_keys( $oldRels )
+ ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+ $rowsInsert = [];
+ foreach ( $res as $row ) {
+ $rowsInsert[] = [
+ // Deletion-specific fields
+ 'fa_storage_group' => 'deleted',
+ 'fa_storage_key' => ( $row->oi_sha1 === '' )
+ ? ''
+ : "{$row->oi_sha1}{$dotExt}",
+ 'fa_deleted_user' => $this->user->getId(),
+ 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
+ 'fa_deleted_reason' => $this->reason,
+ // Counterpart fields
+ 'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
+ 'fa_name' => $row->oi_name,
+ 'fa_archive_name' => $row->oi_archive_name,
+ 'fa_size' => $row->oi_size,
+ 'fa_width' => $row->oi_width,
+ 'fa_height' => $row->oi_height,
+ 'fa_metadata' => $row->oi_metadata,
+ 'fa_bits' => $row->oi_bits,
+ 'fa_media_type' => $row->oi_media_type,
+ 'fa_major_mime' => $row->oi_major_mime,
+ 'fa_minor_mime' => $row->oi_minor_mime,
+ 'fa_description' => $row->oi_description,
+ 'fa_user' => $row->oi_user,
+ 'fa_user_text' => $row->oi_user_text,
+ 'fa_timestamp' => $row->oi_timestamp,
+ 'fa_sha1' => $row->oi_sha1
+ ];
+ }
+
+ $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
}
}
// The live (current) version cannot be hidden!
if ( !$this->unsuppress && $row->fa_deleted ) {
- $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
- $this->cleanupBatch[] = $row->fa_storage_key;
+ $status->fatal( 'undeleterevdel' );
+ $this->file->unlock();
+ return $status;
}
} else {
$archiveName = $row->fa_archive_name;
public function execute() {
$repo = $this->file->repo;
$status = $repo->newGood();
+ $destFile = wfLocalFile( $this->target );
+
+ $this->file->lock(); // begin
+ $destFile->lock(); // quickly fail if destination is not available
$triplets = $this->getMoveTriplets();
$checkStatus = $this->removeNonexistentFiles( $triplets );
if ( !$checkStatus->isGood() ) {
- $status->merge( $checkStatus );
+ $destFile->unlock();
+ $this->file->unlock();
+ $status->merge( $checkStatus ); // couldn't talk to file backend
return $status;
}
$triplets = $checkStatus->value;
- $destFile = wfLocalFile( $this->target );
- $this->file->lock(); // begin
- $destFile->lock(); // quickly fail if destination is not available
- // Rename the file versions metadata in the DB.
- // This implicitly locks the destination file, which avoids race conditions.
- // If we moved the files from A -> C before DB updates, another process could
- // move files from B -> C at this point, causing storeBatch() to fail and thus
- // cleanupTarget() to trigger. It would delete the C files and cause data loss.
- $statusDb = $this->doDBUpdates();
+ // Verify the file versions metadata in the DB.
+ $statusDb = $this->verifyDBUpdates();
if ( !$statusDb->isGood() ) {
$destFile->unlock();
- $this->file->unlockAndRollback();
+ $this->file->unlock();
$statusDb->ok = false;
return $statusDb;
}
- wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
- "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
if ( !$repo->hasSha1Storage() ) {
// Copy the files into their new location.
// Delete any files copied over (while the destination is still locked)
$this->cleanupTarget( $triplets );
$destFile->unlock();
- $this->file->unlockAndRollback(); // unlocks the destination
+ $this->file->unlock();
wfDebugLog( 'imagemove', "Error in moving files: "
. $statusMove->getWikiText( false, false, 'en' ) );
$statusMove->ok = false;
$status->merge( $statusMove );
}
+ // Rename the file versions metadata in the DB.
+ $this->doDBUpdates();
+
+ wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
+ "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+
$destFile->unlock();
$this->file->unlock(); // done
}
/**
- * Do the database updates and return a new FileRepoStatus indicating how
- * many rows where updated.
+ * Verify the database updates and return a new FileRepoStatus indicating how
+ * many rows would be updated.
*
* @return FileRepoStatus
*/
- protected function doDBUpdates() {
+ protected function verifyDBUpdates() {
$repo = $this->file->repo;
$status = $repo->newGood();
$dbw = $this->db;
- // Update current image
- $dbw->update(
+ $hasCurrent = $dbw->selectField(
'image',
- [ 'img_name' => $this->newName ],
+ '1',
[ 'img_name' => $this->oldName ],
- __METHOD__
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+ $oldRowCount = $dbw->selectField(
+ 'oldimage',
+ 'COUNT(*)',
+ [ 'oi_name' => $this->oldName ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
);
- if ( $dbw->affectedRows() ) {
+ if ( $hasCurrent ) {
$status->successCount++;
} else {
$status->failCount++;
- $status->fatal( 'imageinvalidfilename' );
-
- return $status;
}
+ $status->successCount += $oldRowCount;
+ // Bug 34934: oldCount is based on files that actually exist.
+ // There may be more DB rows than such files, in which case $affected
+ // can be greater than $total. We use max() to avoid negatives here.
+ $status->failCount += max( 0, $this->oldCount - $oldRowCount );
+ if ( $status->failCount ) {
+ $status->error( 'imageinvalidfilename' );
+ }
+
+ return $status;
+ }
+
+ /**
+ * Do the database updates and return a new FileRepoStatus indicating how
+ * many rows where updated.
+ */
+ protected function doDBUpdates() {
+ $dbw = $this->db;
+ // Update current image
+ $dbw->update(
+ 'image',
+ [ 'img_name' => $this->newName ],
+ [ 'img_name' => $this->oldName ],
+ __METHOD__
+ );
// Update old images
$dbw->update(
'oldimage',
[ 'oi_name' => $this->oldName ],
__METHOD__
);
-
- $affected = $dbw->affectedRows();
- $total = $this->oldCount;
- $status->successCount += $affected;
- // Bug 34934: $total is based on files that actually exist.
- // There may be more DB rows than such files, in which case $affected
- // can be greater than $total. We use max() to avoid negatives here.
- $status->failCount += max( 0, $total - $affected );
- if ( $status->failCount ) {
- $status->error( 'imageinvalidfilename' );
- }
-
- return $status;
}
/**