FileBackendDBRepoWrapper.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * Proxy backend that manages file layout rewriting for FileRepo.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup FileRepo
  22. * @ingroup FileBackend
  23. */
  24. use Wikimedia\Rdbms\DBConnRef;
  25. /**
  26. * @brief Proxy backend that manages file layout rewriting for FileRepo.
  27. *
  28. * LocalRepo may be configured to store files under their title names or by SHA-1.
  29. * This acts as a shim in the latter case, providing backwards compatability for
  30. * most callers. All "public"/"deleted" zone files actually go in an "original"
  31. * container and are never changed.
  32. *
  33. * This requires something like thumb_handler.php and img_auth.php for client viewing of files.
  34. *
  35. * @ingroup FileRepo
  36. * @ingroup FileBackend
  37. * @since 1.25
  38. */
  39. class FileBackendDBRepoWrapper extends FileBackend {
  40. /** @var FileBackend */
  41. protected $backend;
  42. /** @var string */
  43. protected $repoName;
  44. /** @var Closure */
  45. protected $dbHandleFunc;
  46. /** @var ProcessCacheLRU */
  47. protected $resolvedPathCache;
  48. /** @var DBConnRef[] */
  49. protected $dbs;
  50. public function __construct( array $config ) {
  51. /** @var FileBackend $backend */
  52. $backend = $config['backend'];
  53. $config['name'] = $backend->getName();
  54. $config['wikiId'] = $backend->getWikiId();
  55. parent::__construct( $config );
  56. $this->backend = $config['backend'];
  57. $this->repoName = $config['repoName'];
  58. $this->dbHandleFunc = $config['dbHandleFactory'];
  59. $this->resolvedPathCache = new ProcessCacheLRU( 100 );
  60. }
  61. /**
  62. * Get the underlying FileBackend that is being wrapped
  63. *
  64. * @return FileBackend
  65. */
  66. public function getInternalBackend() {
  67. return $this->backend;
  68. }
  69. /**
  70. * Translate a legacy "title" path to it's "sha1" counterpart
  71. *
  72. * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
  73. * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
  74. *
  75. * @param string $path
  76. * @param bool $latest
  77. * @return string
  78. */
  79. public function getBackendPath( $path, $latest = true ) {
  80. $paths = $this->getBackendPaths( [ $path ], $latest );
  81. return current( $paths );
  82. }
  83. /**
  84. * Translate legacy "title" paths to their "sha1" counterparts
  85. *
  86. * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
  87. * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
  88. *
  89. * @param string[] $paths
  90. * @param bool $latest
  91. * @return string[] Translated paths in same order
  92. */
  93. public function getBackendPaths( array $paths, $latest = true ) {
  94. $db = $this->getDB( $latest ? DB_MASTER : DB_REPLICA );
  95. // @TODO: batching
  96. $resolved = [];
  97. foreach ( $paths as $i => $path ) {
  98. if ( !$latest && $this->resolvedPathCache->has( $path, 'target', 10 ) ) {
  99. $resolved[$i] = $this->resolvedPathCache->get( $path, 'target' );
  100. continue;
  101. }
  102. list( , $container ) = FileBackend::splitStoragePath( $path );
  103. if ( $container === "{$this->repoName}-public" ) {
  104. $name = basename( $path );
  105. if ( strpos( $path, '!' ) !== false ) {
  106. $sha1 = $db->selectField( 'oldimage', 'oi_sha1',
  107. [ 'oi_archive_name' => $name ],
  108. __METHOD__
  109. );
  110. } else {
  111. $sha1 = $db->selectField( 'image', 'img_sha1',
  112. [ 'img_name' => $name ],
  113. __METHOD__
  114. );
  115. }
  116. if ( !strlen( $sha1 ) ) {
  117. $resolved[$i] = $path; // give up
  118. continue;
  119. }
  120. $resolved[$i] = $this->getPathForSHA1( $sha1 );
  121. $this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
  122. } elseif ( $container === "{$this->repoName}-deleted" ) {
  123. $name = basename( $path ); // <hash>.<ext>
  124. $sha1 = substr( $name, 0, strpos( $name, '.' ) ); // ignore extension
  125. $resolved[$i] = $this->getPathForSHA1( $sha1 );
  126. $this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
  127. } else {
  128. $resolved[$i] = $path;
  129. }
  130. }
  131. $res = [];
  132. foreach ( $paths as $i => $path ) {
  133. $res[$i] = $resolved[$i];
  134. }
  135. return $res;
  136. }
  137. protected function doOperationsInternal( array $ops, array $opts ) {
  138. return $this->backend->doOperationsInternal( $this->mungeOpPaths( $ops ), $opts );
  139. }
  140. protected function doQuickOperationsInternal( array $ops ) {
  141. return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ) );
  142. }
  143. protected function doPrepare( array $params ) {
  144. return $this->backend->doPrepare( $params );
  145. }
  146. protected function doSecure( array $params ) {
  147. return $this->backend->doSecure( $params );
  148. }
  149. protected function doPublish( array $params ) {
  150. return $this->backend->doPublish( $params );
  151. }
  152. protected function doClean( array $params ) {
  153. return $this->backend->doClean( $params );
  154. }
  155. public function concatenate( array $params ) {
  156. return $this->translateSrcParams( __FUNCTION__, $params );
  157. }
  158. public function fileExists( array $params ) {
  159. return $this->translateSrcParams( __FUNCTION__, $params );
  160. }
  161. public function getFileTimestamp( array $params ) {
  162. return $this->translateSrcParams( __FUNCTION__, $params );
  163. }
  164. public function getFileSize( array $params ) {
  165. return $this->translateSrcParams( __FUNCTION__, $params );
  166. }
  167. public function getFileStat( array $params ) {
  168. return $this->translateSrcParams( __FUNCTION__, $params );
  169. }
  170. public function getFileXAttributes( array $params ) {
  171. return $this->translateSrcParams( __FUNCTION__, $params );
  172. }
  173. public function getFileSha1Base36( array $params ) {
  174. return $this->translateSrcParams( __FUNCTION__, $params );
  175. }
  176. public function getFileProps( array $params ) {
  177. return $this->translateSrcParams( __FUNCTION__, $params );
  178. }
  179. public function streamFile( array $params ) {
  180. // The stream methods use the file extension to determine the
  181. // Content-Type (as MediaWiki should already validate it on upload).
  182. // The translated SHA1 path has no extension, so this needs to use
  183. // the untranslated path extension.
  184. $type = StreamFile::contentTypeFromPath( $params['src'] );
  185. if ( $type && $type != 'unknown/unknown' ) {
  186. $params['headers'][] = "Content-type: $type";
  187. }
  188. return $this->translateSrcParams( __FUNCTION__, $params );
  189. }
  190. public function getFileContentsMulti( array $params ) {
  191. return $this->translateArrayResults( __FUNCTION__, $params );
  192. }
  193. public function getLocalReferenceMulti( array $params ) {
  194. return $this->translateArrayResults( __FUNCTION__, $params );
  195. }
  196. public function getLocalCopyMulti( array $params ) {
  197. return $this->translateArrayResults( __FUNCTION__, $params );
  198. }
  199. public function getFileHttpUrl( array $params ) {
  200. return $this->translateSrcParams( __FUNCTION__, $params );
  201. }
  202. public function directoryExists( array $params ) {
  203. return $this->backend->directoryExists( $params );
  204. }
  205. public function getDirectoryList( array $params ) {
  206. return $this->backend->getDirectoryList( $params );
  207. }
  208. public function getFileList( array $params ) {
  209. return $this->backend->getFileList( $params );
  210. }
  211. public function getFeatures() {
  212. return $this->backend->getFeatures();
  213. }
  214. public function clearCache( array $paths = null ) {
  215. $this->backend->clearCache( null ); // clear all
  216. }
  217. public function preloadCache( array $paths ) {
  218. $paths = $this->getBackendPaths( $paths );
  219. $this->backend->preloadCache( $paths );
  220. }
  221. public function preloadFileStat( array $params ) {
  222. return $this->translateSrcParams( __FUNCTION__, $params );
  223. }
  224. public function getScopedLocksForOps( array $ops, StatusValue $status ) {
  225. return $this->backend->getScopedLocksForOps( $ops, $status );
  226. }
  227. /**
  228. * Get the ultimate original storage path for a file
  229. *
  230. * Use this when putting a new file into the system
  231. *
  232. * @param string $sha1 File SHA-1 base36
  233. * @return string
  234. */
  235. public function getPathForSHA1( $sha1 ) {
  236. if ( strlen( $sha1 ) < 3 ) {
  237. throw new InvalidArgumentException( "Invalid file SHA-1." );
  238. }
  239. return $this->backend->getContainerStoragePath( "{$this->repoName}-original" ) .
  240. "/{$sha1[0]}/{$sha1[1]}/{$sha1[2]}/{$sha1}";
  241. }
  242. /**
  243. * Get a connection to the repo file registry DB
  244. *
  245. * @param int $index
  246. * @return DBConnRef
  247. */
  248. protected function getDB( $index ) {
  249. if ( !isset( $this->dbs[$index] ) ) {
  250. $func = $this->dbHandleFunc;
  251. $this->dbs[$index] = $func( $index );
  252. }
  253. return $this->dbs[$index];
  254. }
  255. /**
  256. * Translates paths found in the "src" or "srcs" keys of a params array
  257. *
  258. * @param string $function
  259. * @param array $params
  260. * @return mixed
  261. */
  262. protected function translateSrcParams( $function, array $params ) {
  263. $latest = !empty( $params['latest'] );
  264. if ( isset( $params['src'] ) ) {
  265. $params['src'] = $this->getBackendPath( $params['src'], $latest );
  266. }
  267. if ( isset( $params['srcs'] ) ) {
  268. $params['srcs'] = $this->getBackendPaths( $params['srcs'], $latest );
  269. }
  270. return $this->backend->$function( $params );
  271. }
  272. /**
  273. * Translates paths when the backend function returns results keyed by paths
  274. *
  275. * @param string $function
  276. * @param array $params
  277. * @return array
  278. */
  279. protected function translateArrayResults( $function, array $params ) {
  280. $origPaths = $params['srcs'];
  281. $params['srcs'] = $this->getBackendPaths( $params['srcs'], !empty( $params['latest'] ) );
  282. $pathMap = array_combine( $params['srcs'], $origPaths );
  283. $results = $this->backend->$function( $params );
  284. $contents = [];
  285. foreach ( $results as $path => $result ) {
  286. $contents[$pathMap[$path]] = $result;
  287. }
  288. return $contents;
  289. }
  290. /**
  291. * Translate legacy "title" source paths to their "sha1" counterparts
  292. *
  293. * This leaves destination paths alone since we don't want those to mutate
  294. *
  295. * @param array[] $ops
  296. * @return array[]
  297. */
  298. protected function mungeOpPaths( array $ops ) {
  299. // Ops that use 'src' and do not mutate core file data there
  300. static $srcRefOps = [ 'store', 'copy', 'describe' ];
  301. foreach ( $ops as &$op ) {
  302. if ( isset( $op['src'] ) && in_array( $op['op'], $srcRefOps ) ) {
  303. $op['src'] = $this->getBackendPath( $op['src'], true );
  304. }
  305. if ( isset( $op['srcs'] ) ) {
  306. $op['srcs'] = $this->getBackendPaths( $op['srcs'], true );
  307. }
  308. }
  309. return $ops;
  310. }
  311. }