diff options
Diffstat (limited to 'includes/filerepo')
-rw-r--r-- | includes/filerepo/FileBackendDBRepoWrapper.php | 356 | ||||
-rw-r--r-- | includes/filerepo/FileRepo.php | 43 | ||||
-rw-r--r-- | includes/filerepo/ForeignAPIRepo.php | 4 | ||||
-rw-r--r-- | includes/filerepo/ForeignDBRepo.php | 32 | ||||
-rw-r--r-- | includes/filerepo/ForeignDBViaLBRepo.php | 10 | ||||
-rw-r--r-- | includes/filerepo/LocalRepo.php | 113 | ||||
-rw-r--r-- | includes/filerepo/file/ArchivedFile.php | 4 | ||||
-rw-r--r-- | includes/filerepo/file/File.php | 65 | ||||
-rw-r--r-- | includes/filerepo/file/ForeignAPIFile.php | 19 | ||||
-rw-r--r-- | includes/filerepo/file/LocalFile.php | 289 |
10 files changed, 733 insertions, 202 deletions
diff --git a/includes/filerepo/FileBackendDBRepoWrapper.php b/includes/filerepo/FileBackendDBRepoWrapper.php new file mode 100644 index 00000000..c83e5b1b --- /dev/null +++ b/includes/filerepo/FileBackendDBRepoWrapper.php @@ -0,0 +1,356 @@ +<?php +/** + * Proxy backend that manages file layout rewriting for FileRepo. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup FileRepo + * @ingroup FileBackend + * @author Aaron Schulz + */ + +/** + * @brief Proxy backend that manages file layout rewriting for FileRepo. + * + * LocalRepo may be configured to store files under their title names or by SHA-1. + * This acts as a shim in the later case, providing backwards compatability for + * most callers. All "public"/"deleted" zone files actually go in an "original" + * container and are never changed. + * + * This requires something like thumb_handler.php and img_auth.php for client viewing of files. + * + * @ingroup FileRepo + * @ingroup FileBackend + * @since 1.25 + */ +class FileBackendDBRepoWrapper extends FileBackend { + /** @var FileBackend */ + protected $backend; + /** @var string */ + protected $repoName; + /** @var Closure */ + protected $dbHandleFunc; + /** @var ProcessCacheLRU */ + protected $resolvedPathCache; + /** @var DBConnRef[] */ + protected $dbs; + + public function __construct( array $config ) { + $config['name'] = $config['backend']->getName(); + $config['wikiId'] = $config['backend']->getWikiId(); + parent::__construct( $config ); + $this->backend = $config['backend']; + $this->repoName = $config['repoName']; + $this->dbHandleFunc = $config['dbHandleFactory']; + $this->resolvedPathCache = new ProcessCacheLRU( 100 ); + } + + /** + * Get the underlying FileBackend that is being wrapped + * + * @return FileBackend + */ + public function getInternalBackend() { + return $this->backend; + } + + /** + * Translate a legacy "title" path to it's "sha1" counterpart + * + * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg + * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg + * + * @param string $path + * @param bool $latest + * @return string + */ + public function getBackendPath( $path, $latest = true ) { + $paths = $this->getBackendPaths( array( $path ), $latest ); + return current( $paths ); + } + + /** + * Translate legacy "title" paths to their "sha1" counterparts + * + * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg + * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg + * + * @param array $paths + * @param bool $latest + * @return array Translated paths in same order + */ + public function getBackendPaths( array $paths, $latest = true ) { + $db = $this->getDB( $latest ? DB_MASTER : DB_SLAVE ); + + // @TODO: batching + $resolved = array(); + foreach ( $paths as $i => $path ) { + if ( !$latest && $this->resolvedPathCache->has( $path, 'target', 10 ) ) { + $resolved[$i] = $this->resolvedPathCache->get( $path, 'target' ); + continue; + } + + list( , $container ) = FileBackend::splitStoragePath( $path ); + + if ( $container === "{$this->repoName}-public" ) { + $name = basename( $path ); + if ( strpos( $path, '!' ) !== false ) { + $sha1 = $db->selectField( 'oldimage', 'oi_sha1', + array( 'oi_archive_name' => $name ), + __METHOD__ + ); + } else { + $sha1 = $db->selectField( 'image', 'img_sha1', + array( 'img_name' => $name ), + __METHOD__ + ); + } + if ( !strlen( $sha1 ) ) { + $resolved[$i] = $path; // give up + continue; + } + $resolved[$i] = $this->getPathForSHA1( $sha1 ); + $this->resolvedPathCache->set( $path, 'target', $resolved[$i] ); + } elseif ( $container === "{$this->repoName}-deleted" ) { + $name = basename( $path ); // <hash>.<ext> + $sha1 = substr( $name, 0, strpos( $name, '.' ) ); // ignore extension + $resolved[$i] = $this->getPathForSHA1( $sha1 ); + $this->resolvedPathCache->set( $path, 'target', $resolved[$i] ); + } else { + $resolved[$i] = $path; + } + } + + $res = array(); + foreach ( $paths as $i => $path ) { + $res[$i] = $resolved[$i]; + } + + return $res; + } + + protected function doOperationsInternal( array $ops, array $opts ) { + return $this->backend->doOperationsInternal( $this->mungeOpPaths( $ops ), $opts ); + } + + protected function doQuickOperationsInternal( array $ops ) { + return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ) ); + } + + protected function doPrepare( array $params ) { + return $this->backend->doPrepare( $params ); + } + + protected function doSecure( array $params ) { + return $this->backend->doSecure( $params ); + } + + protected function doPublish( array $params ) { + return $this->backend->doPublish( $params ); + } + + protected function doClean( array $params ) { + return $this->backend->doClean( $params ); + } + + public function concatenate( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function fileExists( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileTimestamp( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileSize( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileStat( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileXAttributes( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileSha1Base36( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileProps( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function streamFile( array $params ) { + // The stream methods use the file extension to determine the + // Content-Type (as MediaWiki should already validate it on upload). + // The translated SHA1 path has no extension, so this needs to use + // the untranslated path extension. + $type = StreamFile::contentTypeFromPath( $params['src'] ); + if ( $type && $type != 'unknown/unknown' ) { + $params['headers'][] = "Content-type: $type"; + } + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getFileContentsMulti( array $params ) { + return $this->translateArrayResults( __FUNCTION__, $params ); + } + + public function getLocalReferenceMulti( array $params ) { + return $this->translateArrayResults( __FUNCTION__, $params ); + } + + public function getLocalCopyMulti( array $params ) { + return $this->translateArrayResults( __FUNCTION__, $params ); + } + + public function getFileHttpUrl( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function directoryExists( array $params ) { + return $this->backend->directoryExists( $params ); + } + + public function getDirectoryList( array $params ) { + return $this->backend->getDirectoryList( $params ); + } + + public function getFileList( array $params ) { + return $this->backend->getFileList( $params ); + } + + public function getFeatures() { + return $this->backend->getFeatures(); + } + + public function clearCache( array $paths = null ) { + $this->backend->clearCache( null ); // clear all + } + + public function preloadCache( array $paths ) { + $paths = $this->getBackendPaths( $paths ); + $this->backend->preloadCache( $paths ); + } + + public function preloadFileStat( array $params ) { + return $this->translateSrcParams( __FUNCTION__, $params ); + } + + public function getScopedLocksForOps( array $ops, Status $status ) { + return $this->backend->getScopedLocksForOps( $ops, $status ); + } + + /** + * Get the ultimate original storage path for a file + * + * Use this when putting a new file into the system + * + * @param string $sha1 File SHA-1 base36 + * @return string + */ + public function getPathForSHA1( $sha1 ) { + if ( strlen( $sha1 ) < 3 ) { + throw new InvalidArgumentException( "Invalid file SHA-1." ); + } + return $this->backend->getContainerStoragePath( "{$this->repoName}-original" ) . + "/{$sha1[0]}/{$sha1[1]}/{$sha1[2]}/{$sha1}"; + } + + /** + * Get a connection to the repo file registry DB + * + * @param integer $index + * @return DBConnRef + */ + protected function getDB( $index ) { + if ( !isset( $this->dbs[$index] ) ) { + $func = $this->dbHandleFunc; + $this->dbs[$index] = $func( $index ); + } + return $this->dbs[$index]; + } + + /** + * Translates paths found in the "src" or "srcs" keys of a params array + * + * @param string $function + * @param array $params + */ + protected function translateSrcParams( $function, array $params ) { + $latest = !empty( $params['latest'] ); + + if ( isset( $params['src'] ) ) { + $params['src'] = $this->getBackendPath( $params['src'], $latest ); + } + + if ( isset( $params['srcs'] ) ) { + $params['srcs'] = $this->getBackendPaths( $params['srcs'], $latest ); + } + + return $this->backend->$function( $params ); + } + + /** + * Translates paths when the backend function returns results keyed by paths + * + * @param string $function + * @param array $params + * @return array + */ + protected function translateArrayResults( $function, array $params ) { + $origPaths = $params['srcs']; + $params['srcs'] = $this->getBackendPaths( $params['srcs'], !empty( $params['latest'] ) ); + $pathMap = array_combine( $params['srcs'], $origPaths ); + + $results = $this->backend->$function( $params ); + + $contents = array(); + foreach ( $results as $path => $result ) { + $contents[$pathMap[$path]] = $result; + } + + return $contents; + } + + /** + * Translate legacy "title" source paths to their "sha1" counterparts + * + * This leaves destination paths alone since we don't want those to mutate + * + * @param array $ops + * @return array + */ + protected function mungeOpPaths( array $ops ) { + // Ops that use 'src' and do not mutate core file data there + static $srcRefOps = array( 'store', 'copy', 'describe' ); + foreach ( $ops as &$op ) { + if ( isset( $op['src'] ) && in_array( $op['op'], $srcRefOps ) ) { + $op['src'] = $this->getBackendPath( $op['src'], true ); + } + if ( isset( $op['srcs'] ) ) { + $op['srcs'] = $this->getBackendPaths( $op['srcs'], true ); + } + } + return $ops; + } +} diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index cef1176d..7370c5cd 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -49,6 +49,9 @@ class FileRepo { /** @var int */ public $descriptionCacheExpiry; + /** @var bool */ + protected $hasSha1Storage = false; + /** @var FileBackend */ protected $backend; @@ -63,7 +66,7 @@ class FileRepo { protected $transformVia404; /** @var string URL of image description pages, e.g. - * http://en.wikipedia.org/wiki/File: + * https://en.wikipedia.org/wiki/File: */ protected $descBaseUrl; @@ -76,7 +79,7 @@ class FileRepo { * to $wgScriptExtension, e.g. .php5 defaults to .php */ protected $scriptExtension; - /** @var string Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 */ + /** @var string Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 */ protected $articleUrl; /** @var bool Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], @@ -433,16 +436,16 @@ class FileRepo { $img = $this->newFile( $title, $time ); if ( $img ) { $img->load( $flags ); - } - if ( $img && $img->exists() ) { - if ( !$img->isDeleted( File::DELETED_FILE ) ) { - return $img; // always OK - } elseif ( !empty( $options['private'] ) && - $img->userCan( File::DELETED_FILE, - $options['private'] instanceof User ? $options['private'] : null - ) - ) { - return $img; + if ( $img->exists() ) { + if ( !$img->isDeleted( File::DELETED_FILE ) ) { + return $img; // always OK + } elseif ( !empty( $options['private'] ) && + $img->userCan( File::DELETED_FILE, + $options['private'] instanceof User ? $options['private'] : null + ) + ) { + return $img; + } } } } @@ -909,9 +912,9 @@ class FileRepo { $status->merge( $backend->doOperations( $operations, $opts ) ); // Cleanup for disk source files... foreach ( $sourceFSFilesToDelete as $file ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); unlink( $file ); // FS cleanup - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } return $status; @@ -1297,9 +1300,9 @@ class FileRepo { } // Cleanup for disk source files... foreach ( $sourceFSFilesToDelete as $file ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); unlink( $file ); // FS cleanup - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } return $status; @@ -1885,6 +1888,14 @@ class FileRepo { return $ret; } + + /** + * Returns whether or not storage is SHA-1 based + * @return boolean + */ + public function hasSha1Storage() { + return $this->hasSha1Storage; + } } /** diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 71d2b919..4ffbf4ad 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -31,7 +31,7 @@ use MediaWiki\Logger\LoggerFactory; * $wgForeignFileRepos[] = array( * 'class' => 'ForeignAPIRepo', * 'name' => 'shared', - * 'apibase' => 'http://en.wikipedia.org/w/api.php', + * 'apibase' => 'https://en.wikipedia.org/w/api.php', * 'fetchDescription' => true, // Optional * 'descriptionCacheExpiry' => 3600, * ); @@ -74,7 +74,7 @@ class ForeignAPIRepo extends FileRepo { global $wgLocalFileRepo; parent::__construct( $info ); - // http://commons.wikimedia.org/w/api.php + // https://commons.wikimedia.org/w/api.php $this->mApiBase = isset( $info['apibase'] ) ? $info['apibase'] : null; if ( isset( $info['apiThumbCacheExpiry'] ) ) { diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 6e9e6add..dfdb3753 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -76,17 +76,8 @@ class ForeignDBRepo extends LocalRepo { */ function getMasterDB() { if ( !isset( $this->dbConn ) ) { - $this->dbConn = DatabaseBase::factory( $this->dbType, - array( - 'host' => $this->dbServer, - 'user' => $this->dbUser, - 'password' => $this->dbPassword, - 'dbname' => $this->dbName, - 'flags' => $this->dbFlags, - 'tablePrefix' => $this->tablePrefix, - 'foreign' => true, - ) - ); + $func = $this->getDBFactory(); + $this->dbConn = $func( DB_MASTER ); } return $this->dbConn; @@ -100,6 +91,25 @@ class ForeignDBRepo extends LocalRepo { } /** + * @return Closure + */ + protected function getDBFactory() { + return function( $index ) { + return DatabaseBase::factory( $this->dbType, + array( + 'host' => $this->dbServer, + 'user' => $this->dbUser, + 'password' => $this->dbPassword, + 'dbname' => $this->dbName, + 'flags' => $this->dbFlags, + 'tablePrefix' => $this->tablePrefix, + 'foreign' => true, + ) + ); + }; + } + + /** * @return bool */ function hasSharedCache() { diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index 8153ffb4..f49b716f 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -66,6 +66,16 @@ class ForeignDBViaLBRepo extends LocalRepo { return wfGetDB( DB_SLAVE, array(), $this->wiki ); } + /** + * @return Closure + */ + protected function getDBFactory() { + $wiki = $this->wiki; + return function( $index ) use ( $wiki ) { + return wfGetDB( $index, array(), $wiki ); + }; + } + function hasSharedCache() { return $this->hasSharedCache; } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 926fd0b8..6a2c0640 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -29,6 +29,9 @@ * @ingroup FileRepo */ class LocalRepo extends FileRepo { + /** @var bool */ + protected $hasSha1Storage = false; + /** @var array */ protected $fileFactory = array( 'LocalFile', 'newFromTitle' ); @@ -47,6 +50,20 @@ class LocalRepo extends FileRepo { /** @var array */ protected $oldFileFactoryKey = array( 'OldLocalFile', 'newFromKey' ); + function __construct( array $info = null ) { + parent::__construct( $info ); + + $this->hasSha1Storage = isset( $info['storageLayout'] ) && $info['storageLayout'] === 'sha1'; + + if ( $this->hasSha1Storage() ) { + $this->backend = new FileBackendDBRepoWrapper( array( + 'backend' => $this->backend, + 'repoName' => $this->name, + 'dbHandleFactory' => $this->getDBFactory() + ) ); + } + } + /** * @throws MWException * @param stdClass $row @@ -82,6 +99,11 @@ class LocalRepo extends FileRepo { * @return FileRepoStatus */ function cleanupDeletedBatch( array $storageKeys ) { + if ( $this->hasSha1Storage() ) { + wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths\n" ); + return Status::newGood(); + } + $backend = $this->backend; // convenience $root = $this->getZonePath( 'deleted' ); $dbw = $this->getMasterDB(); @@ -90,7 +112,7 @@ class LocalRepo extends FileRepo { foreach ( $storageKeys as $key ) { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; - $dbw->begin( __METHOD__ ); + $dbw->startAtomic( __METHOD__ ); // Check for usage in deleted/hidden files and preemptively // lock the key to avoid any future use until we are finished. $deleted = $this->deletedFileHasKey( $key, 'lock' ); @@ -106,7 +128,7 @@ class LocalRepo extends FileRepo { wfDebug( __METHOD__ . ": $key still in use\n" ); $status->successCount++; } - $dbw->commit( __METHOD__ ); + $dbw->endAtomic( __METHOD__ ); } return $status; @@ -170,7 +192,7 @@ class LocalRepo extends FileRepo { * @return bool|Title */ function checkRedirect( Title $title ) { - global $wgMemc; + $cache = ObjectCache::getMainWANInstance(); $title = File::normalizeTitle( $title, 'exception' ); @@ -181,7 +203,7 @@ class LocalRepo extends FileRepo { } else { $expiry = 86400; // has invalidation, 1 day } - $cachedValue = $wgMemc->get( $memcKey ); + $cachedValue = $cache->get( $memcKey ); if ( $cachedValue === ' ' || $cachedValue === '' ) { // Does not exist return false; @@ -191,7 +213,7 @@ class LocalRepo extends FileRepo { $id = $this->getArticleID( $title ); if ( !$id ) { - $wgMemc->add( $memcKey, " ", $expiry ); + $cache->set( $memcKey, " ", $expiry ); return false; } @@ -205,11 +227,11 @@ class LocalRepo extends FileRepo { if ( $row && $row->rd_namespace == NS_FILE ) { $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title ); - $wgMemc->add( $memcKey, $targetTitle->getDBkey(), $expiry ); + $cache->set( $memcKey, $targetTitle->getDBkey(), $expiry ); return $targetTitle; } else { - $wgMemc->add( $memcKey, '', $expiry ); + $cache->set( $memcKey, '', $expiry ); return false; } @@ -275,17 +297,17 @@ class LocalRepo extends FileRepo { ); }; - $repo = $this; + $that = $this; $applyMatchingFiles = function ( ResultWrapper $res, &$searchSet, &$finalFiles ) - use ( $repo, $fileMatchesSearch, $flags ) + use ( $that, $fileMatchesSearch, $flags ) { global $wgContLang; - $info = $repo->getInfo(); + $info = $that->getInfo(); foreach ( $res as $row ) { - $file = $repo->newFileFromRow( $row ); + $file = $that->newFileFromRow( $row ); // There must have been a search for this DB key, but this has to handle the // cases were title capitalization is different on the client and repo wikis. - $dbKeysLook = array( str_replace( ' ', '_', $file->getName() ) ); + $dbKeysLook = array( strtr( $file->getName(), ' ', '_' ) ); if ( !empty( $info['initialCapital'] ) ) { // Search keys for "hi.png" and "Hi.png" should use the "Hi.png file" $dbKeysLook[] = $wgContLang->lcfirst( $file->getName() ); @@ -470,6 +492,16 @@ class LocalRepo extends FileRepo { } /** + * Get a callback to get a DB handle given an index (DB_SLAVE/DB_MASTER) + * @return Closure + */ + protected function getDBFactory() { + return function( $index ) { + return wfGetDB( $index ); + }; + } + + /** * Get a key on the primary cache for this repository. * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). @@ -489,14 +521,15 @@ class LocalRepo extends FileRepo { * @return void */ function invalidateImageRedirect( Title $title ) { - global $wgMemc; + $cache = ObjectCache::getMainWANInstance(); + $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) ); if ( $memcKey ) { // Set a temporary value for the cache key, to ensure // that this value stays purged long enough so that // it isn't refreshed with a stale value due to a // lagged slave. - $wgMemc->set( $memcKey, ' PURGED', 12 ); + $cache->delete( $memcKey, 12 ); } } @@ -513,4 +546,56 @@ class LocalRepo extends FileRepo { 'favicon' => wfExpandUrl( $wgFavicon ), ) ); } + + public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function storeBatch( array $triplets, $flags = 0 ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function cleanupBatch( array $files, $flags = 0 ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function publish( + $srcPath, + $dstRel, + $archiveRel, + $flags = 0, + array $options = array() + ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function publishBatch( array $ntuples, $flags = 0 ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function delete( $srcRel, $archiveRel ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + public function deleteBatch( array $sourceDestPairs ) { + return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); + } + + /** + * Skips the write operation if storage is sha1-based, executes it normally otherwise + * + * @param string $function + * @param array $args + * @return FileRepoStatus + */ + protected function skipWriteOperationIfSha1( $function, array $args ) { + $this->assertWritableRepo(); // fail out if read-only + + if ( $this->hasSha1Storage() ) { + wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths\n" ); + return Status::newGood(); + } else { + return call_user_func_array( 'parent::' . $function, $args ); + } + } } diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 1d454283..1aec4464 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -485,7 +485,7 @@ class ArchivedFile { if ( $type == 'text' ) { return $this->user_text; } elseif ( $type == 'id' ) { - return $this->user; + return (int)$this->user; } throw new MWException( "Unknown type '$type'." ); @@ -587,6 +587,6 @@ class ArchivedFile { $this->load(); $title = $this->getTitle(); - return Revision::userCanBitfield( $this->deleted, $field, $user, $title ? : null ); + return Revision::userCanBitfield( $this->deleted, $field, $user, $title ?: null ); } } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 6edd6fcc..f40d216c 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -163,7 +163,8 @@ abstract class File implements IDBAccessObject { * @param FileRepo|bool $repo */ function __construct( $title, $repo ) { - if ( $title !== false ) { // subclasses may not use MW titles + // Some subclasses do not use $title, but set name/title some other way + if ( $title !== false ) { $title = self::normalizeTitle( $title, 'exception' ); } $this->title = $title; @@ -212,14 +213,15 @@ abstract class File implements IDBAccessObject { } /** - * Normalize a file extension to the common form, and ensure it's clean. - * Extensions with non-alphanumeric characters will be discarded. + * Normalize a file extension to the common form, making it lowercase and checking some synonyms, + * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded. + * Keep in sync with mw.Title.normalizeExtension() in JS. * - * @param string $ext (without the .) - * @return string + * @param string $extension File extension (without the leading dot) + * @return string File extension in canonical form */ - static function normalizeExtension( $ext ) { - $lower = strtolower( $ext ); + static function normalizeExtension( $extension ) { + $lower = strtolower( $extension ); $squish = array( 'htm' => 'html', 'jpeg' => 'jpg', @@ -420,7 +422,13 @@ abstract class File implements IDBAccessObject { public function getLocalRefPath() { $this->assertRepoDefined(); if ( !isset( $this->fsFile ) ) { + $starttime = microtime( true ); $this->fsFile = $this->repo->getLocalReference( $this->getPath() ); + + $statTiming = microtime( true ) - $starttime; + RequestContext::getMain()->getStats()->timing( + 'media.thumbnail.generate.fetchoriginal', 1000 * $statTiming ); + if ( !$this->fsFile ) { $this->fsFile = false; // null => false; cache negative hits } @@ -1091,7 +1099,9 @@ abstract class File implements IDBAccessObject { * @return bool|MediaTransformOutput */ public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) { - global $wgUseSquid, $wgIgnoreImageErrors; + global $wgIgnoreImageErrors; + + $stats = RequestContext::getMain()->getStats(); $handler = $this->getHandler(); @@ -1108,10 +1118,15 @@ abstract class File implements IDBAccessObject { $this->generateBucketsIfNeeded( $normalisedParams, $flags ); } + $starttime = microtime( true ); + // Actually render the thumbnail... $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams ); $tmpFile->bind( $thumb ); // keep alive with $thumb + $statTiming = microtime( true ) - $starttime; + $stats->timing( 'media.thumbnail.generate.transform', 1000 * $statTiming ); + if ( !$thumb ) { // bad params? $thumb = false; } elseif ( $thumb->isError() ) { // transform error @@ -1122,6 +1137,9 @@ abstract class File implements IDBAccessObject { } } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) { // Copy the thumbnail from the file system into storage... + + $starttime = microtime( true ); + $disposition = $this->getThumbDisposition( $thumbName ); $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition ); if ( $status->isOK() ) { @@ -1129,19 +1147,14 @@ abstract class File implements IDBAccessObject { } else { $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags ); } + + $statTiming = microtime( true ) - $starttime; + $stats->timing( 'media.thumbnail.generate.store', 1000 * $statTiming ); + // Give extensions a chance to do something with this thumbnail... Hooks::run( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) ); } - // Purge. Useful in the event of Core -> Squid connection failure or squid - // purge collisions from elsewhere during failure. Don't keep triggering for - // "thumbs" which have the main image URL though (bug 13776) - if ( $wgUseSquid ) { - if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) { - SquidUpdate::purge( array( $thumbUrl ) ); - } - } - return $thumb; } @@ -1166,13 +1179,13 @@ abstract class File implements IDBAccessObject { return false; } + $starttime = microtime( true ); + $params['physicalWidth'] = $bucket; $params['width'] = $bucket; $params = $this->getHandler()->sanitizeParamsForBucketing( $params ); - $bucketName = $this->getBucketThumbName( $bucket ); - $tmpFile = $this->makeTransformTmpFile( $bucketPath ); if ( !$tmpFile ) { @@ -1181,6 +1194,8 @@ abstract class File implements IDBAccessObject { $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags ); + $buckettime = microtime( true ) - $starttime; + if ( !$thumb || $thumb->isError() ) { return false; } @@ -1190,6 +1205,9 @@ abstract class File implements IDBAccessObject { // this object exists $tmpFile->bind( $this ); + RequestContext::getMain()->getStats()->timing( + 'media.thumbnail.generate.bucket', 1000 * $buckettime ); + return true; } @@ -2230,4 +2248,13 @@ abstract class File implements IDBAccessObject { $handler = $this->getHandler(); return $handler ? $handler->isExpensiveToThumbnail( $this ) : false; } + + /** + * Whether the thumbnails created on the same server as this code is running. + * @since 1.25 + * @return bool + */ + public function isTransformedLocally() { + return true; + } } diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index 3d5d5d60..3c78290c 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -219,11 +219,15 @@ class ForeignAPIFile extends File { } /** - * @param string $method + * @param string $type * @return int|null|string */ - public function getUser( $method = 'text' ) { - return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; + public function getUser( $type = 'text' ) { + if ( $type == 'text' ) { + return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; + } elseif ( $type == 'id' ) { + return 0; // What makes sense here, for a remote user? + } } /** @@ -365,4 +369,13 @@ class ForeignAPIFile extends File { # Clear out the thumbnail directory if empty $this->repo->quickCleanDir( $dir ); } + + /** + * The thumbnail is created on the foreign server and fetched over internet + * @since 1.25 + * @return bool + */ + public function isTransformedLocally() { + return false; + } } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index b4cced38..d2c37e61 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -243,21 +243,19 @@ class LocalFile extends File { * @return bool */ function loadFromCache() { - global $wgMemc; - $this->dataLoaded = false; $this->extraDataLoaded = false; $key = $this->getCacheKey(); if ( !$key ) { - return false; } - $cachedValues = $wgMemc->get( $key ); + $cache = ObjectCache::getMainWANInstance(); + $cachedValues = $cache->get( $key ); // Check if the key existed and belongs to this version of MediaWiki - if ( isset( $cachedValues['version'] ) && $cachedValues['version'] == MW_FILE_VERSION ) { + if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) { wfDebug( "Pulling file metadata from cache key $key\n" ); $this->fileExists = $cachedValues['fileExists']; if ( $this->fileExists ) { @@ -271,9 +269,9 @@ class LocalFile extends File { } if ( $this->dataLoaded ) { - wfIncrStats( 'image_cache_hit' ); + wfIncrStats( 'image_cache.hit' ); } else { - wfIncrStats( 'image_cache_miss' ); + wfIncrStats( 'image_cache.miss' ); } return $this->dataLoaded; @@ -283,22 +281,20 @@ class LocalFile extends File { * Save the file metadata to memcached */ function saveToCache() { - global $wgMemc; - $this->load(); - $key = $this->getCacheKey(); + $key = $this->getCacheKey(); if ( !$key ) { return; } $fields = $this->getCacheFields( '' ); - $cache = array( 'version' => MW_FILE_VERSION ); - $cache['fileExists'] = $this->fileExists; + $cacheVal = array( 'version' => MW_FILE_VERSION ); + $cacheVal['fileExists'] = $this->fileExists; if ( $this->fileExists ) { foreach ( $fields as $field ) { - $cache[$field] = $this->$field; + $cacheVal[$field] = $this->$field; } } @@ -306,13 +302,26 @@ class LocalFile extends File { // If the cache value gets to large it will not fit in memcached and nothing will // get cached at all, causing master queries for any file access. foreach ( $this->getLazyCacheFields( '' ) as $field ) { - if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) { - unset( $cache[$field] ); // don't let the value get too big + if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) { + unset( $cacheVal[$field] ); // don't let the value get too big } } // Cache presence for 1 week and negatives for 1 day - $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 ); + $cache = ObjectCache::getMainWANInstance(); + $cache->set( $key, $cacheVal, $this->fileExists ? 86400 * 7 : 86400 ); + } + + /** + * Purge the file object/metadata cache + */ + function invalidateCache() { + $key = $this->getCacheKey(); + if ( !$key ) { + return; + } + + ObjectCache::getMainWANInstance()->delete( $key ); } /** @@ -493,9 +502,17 @@ class LocalFile extends File { $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime']; } - # Trim zero padding from char/binary field + // Trim zero padding from char/binary field $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); + // Normalize some fields to integer type, per their database definition. + // Use unary + so that overflows will be upgraded to double instead of + // being trucated as with intval(). This is important to allow >2GB + // files on 32-bit systems. + foreach ( array( 'size', 'width', 'height', 'bits' ) as $field ) { + $decoded[$field] = +$decoded[$field]; + } + return $decoded; } @@ -612,7 +629,7 @@ class LocalFile extends File { __METHOD__ ); - $this->saveToCache(); + $this->invalidateCache(); $this->unlock(); // done @@ -734,7 +751,7 @@ class LocalFile extends File { if ( $type == 'text' ) { return $this->user_text; } elseif ( $type == 'id' ) { - return $this->user; + return (int)$this->user; } } @@ -753,7 +770,7 @@ class LocalFile extends File { function getBitDepth() { $this->load(); - return $this->bits; + return (int)$this->bits; } /** @@ -842,25 +859,7 @@ class LocalFile extends File { * Refresh metadata in memcached, but don't touch thumbnails or squid */ function purgeMetadataCache() { - $this->loadFromDB( File::READ_LATEST ); - $this->saveToCache(); - $this->purgeHistory(); - } - - /** - * Purge the shared history (OldLocalFile) cache. - * - * @note This used to purge old thumbnails as well. - */ - function purgeHistory() { - global $wgMemc; - - $hashedName = md5( $this->getName() ); - $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName ); - - if ( $oldKey ) { - $wgMemc->delete( $oldKey ); - } + $this->invalidateCache(); } /** @@ -1406,11 +1405,8 @@ class LocalFile extends File { # to after $wikiPage->doEdit has been called. $dbw->commit( __METHOD__ ); - # Save to memcache. - # We shall not saveToCache before the commit since otherwise - # in case of a rollback there is an usable file from memcached - # which in fact doesn't really exist (bug 24978) - $this->saveToCache(); + # Update memcache after the commit + $this->invalidateCache(); if ( $exists ) { # Invalidate the cache for the description page @@ -1471,7 +1467,7 @@ class LocalFile extends File { * The archive name should be passed through to recordUpload for database * registration. * - * @param string $srcPath Local filesystem path to the source image + * @param string $srcPath Local filesystem path or virtual URL to the source image * @param int $flags A bitwise combination of: * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy * @param array $options Optional additional parameters @@ -1489,7 +1485,7 @@ class LocalFile extends File { * The archive name should be passed through to recordUpload for database * registration. * - * @param string $srcPath Local filesystem path to the source image + * @param string $srcPath Local filesystem path or virtual URL to the source image * @param string $dstRel Target relative path * @param int $flags A bitwise combination of: * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy @@ -1498,7 +1494,8 @@ class LocalFile extends File { * archive name, or an empty string if it was a new file. */ function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) { - if ( $this->getRepo()->getReadOnlyReason() !== false ) { + $repo = $this->getRepo(); + if ( $repo->getReadOnlyReason() !== false ) { return $this->readOnlyFatalStatus(); } @@ -1506,13 +1503,29 @@ class LocalFile extends File { $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, $options ); - if ( $status->value == 'new' ) { - $status->value = ''; + if ( $repo->hasSha1Storage() ) { + $sha1 = $repo->isVirtualUrl( $srcPath ) + ? $repo->getFileSha1( $srcPath ) + : File::sha1Base36( $srcPath ); + $dst = $repo->getBackend()->getPathForSHA1( $sha1 ); + $status = $repo->quickImport( $srcPath, $dst ); + if ( $flags & File::DELETE_SOURCE ) { + unlink( $srcPath ); + } + + if ( $this->exists() ) { + $status->value = $archiveName; + } } else { - $status->value = $archiveName; + $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; + $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options ); + + if ( $status->value == 'new' ) { + $status->value = ''; + } else { + $status->value = $archiveName; + } } $this->unlock(); // done @@ -1612,21 +1625,21 @@ class LocalFile extends File { // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside. - $file = $this; + $that = $this; $this->getRepo()->getMasterDB()->onTransactionIdle( - function () use ( $file, $archiveNames ) { + function () use ( $that, $archiveNames ) { global $wgUseSquid; - $file->purgeEverything(); + $that->purgeEverything(); foreach ( $archiveNames as $archiveName ) { - $file->purgeOldThumbnails( $archiveName ); + $that->purgeOldThumbnails( $archiveName ); } if ( $wgUseSquid ) { // Purge the squid $purgeUrls = array(); foreach ( $archiveNames as $archiveName ) { - $purgeUrls[] = $file->getArchiveUrl( $archiveName ); + $purgeUrls[] = $that->getArchiveUrl( $archiveName ); } SquidUpdate::purge( $purgeUrls ); } @@ -1667,7 +1680,6 @@ class LocalFile extends File { $this->purgeOldThumbnails( $archiveName ); if ( $status->isOK() ) { $this->purgeDescription(); - $this->purgeHistory(); } if ( $wgUseSquid ) { @@ -1811,7 +1823,7 @@ class LocalFile extends File { array( 'img_sha1' => $this->sha1 ), array( 'img_name' => $this->getName() ), __METHOD__ ); - $this->saveToCache(); + $this->invalidateCache(); } $this->unlock(); // done @@ -1954,14 +1966,14 @@ class LocalFileDeleteBatch { $this->status = $file->repo->newGood(); } - function addCurrent() { + public function addCurrent() { $this->srcRels['.'] = $this->file->getRel(); } /** * @param string $oldName */ - function addOld( $oldName ) { + public function addOld( $oldName ) { $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName ); $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName ); } @@ -1970,7 +1982,7 @@ class LocalFileDeleteBatch { * Add the old versions of the image to the batch * @return array List of archive names from old versions */ - function addOlds() { + public function addOlds() { $archiveNames = array(); $dbw = $this->file->repo->getMasterDB(); @@ -1991,7 +2003,7 @@ class LocalFileDeleteBatch { /** * @return array */ - function getOldRels() { + protected function getOldRels() { if ( !isset( $this->srcRels['.'] ) ) { $oldRels =& $this->srcRels; $deleteCurrent = false; @@ -2063,7 +2075,7 @@ class LocalFileDeleteBatch { return $hashes; } - function doDBInserts() { + protected function doDBInserts() { $dbw = $this->file->repo->getMasterDB(); $encTimestamp = $dbw->addQuotes( $dbw->timestamp() ); $encUserId = $dbw->addQuotes( $this->user->getId() ); @@ -2178,8 +2190,8 @@ class LocalFileDeleteBatch { * Run the transaction * @return FileRepoStatus */ - function execute() { - + public function execute() { + $repo = $this->file->getRepo(); $this->file->lock(); // Prepare deletion batch @@ -2193,7 +2205,7 @@ class LocalFileDeleteBatch { if ( isset( $hashes[$name] ) ) { $hash = $hashes[$name]; $key = $hash . $dotExt; - $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $dstRel = $repo->getDeletedHashPath( $key ) . $key; $this->deletionBatch[$name] = array( $srcRel, $dstRel ); } } @@ -2206,20 +2218,22 @@ class LocalFileDeleteBatch { // them in a separate transaction, then run the file ops, then update the fa_name fields. $this->doDBInserts(); - // Removes non-existent file from the batch, so we don't get errors. - // This also handles files in the 'deleted' zone deleted via revision deletion. - $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch ); - if ( !$checkStatus->isGood() ) { - $this->status->merge( $checkStatus ); - return $this->status; - } - $this->deletionBatch = $checkStatus->value; + if ( !$repo->hasSha1Storage() ) { + // Removes non-existent file from the batch, so we don't get errors. + // This also handles files in the 'deleted' zone deleted via revision deletion. + $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch ); + if ( !$checkStatus->isGood() ) { + $this->status->merge( $checkStatus ); + return $this->status; + } + $this->deletionBatch = $checkStatus->value; - // Execute the file deletion batch - $status = $this->file->repo->deleteBatch( $this->deletionBatch ); + // Execute the file deletion batch + $status = $this->file->repo->deleteBatch( $this->deletionBatch ); - if ( !$status->isGood() ) { - $this->status->merge( $status ); + if ( !$status->isGood() ) { + $this->status->merge( $status ); + } } if ( !$this->status->isOK() ) { @@ -2245,7 +2259,7 @@ class LocalFileDeleteBatch { * @param array $batch * @return Status */ - function removeNonexistentFiles( $batch ) { + protected function removeNonexistentFiles( $batch ) { $files = $newBatch = array(); foreach ( $batch as $batchItem ) { @@ -2306,7 +2320,7 @@ class LocalFileRestoreBatch { * Add a file by ID * @param int $fa_id */ - function addId( $fa_id ) { + public function addId( $fa_id ) { $this->ids[] = $fa_id; } @@ -2314,14 +2328,14 @@ class LocalFileRestoreBatch { * Add a whole lot of files by ID * @param int[] $ids */ - function addIds( $ids ) { + public function addIds( $ids ) { $this->ids = array_merge( $this->ids, $ids ); } /** * Add all revisions of the file */ - function addAll() { + public function addAll() { $this->all = true; } @@ -2333,12 +2347,13 @@ class LocalFileRestoreBatch { * So we save the batch and let the caller call cleanup() * @return FileRepoStatus */ - function execute() { + public function execute() { global $wgLang; + $repo = $this->file->getRepo(); if ( !$this->all && !$this->ids ) { // Do nothing - return $this->file->repo->newGood(); + return $repo->newGood(); } $lockOwnsTrx = $this->file->lock(); @@ -2395,9 +2410,9 @@ class LocalFileRestoreBatch { continue; } - $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . + $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key; - $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; + $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel; if ( isset( $row->fa_sha1 ) ) { $sha1 = $row->fa_sha1; @@ -2511,27 +2526,29 @@ class LocalFileRestoreBatch { $status->error( 'undelete-missing-filearchive', $id ); } - // Remove missing files from batch, so we don't get errors when undeleting them - $checkStatus = $this->removeNonexistentFiles( $storeBatch ); - if ( !$checkStatus->isGood() ) { - $status->merge( $checkStatus ); - return $status; - } - $storeBatch = $checkStatus->value; + if ( !$repo->hasSha1Storage() ) { + // Remove missing files from batch, so we don't get errors when undeleting them + $checkStatus = $this->removeNonexistentFiles( $storeBatch ); + if ( !$checkStatus->isGood() ) { + $status->merge( $checkStatus ); + return $status; + } + $storeBatch = $checkStatus->value; - // Run the store batch - // Use the OVERWRITE_SAME flag to smooth over a common error - $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); - $status->merge( $storeStatus ); + // Run the store batch + // Use the OVERWRITE_SAME flag to smooth over a common error + $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); + $status->merge( $storeStatus ); - if ( !$status->isGood() ) { - // Even if some files could be copied, fail entirely as that is the - // easiest thing to do without data loss - $this->cleanupFailedBatch( $storeStatus, $storeBatch ); - $status->ok = false; - $this->file->unlock(); + if ( !$status->isGood() ) { + // Even if some files could be copied, fail entirely as that is the + // easiest thing to do without data loss + $this->cleanupFailedBatch( $storeStatus, $storeBatch ); + $status->ok = false; + $this->file->unlock(); - return $status; + return $status; + } } // Run the DB updates @@ -2555,7 +2572,7 @@ class LocalFileRestoreBatch { } // If store batch is empty (all files are missing), deletion is to be considered successful - if ( $status->successCount > 0 || !$storeBatch ) { + if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) { if ( !$exists ) { wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" ); @@ -2565,7 +2582,6 @@ class LocalFileRestoreBatch { } else { wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" ); $this->file->purgeDescription(); - $this->file->purgeHistory(); } } @@ -2579,7 +2595,7 @@ class LocalFileRestoreBatch { * @param array $triplets * @return Status */ - function removeNonexistentFiles( $triplets ) { + protected function removeNonexistentFiles( $triplets ) { $files = $filteredTriplets = array(); foreach ( $triplets as $file ) { $files[$file[0]] = $file[0]; @@ -2605,7 +2621,7 @@ class LocalFileRestoreBatch { * @param array $batch * @return array */ - function removeNonexistentFromCleanup( $batch ) { + protected function removeNonexistentFromCleanup( $batch ) { $files = $newBatch = array(); $repo = $this->file->repo; @@ -2630,7 +2646,7 @@ class LocalFileRestoreBatch { * This should be called from outside the transaction in which execute() was called. * @return FileRepoStatus */ - function cleanup() { + public function cleanup() { if ( !$this->cleanupBatch ) { return $this->file->repo->newGood(); } @@ -2649,7 +2665,7 @@ class LocalFileRestoreBatch { * @param Status $storeStatus * @param array $storeBatch */ - function cleanupFailedBatch( $storeStatus, $storeBatch ) { + protected function cleanupFailedBatch( $storeStatus, $storeBatch ) { $cleanupBatch = array(); foreach ( $storeStatus->success as $i => $success ) { @@ -2707,7 +2723,7 @@ class LocalFileMoveBatch { /** * Add the current image to the batch */ - function addCurrent() { + public function addCurrent() { $this->cur = array( $this->oldRel, $this->newRel ); } @@ -2715,7 +2731,7 @@ class LocalFileMoveBatch { * Add the old versions of the image to the batch * @return array List of archive names from old versions */ - function addOlds() { + public function addOlds() { $archiveBase = 'archive'; $this->olds = array(); $this->oldCount = 0; @@ -2765,7 +2781,7 @@ class LocalFileMoveBatch { * Perform the move. * @return FileRepoStatus */ - function execute() { + public function execute() { $repo = $this->file->repo; $status = $repo->newGood(); @@ -2796,22 +2812,26 @@ class LocalFileMoveBatch { wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " . "{$statusDb->successCount} successes, {$statusDb->failCount} failures" ); - // Copy the files into their new location. - // If a prior process fataled copying or cleaning up files we tolerate any - // of the existing files if they are identical to the ones being stored. - $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME ); - wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " . - "{$statusMove->successCount} successes, {$statusMove->failCount} failures" ); - if ( !$statusMove->isGood() ) { - // Delete any files copied over (while the destination is still locked) - $this->cleanupTarget( $triplets ); - $destFile->unlock(); - $this->file->unlockAndRollback(); // unlocks the destination - wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); - $statusMove->ok = false; - - return $statusMove; + if ( !$repo->hasSha1Storage() ) { + // Copy the files into their new location. + // If a prior process fataled copying or cleaning up files we tolerate any + // of the existing files if they are identical to the ones being stored. + $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME ); + wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " . + "{$statusMove->successCount} successes, {$statusMove->failCount} failures" ); + if ( !$statusMove->isGood() ) { + // Delete any files copied over (while the destination is still locked) + $this->cleanupTarget( $triplets ); + $destFile->unlock(); + $this->file->unlockAndRollback(); // unlocks the destination + wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); + $statusMove->ok = false; + + return $statusMove; + } + $status->merge( $statusMove ); } + $destFile->unlock(); $this->file->unlock(); // done @@ -2819,7 +2839,6 @@ class LocalFileMoveBatch { $this->cleanupSource( $triplets ); $status->merge( $statusDb ); - $status->merge( $statusMove ); return $status; } @@ -2830,7 +2849,7 @@ class LocalFileMoveBatch { * * @return FileRepoStatus */ - function doDBUpdates() { + protected function doDBUpdates() { $repo = $this->file->repo; $status = $repo->newGood(); $dbw = $this->db; @@ -2882,7 +2901,7 @@ class LocalFileMoveBatch { * Generate triplets for FileRepo::storeBatch(). * @return array */ - function getMoveTriplets() { + protected function getMoveTriplets() { $moves = array_merge( array( $this->cur ), $this->olds ); $triplets = array(); // The format is: (srcUrl, destZone, destUrl) @@ -2904,7 +2923,7 @@ class LocalFileMoveBatch { * @param array $triplets * @return Status */ - function removeNonexistentFiles( $triplets ) { + protected function removeNonexistentFiles( $triplets ) { $files = array(); foreach ( $triplets as $file ) { @@ -2934,7 +2953,7 @@ class LocalFileMoveBatch { * files. Called if something went wrong half way. * @param array $triplets */ - function cleanupTarget( $triplets ) { + protected function cleanupTarget( $triplets ) { // Create dest pairs from the triplets $pairs = array(); foreach ( $triplets as $triplet ) { @@ -2950,7 +2969,7 @@ class LocalFileMoveBatch { * Called at the end of the move process if everything else went ok. * @param array $triplets */ - function cleanupSource( $triplets ) { + protected function cleanupSource( $triplets ) { // Create source file names from the triplets $files = array(); foreach ( $triplets as $triplet ) { |