123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- <?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
- */
- use Wikimedia\Rdbms\DBConnRef;
- /**
- * @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 latter 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 ) {
- /** @var FileBackend $backend */
- $backend = $config['backend'];
- $config['name'] = $backend->getName();
- $config['wikiId'] = $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( [ $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 string[] $paths
- * @param bool $latest
- * @return string[] Translated paths in same order
- */
- public function getBackendPaths( array $paths, $latest = true ) {
- $db = $this->getDB( $latest ? DB_MASTER : DB_REPLICA );
- // @TODO: batching
- $resolved = [];
- 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',
- [ 'oi_archive_name' => $name ],
- __METHOD__
- );
- } else {
- $sha1 = $db->selectField( 'image', 'img_sha1',
- [ '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 = [];
- 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, StatusValue $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 int $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
- * @return mixed
- */
- 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 = [];
- 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 = [ '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;
- }
- }
|