* @private
*/
var
- $fileExists, # does the file file exist on disk? (loadFromXxx)
+ $fileExists, # does the file exist on disk? (loadFromXxx)
$historyLine, # Number of line to return by nextHistoryLine() (constructor)
$historyRes, # result of the query for the file's history (nextHistoryLine)
$width, # \
/**#@-*/
+ protected $repoClass = 'LocalRepo';
+
/**
* Create a LocalFile from a title
* Do not call this except from inside a repo class.
*
* Note: $unused param is only here to avoid an E_STRICT
+ *
+ * @param $title
+ * @param $repo
+ * @param $unused
+ *
+ * @return LocalFile
*/
static function newFromTitle( $title, $repo, $unused = null ) {
return new self( $title, $repo );
/**
* Create a LocalFile from a title
* Do not call this except from inside a repo class.
+ *
+ * @param $row
+ * @param $repo
+ *
+ * @return LocalFile
*/
static function newFromRow( $row, $repo ) {
$title = Title::makeTitle( NS_FILE, $row->img_name );
/**
* Create a LocalFile from a SHA-1 key
* Do not call this except from inside a repo class.
+ *
+ * @param $sha1 string base-36 SHA-1
+ * @param $repo LocalRepo
+ * @param string|bool $timestamp MW_timestamp (optional)
+ *
+ * @return bool|LocalFile
*/
static function newFromKey( $sha1, $repo, $timestamp = false ) {
- $conds = array( 'img_sha1' => $sha1 );
+ $dbr = $repo->getSlaveDB();
+ $conds = array( 'img_sha1' => $sha1 );
if ( $timestamp ) {
- $conds['img_timestamp'] = $timestamp;
+ $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
}
- $dbr = $repo->getSlaveDB();
$row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
-
if ( $row ) {
return self::newFromRow( $row, $repo );
} else {
* Do not call this except from inside a repo class.
*/
function __construct( $title, $repo ) {
- if ( !is_object( $title ) ) {
- throw new MWException( __CLASS__ . ' constructor given bogus title.' );
- }
-
parent::__construct( $title, $repo );
$this->metadata = '';
$this->historyLine = 0;
$this->historyRes = null;
$this->dataLoaded = false;
+
+ $this->assertRepoDefined();
+ $this->assertTitleDefined();
}
/**
* Upgrade a row if it needs it
*/
function maybeUpgradeRow() {
+ global $wgUpdateCompatibleMetadata;
if ( wfReadOnly() ) {
return;
}
$this->upgraded = true;
} else {
$handler = $this->getHandler();
- if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
- $this->upgradeRow();
- $this->upgraded = true;
+ if ( $handler ) {
+ $validity = $handler->isMetadataValid( $this, $this->metadata );
+ if ( $validity === MediaHandler::METADATA_BAD
+ || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
+ ) {
+ $this->upgradeRow();
+ $this->upgraded = true;
+ }
}
}
}
/** isTrustedFile inherited */
/**
- * Returns true if the file file exists on disk.
- * @return boolean Whether file file exist on disk.
+ * Returns true if the file exists on disk.
+ * @return boolean Whether file exist on disk.
*/
public function exists() {
$this->load();
/** getUnscaledThumb inherited */
/** thumbName inherited */
/** createThumb inherited */
- /** getThumbnail inherited */
/** transform inherited */
/**
/**
* Get all thumbnail names previously generated for this file
+ * @param $archiveName string|false Name of an archive file
+ * @return array first element is the base dir, then files in that base dir.
*/
- function getThumbnails() {
+ function getThumbnails( $archiveName = false ) {
$this->load();
+ if ( $archiveName ) {
+ $dir = $this->getArchiveThumbPath( $archiveName );
+ } else {
+ $dir = $this->getThumbPath();
+ }
$files = array();
- $dir = $this->getThumbPath();
+ $files[] = $dir;
if ( is_dir( $dir ) ) {
$handle = opendir( $dir );
$hashedName = md5( $this->getName() );
$oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
+ // Must purge thumbnails for old versions too! bug 30192
+ foreach( $this->getHistory() as $oldFile ) {
+ $oldFile->purgeThumbnails();
+ }
+
if ( $oldKey ) {
$wgMemc->delete( $oldKey );
}
}
/**
- * Delete cached transformed files
+ * Delete cached transformed files for archived files
+ * @param $archiveName string name of the archived file
+ */
+ function purgeOldThumbnails( $archiveName ) {
+ global $wgUseSquid;
+ // Get a list of old thumbnails and URLs
+ $files = $this->getThumbnails( $archiveName );
+ $dir = array_shift( $files );
+ $this->purgeThumbList( $dir, $files );
+
+ // Directory should be empty, delete it too. This will probably suck on
+ // something like NFS or if the directory isn't actually empty, so hide
+ // the warnings :D
+ wfSuppressWarnings();
+ if( !rmdir( $dir ) ) {
+ wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
+ }
+ wfRestoreWarnings();
+
+ // Purge any custom thumbnail caches
+ wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, 'archive' ) );
+
+ // Purge the squid
+ if ( $wgUseSquid ) {
+ $urls = array();
+ foreach( $files as $file ) {
+ $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
+ }
+ SquidUpdate::purge( $urls );
+ }
+ }
+
+
+ /**
+ * Delete cached transformed files for the current version only.
*/
function purgeThumbnails() {
global $wgUseSquid;
// Delete thumbnails
$files = $this->getThumbnails();
- $dir = $this->getThumbPath();
- $urls = array();
+ $dir = array_shift( $files );
+ $this->purgeThumbList( $dir, $files );
+
+ // Purge any custom thumbnail caches
+ wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, 'current' ) );
+
+ // Purge the squid
+ if ( $wgUseSquid ) {
+ $urls = array();
+ foreach( $files as $file ) {
+ $urls[] = $this->getThumbUrl( $file );
+ }
+ SquidUpdate::purge( $urls );
+ }
+ }
+ /**
+ * Delete a list of thumbnails visible at urls
+ * @param $dir string base dir of the files.
+ * @param $files array of strings: relative filenames (to $dir)
+ */
+ protected function purgeThumbList($dir, $files) {
+ wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
foreach ( $files as $file ) {
# Check that the base file name is part of the thumb name
# This is a basic sanity check to avoid erasing unrelated directories
if ( strpos( $file, $this->getName() ) !== false ) {
- $url = $this->getThumbUrl( $file );
- $urls[] = $url;
- @unlink( "$dir/$file" );
+ wfSuppressWarnings();
+ unlink( "$dir/$file" );
+ wfRestoreWarnings();
}
}
-
- // Purge the squid
- if ( $wgUseSquid ) {
- SquidUpdate::purge( $urls );
- }
}
/** purgeDescription inherited */
}
}
- /** getFullPath inherited */
/** getHashPath inherited */
/** getRel inherited */
/** getUrlRel inherited */
/** getArchiveRel inherited */
- /** getThumbRel inherited */
/** getArchivePath inherited */
/** getThumbPath inherited */
/** getArchiveUrl inherited */
/**
* Record a file upload in the upload log and the image table
- * @deprecated use upload()
*/
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
$watch = false, $timestamp = false )
$wgUser->addWatch( $this->getTitle() );
}
return true;
-
}
/**
* Record a file upload in the upload log and the image table
*/
- function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null )
- {
+ function recordUpload2(
+ $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
+ ) {
if ( is_null( $user ) ) {
global $wgUser;
$user = $wgUser;
$props = $this->repo->getFileProps( $this->getVirtualUrl() );
}
+ if ( $timestamp === false ) {
+ $timestamp = $dbw->timestamp();
+ }
+
$props['description'] = $comment;
$props['user'] = $user->getId();
$props['user_text'] = $user->getName();
- $props['timestamp'] = wfTimestamp( TS_MW );
+ $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
$this->setProps( $props );
# Delete thumbnails
# Fail now if the file isn't there
if ( !$this->fileExists ) {
- wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" );
+ wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
return false;
}
$reupload = false;
- if ( $timestamp === false ) {
- $timestamp = $dbw->timestamp();
- }
-
# Test to see if the row exists using INSERT IGNORE
# This avoids race conditions by locking the row until the commit, and also
# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
} else {
# This is a new file
# Update the image count
- $site_stats = $dbw->tableName( 'site_stats' );
- $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+ $dbw->begin( __METHOD__ );
+ $dbw->update(
+ 'site_stats',
+ array( 'ss_images = ss_images+1' ),
+ '*',
+ __METHOD__
+ );
+ $dbw->commit( __METHOD__ );
}
$descTitle = $this->getTitle();
$log->getRcComment(),
false
);
- $nullRevision->insertOn( $dbw );
-
- wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
- $article->updateRevisionOn( $dbw, $nullRevision );
+ if (!is_null($nullRevision)) {
+ $nullRevision->insertOn( $dbw );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
+ $article->updateRevisionOn( $dbw, $nullRevision );
+ }
# Invalidate the cache for the description page
$descTitle->invalidateCache();
$descTitle->purgeSquid();
*
* @param $srcPath String: local filesystem path to the source image
* @param $flags Integer: a bitwise combination of:
- * File::DELETE_SOURCE Delete the source file, i.e. move
- * rather than copy
+ * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*/
function publish( $srcPath, $flags = 0 ) {
+ return $this->publishTo( $srcPath, $this->getRel(), $flags );
+ }
+
+ /**
+ * Move or copy a file to a specified location. Returns a FileRepoStatus
+ * object with the archive name in the "value" member on success.
+ *
+ * The archive name should be passed through to recordUpload for database
+ * registration.
+ *
+ * @param $srcPath String: local filesystem path to the source image
+ * @param $dstRel String: target relative path
+ * @param $flags Integer: a bitwise combination of:
+ * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
+ * @return FileRepoStatus object. On success, the value member contains the
+ * archive name, or an empty string if it was a new file.
+ */
+ function publishTo( $srcPath, $dstRel, $flags = 0 ) {
$this->lock();
- $dstRel = $this->getRel();
- $archiveName = gmdate( 'YmdHis' ) . '!' . $this->getName();
+ $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
$status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
array( 'oi_name' => $this->getName() ) );
foreach ( $result as $row ) {
$batch->addOld( $row->oi_archive_name );
+ $this->purgeOldThumbnails( $row->oi_archive_name );
}
$status = $batch->execute();
$batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
$batch->addOld( $archiveName );
+ $this->purgeOldThumbnails( $archiveName );
$status = $batch->execute();
$this->unlock();
/** scaleHeight inherited */
/** getImageSize inherited */
- /** getDescriptionUrl inherited */
- /** getDescriptionText inherited */
+ /**
+ * Get the URL of the file description page.
+ */
+ function getDescriptionUrl() {
+ return $this->title->getLocalUrl();
+ }
+
+ /**
+ * Get the HTML text of the description page
+ * This is not used by ImagePage for local files, since (among other things)
+ * it skips the parser cache.
+ */
+ function getDescriptionText() {
+ global $wgParser;
+ $revision = Revision::newFromTitle( $this->title );
+ if ( !$revision ) return false;
+ $text = $revision->getText();
+ if ( !$text ) return false;
+ $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+ return $pout->getText();
+ }
function getDescription() {
$this->load();
* @ingroup FileRepo
*/
class LocalFileDeleteBatch {
- var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
+
+ /**
+ * @var LocalFile
+ */
+ var $file;
+
+ var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
var $status;
function __construct( File $file, $reason = '', $suppress = false ) {
return array( $oldRels, $deleteCurrent );
}
- /*protected*/ function getHashes() {
+ protected function getHashes() {
$hashes = array();
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
* @ingroup FileRepo
*/
class LocalFileRestoreBatch {
- var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
+ /**
+ * @var LocalFile
+ */
+ var $file;
+
+ var $cleanupBatch, $ids, $all, $unsuppress = false;
function __construct( File $file, $unsuppress = false ) {
$this->file = $file;
return $status;
}
-
+
+ /**
+ * Cleanup a failed batch. The batch was only partially successful, so
+ * rollback by removing all items that were succesfully copied.
+ *
+ * @param Status $storeStatus
+ * @param array $storeBatch
+ */
function cleanupFailedBatch( $storeStatus, $storeBatch ) {
- $cleanupBatch = array();
-
+ $cleanupBatch = array();
+
foreach ( $storeStatus->success as $i => $success ) {
+ // Check if this item of the batch was successfully copied
if ( $success ) {
- $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][1] );
+ // Item was successfully copied and needs to be removed again
+ // Extract ($dstZone, $dstRel) from the batch
+ $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
}
}
$this->file->repo->cleanupBatch( $cleanupBatch );
* @ingroup FileRepo
*/
class LocalFileMoveBatch {
- var $file, $cur, $olds, $oldCount, $archive, $target, $db;
+
+ /**
+ * @var File
+ */
+ var $file;
+
+ /**
+ * @var Title
+ */
+ var $target;
+
+ var $cur, $olds, $oldCount, $archive, $db;
function __construct( File $file, Title $target ) {
$this->file = $file;
$bits = explode( '!', $oldName, 2 );
if ( count( $bits ) != 2 ) {
- wfDebug( "Invalid old file name: $oldName \n" );
+ wfDebug( "Old file name missing !: '$oldName' \n" );
continue;
}
list( $timestamp, $filename ) = $bits;
if ( $this->oldName != $filename ) {
- wfDebug( "Invalid old file name: $oldName \n" );
+ wfDebug( "Old file name doesn't match: '$oldName' \n" );
continue;
}
$triplets = $this->getMoveTriplets();
$triplets = $this->removeNonexistentFiles( $triplets );
- $statusDb = $this->doDBUpdates();
- wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
- $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
- wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
- if ( !$statusMove->isOk() ) {
+ // Copy the files into their new location
+ $statusMove = $repo->storeBatch( $triplets );
+ wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
+ if ( !$statusMove->isGood() ) {
wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
+ $this->cleanupTarget( $triplets );
+ $statusMove->ok = false;
+ return $statusMove;
+ }
+
+ $this->db->begin();
+ $statusDb = $this->doDBUpdates();
+ wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+ if ( !$statusDb->isGood() ) {
$this->db->rollback();
+ // Something went wrong with the DB updates, so remove the target files
+ $this->cleanupTarget( $triplets );
+ $statusDb->ok = false;
+ return $statusDb;
}
+ $this->db->commit();
+
+ // Everything went ok, remove the source files
+ $this->cleanupSource( $triplets );
$status->merge( $statusDb );
$status->merge( $statusMove );
$status->successCount++;
} else {
$status->failCount++;
+ $status->fatal( 'imageinvalidfilename' );
+ return $status;
}
// Update old images
$total = $this->oldCount;
$status->successCount += $affected;
$status->failCount += $total - $affected;
+ if ( $status->failCount ) {
+ $status->error( 'imageinvalidfilename' );
+ }
return $status;
}
// $move: (oldRelativePath, newRelativePath)
$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
$triplets[] = array( $srcUrl, 'public', $move[1] );
- wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->name}: {$srcUrl} :: public :: {$move[1]}" );
+ wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
}
return $triplets;
return $filteredTriplets;
}
+
+ /**
+ * Cleanup a partially moved array of triplets by deleting the target
+ * files. Called if something went wrong half way.
+ */
+ function cleanupTarget( $triplets ) {
+ // Create dest pairs from the triplets
+ $pairs = array();
+ foreach ( $triplets as $triplet ) {
+ $pairs[] = array( $triplet[1], $triplet[2] );
+ }
+
+ $this->file->repo->cleanupBatch( $pairs );
+ }
+
+ /**
+ * Cleanup a fully moved array of triplets by deleting the source files.
+ * Called at the end of the move process if everything else went ok.
+ */
+ function cleanupSource( $triplets ) {
+ // Create source file names from the triplets
+ $files = array();
+ foreach ( $triplets as $triplet ) {
+ $files[] = $triplet[0];
+ }
+
+ $this->file->repo->cleanupBatch( $files );
+ }
}