Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
35.00% |
91 / 260 |
|
50.00% |
16 / 32 |
CRAP | |
0.00% |
0 / 1 |
LocalRepo | |
35.14% |
91 / 259 |
|
50.00% |
16 / 32 |
2453.45 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
newFileFromRow | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
newFromArchiveName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
cleanupDeletedBatch | |
12.50% |
3 / 24 |
|
0.00% |
0 / 1 |
30.12 | |||
deletedFileHasKey | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
hiddenFileHasKey | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
getHashFromKey | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
checkRedirect | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
6.01 | |||
findFiles | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
1056 | |||
findBySha1 | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
findBySha1s | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
findFilesByPrefix | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getReplicaDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPrimaryDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDBFactory | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
2.86 | |||
hasAcessibleSharedCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSharedCacheKey | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
invalidateImageRedirect | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
store | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
storeBatch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
cleanupBatch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
publish | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
publishBatch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
delete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
deleteBatch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
skipWriteOperationIfSha1 | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
isJsonMetadataEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isSplitMetadataEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getSplitMetadataThreshold | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isMetadataUpdateEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isMetadataReserializeEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBlobStore | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\FileRepo; |
22 | |
23 | use Closure; |
24 | use InvalidArgumentException; |
25 | use MediaWiki\Context\RequestContext; |
26 | use MediaWiki\FileRepo\File\File; |
27 | use MediaWiki\FileRepo\File\FileSelectQueryBuilder; |
28 | use MediaWiki\FileRepo\File\LocalFile; |
29 | use MediaWiki\FileRepo\File\OldLocalFile; |
30 | use MediaWiki\Linker\LinkTarget; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Page\PageIdentity; |
33 | use MediaWiki\Permissions\Authority; |
34 | use MediaWiki\Status\Status; |
35 | use MediaWiki\Storage\BlobStore; |
36 | use MediaWiki\Title\Title; |
37 | use MediaWiki\WikiMap\WikiMap; |
38 | use stdClass; |
39 | use Wikimedia\ObjectCache\WANObjectCache; |
40 | use Wikimedia\Rdbms\Database; |
41 | use Wikimedia\Rdbms\IConnectionProvider; |
42 | use Wikimedia\Rdbms\IDatabase; |
43 | use Wikimedia\Rdbms\IExpression; |
44 | use Wikimedia\Rdbms\IReadableDatabase; |
45 | use Wikimedia\Rdbms\IResultWrapper; |
46 | use Wikimedia\Rdbms\LikeValue; |
47 | |
48 | /** |
49 | * Local repository that stores files in the local filesystem and registers them |
50 | * in the wiki's own database. |
51 | * |
52 | * This is the most commonly used file repository class. |
53 | * |
54 | * @ingroup FileRepo |
55 | * @method LocalFile|null newFile( $title, $time = false ) |
56 | */ |
57 | class LocalRepo extends FileRepo { |
58 | /** @var callable */ |
59 | protected $fileFactory = [ LocalFile::class, 'newFromTitle' ]; |
60 | /** @var callable */ |
61 | protected $fileFactoryKey = [ LocalFile::class, 'newFromKey' ]; |
62 | /** @var callable */ |
63 | protected $fileFromRowFactory = [ LocalFile::class, 'newFromRow' ]; |
64 | /** @var callable */ |
65 | protected $oldFileFromRowFactory = [ OldLocalFile::class, 'newFromRow' ]; |
66 | /** @var callable */ |
67 | protected $oldFileFactory = [ OldLocalFile::class, 'newFromTitle' ]; |
68 | /** @var callable */ |
69 | protected $oldFileFactoryKey = [ OldLocalFile::class, 'newFromKey' ]; |
70 | |
71 | /** @var string DB domain of the repo wiki */ |
72 | protected $dbDomain; |
73 | protected IConnectionProvider $dbProvider; |
74 | /** @var bool Whether shared cache keys are exposed/accessible */ |
75 | protected $hasAccessibleSharedCache; |
76 | |
77 | /** @var BlobStore */ |
78 | protected $blobStore; |
79 | |
80 | /** @var bool */ |
81 | protected $useJsonMetadata = true; |
82 | |
83 | /** @var bool */ |
84 | protected $useSplitMetadata = false; |
85 | |
86 | /** @var int|null */ |
87 | protected $splitMetadataThreshold = 1000; |
88 | |
89 | /** @var bool */ |
90 | protected $updateCompatibleMetadata = false; |
91 | |
92 | /** @var bool */ |
93 | protected $reserializeMetadata = false; |
94 | |
95 | public function __construct( ?array $info = null ) { |
96 | parent::__construct( $info ); |
97 | |
98 | $this->dbDomain = WikiMap::getCurrentWikiDbDomain(); |
99 | $this->hasAccessibleSharedCache = true; |
100 | |
101 | $this->hasSha1Storage = ( $info['storageLayout'] ?? null ) === 'sha1'; |
102 | $this->dbProvider = MediaWikiServices::getInstance()->getConnectionProvider(); |
103 | |
104 | if ( $this->hasSha1Storage() ) { |
105 | $this->backend = new FileBackendDBRepoWrapper( [ |
106 | 'backend' => $this->backend, |
107 | 'repoName' => $this->name, |
108 | 'dbHandleFactory' => $this->getDBFactory() |
109 | ] ); |
110 | } |
111 | |
112 | foreach ( |
113 | [ |
114 | 'useJsonMetadata', |
115 | 'useSplitMetadata', |
116 | 'splitMetadataThreshold', |
117 | 'updateCompatibleMetadata', |
118 | 'reserializeMetadata', |
119 | ] as $option |
120 | ) { |
121 | if ( isset( $info[$option] ) ) { |
122 | $this->$option = $info[$option]; |
123 | } |
124 | } |
125 | } |
126 | |
127 | /** |
128 | * @param stdClass $row |
129 | * @return LocalFile |
130 | */ |
131 | public function newFileFromRow( $row ) { |
132 | if ( isset( $row->img_name ) ) { |
133 | return ( $this->fileFromRowFactory )( $row, $this ); |
134 | } elseif ( isset( $row->oi_name ) ) { |
135 | return ( $this->oldFileFromRowFactory )( $row, $this ); |
136 | } else { |
137 | throw new InvalidArgumentException( __METHOD__ . ': invalid row' ); |
138 | } |
139 | } |
140 | |
141 | /** |
142 | * @param PageIdentity|LinkTarget|string $title |
143 | * @param string $archiveName |
144 | * @return OldLocalFile |
145 | */ |
146 | public function newFromArchiveName( $title, $archiveName ) { |
147 | $title = File::normalizeTitle( $title ); |
148 | return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); |
149 | } |
150 | |
151 | /** |
152 | * Delete files in the deleted directory if they are not referenced in the |
153 | * filearchive table. This needs to be done in the repo because it needs to |
154 | * interleave database locks with file operations, which is potentially a |
155 | * remote operation. |
156 | * |
157 | * @param string[] $storageKeys |
158 | * |
159 | * @return Status |
160 | */ |
161 | public function cleanupDeletedBatch( array $storageKeys ) { |
162 | if ( $this->hasSha1Storage() ) { |
163 | wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths" ); |
164 | return Status::newGood(); |
165 | } |
166 | |
167 | $backend = $this->backend; // convenience |
168 | $root = $this->getZonePath( 'deleted' ); |
169 | $dbw = $this->getPrimaryDB(); |
170 | $status = $this->newGood(); |
171 | $storageKeys = array_unique( $storageKeys ); |
172 | foreach ( $storageKeys as $key ) { |
173 | $hashPath = $this->getDeletedHashPath( $key ); |
174 | $path = "$root/$hashPath$key"; |
175 | $dbw->startAtomic( __METHOD__ ); |
176 | // Check for usage in deleted/hidden files and preemptively |
177 | // lock the key to avoid any future use until we are finished. |
178 | $deleted = $this->deletedFileHasKey( $key, 'lock' ); |
179 | $hidden = $this->hiddenFileHasKey( $key, 'lock' ); |
180 | if ( !$deleted && !$hidden ) { // not in use now |
181 | wfDebug( __METHOD__ . ": deleting $key" ); |
182 | $op = [ 'op' => 'delete', 'src' => $path ]; |
183 | if ( !$backend->doOperation( $op )->isOK() ) { |
184 | $status->error( 'undelete-cleanup-error', $path ); |
185 | $status->failCount++; |
186 | } |
187 | } else { |
188 | wfDebug( __METHOD__ . ": $key still in use" ); |
189 | $status->successCount++; |
190 | } |
191 | $dbw->endAtomic( __METHOD__ ); |
192 | } |
193 | |
194 | return $status; |
195 | } |
196 | |
197 | /** |
198 | * Check if a deleted (filearchive) file has this sha1 key |
199 | * |
200 | * @param string $key File storage key (base-36 sha1 key with file extension) |
201 | * @param string|null $lock Use "lock" to lock the row via FOR UPDATE |
202 | * @return bool File with this key is in use |
203 | */ |
204 | protected function deletedFileHasKey( $key, $lock = null ) { |
205 | $queryBuilder = $this->getPrimaryDB()->newSelectQueryBuilder() |
206 | ->select( '1' ) |
207 | ->from( 'filearchive' ) |
208 | ->where( [ 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ] ); |
209 | if ( $lock === 'lock' ) { |
210 | $queryBuilder->forUpdate(); |
211 | } |
212 | return (bool)$queryBuilder->caller( __METHOD__ )->fetchField(); |
213 | } |
214 | |
215 | /** |
216 | * Check if a hidden (revision delete) file has this sha1 key |
217 | * |
218 | * @param string $key File storage key (base-36 sha1 key with file extension) |
219 | * @param string|null $lock Use "lock" to lock the row via FOR UPDATE |
220 | * @return bool File with this key is in use |
221 | */ |
222 | protected function hiddenFileHasKey( $key, $lock = null ) { |
223 | $sha1 = self::getHashFromKey( $key ); |
224 | $ext = File::normalizeExtension( substr( $key, strcspn( $key, '.' ) + 1 ) ); |
225 | |
226 | $dbw = $this->getPrimaryDB(); |
227 | $queryBuilder = $dbw->newSelectQueryBuilder() |
228 | ->select( '1' ) |
229 | ->from( 'oldimage' ) |
230 | ->where( [ |
231 | 'oi_sha1' => $sha1, |
232 | $dbw->expr( 'oi_archive_name', IExpression::LIKE, new LikeValue( $dbw->anyString(), ".$ext" ) ), |
233 | $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE, |
234 | ] ); |
235 | if ( $lock === 'lock' ) { |
236 | $queryBuilder->forUpdate(); |
237 | } |
238 | |
239 | return (bool)$queryBuilder->caller( __METHOD__ )->fetchField(); |
240 | } |
241 | |
242 | /** |
243 | * Gets the SHA1 hash from a storage key |
244 | * |
245 | * @param string $key |
246 | * @return string |
247 | */ |
248 | public static function getHashFromKey( $key ) { |
249 | $sha1 = strtok( $key, '.' ); |
250 | if ( is_string( $sha1 ) && strlen( $sha1 ) === 32 && $sha1[0] === '0' ) { |
251 | $sha1 = substr( $sha1, 1 ); |
252 | } |
253 | return $sha1; |
254 | } |
255 | |
256 | /** |
257 | * Checks if there is a redirect named as $title |
258 | * |
259 | * @param PageIdentity|LinkTarget $title Title of file |
260 | * @return Title|false |
261 | */ |
262 | public function checkRedirect( $title ) { |
263 | $title = File::normalizeTitle( $title, 'exception' ); |
264 | |
265 | $memcKey = $this->getSharedCacheKey( 'file-redirect', md5( $title->getDBkey() ) ); |
266 | if ( $memcKey === false ) { |
267 | $memcKey = $this->getLocalCacheKey( 'file-redirect', md5( $title->getDBkey() ) ); |
268 | $expiry = 300; // no invalidation, 5 minutes |
269 | } else { |
270 | $expiry = 86400; // has invalidation, 1 day |
271 | } |
272 | |
273 | $method = __METHOD__; |
274 | $redirDbKey = $this->wanCache->getWithSetCallback( |
275 | $memcKey, |
276 | $expiry, |
277 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $method, $title ) { |
278 | $dbr = $this->getReplicaDB(); // possibly remote DB |
279 | |
280 | $setOpts += Database::getCacheSetOptions( $dbr ); |
281 | |
282 | $row = $dbr->newSelectQueryBuilder() |
283 | ->select( [ 'rd_namespace', 'rd_title' ] ) |
284 | ->from( 'page' ) |
285 | ->join( 'redirect', null, 'rd_from = page_id' ) |
286 | ->where( [ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ] ) |
287 | ->caller( $method )->fetchRow(); |
288 | |
289 | return ( $row && $row->rd_namespace == NS_FILE ) |
290 | ? Title::makeTitle( $row->rd_namespace, $row->rd_title )->getDBkey() |
291 | : ''; // negative cache |
292 | }, |
293 | [ 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] |
294 | ); |
295 | |
296 | // @note: also checks " " for b/c |
297 | if ( $redirDbKey !== ' ' && strval( $redirDbKey ) !== '' ) { |
298 | // Page is a redirect to another file |
299 | return Title::newFromText( $redirDbKey, NS_FILE ); |
300 | } |
301 | |
302 | return false; // no redirect |
303 | } |
304 | |
305 | public function findFiles( array $items, $flags = 0 ) { |
306 | $finalFiles = []; // map of (DB key => corresponding File) for matches |
307 | |
308 | $searchSet = []; // map of (normalized DB key => search params) |
309 | foreach ( $items as $item ) { |
310 | if ( is_array( $item ) ) { |
311 | $title = File::normalizeTitle( $item['title'] ); |
312 | if ( $title ) { |
313 | $searchSet[$title->getDBkey()] = $item; |
314 | } |
315 | } else { |
316 | $title = File::normalizeTitle( $item ); |
317 | if ( $title ) { |
318 | $searchSet[$title->getDBkey()] = []; |
319 | } |
320 | } |
321 | } |
322 | |
323 | $fileMatchesSearch = static function ( File $file, array $search ) { |
324 | // Note: file name comparison done elsewhere (to handle redirects) |
325 | |
326 | // Fallback to RequestContext::getMain should be replaced with a better |
327 | // way of setting the user that should be used; currently it needs to be |
328 | // set for each file individually. See T263033#6477586 |
329 | $contextPerformer = RequestContext::getMain()->getAuthority(); |
330 | $performer = ( !empty( $search['private'] ) && $search['private'] instanceof Authority ) |
331 | ? $search['private'] |
332 | : $contextPerformer; |
333 | |
334 | return ( |
335 | $file->exists() && |
336 | ( |
337 | ( empty( $search['time'] ) && !$file->isOld() ) || |
338 | ( !empty( $search['time'] ) && $search['time'] === $file->getTimestamp() ) |
339 | ) && |
340 | ( !empty( $search['private'] ) || !$file->isDeleted( File::DELETED_FILE ) ) && |
341 | $file->userCan( File::DELETED_FILE, $performer ) |
342 | ); |
343 | }; |
344 | |
345 | $applyMatchingFiles = function ( IResultWrapper $res, &$searchSet, &$finalFiles ) |
346 | use ( $fileMatchesSearch, $flags ) |
347 | { |
348 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
349 | $info = $this->getInfo(); |
350 | foreach ( $res as $row ) { |
351 | $file = $this->newFileFromRow( $row ); |
352 | // There must have been a search for this DB key, but this has to handle the |
353 | // cases were title capitalization is different on the client and repo wikis. |
354 | $dbKeysLook = [ strtr( $file->getName(), ' ', '_' ) ]; |
355 | if ( !empty( $info['initialCapital'] ) ) { |
356 | // Search keys for "hi.png" and "Hi.png" should use the "Hi.png file" |
357 | $dbKeysLook[] = $contLang->lcfirst( $file->getName() ); |
358 | } |
359 | foreach ( $dbKeysLook as $dbKey ) { |
360 | if ( isset( $searchSet[$dbKey] ) |
361 | && $fileMatchesSearch( $file, $searchSet[$dbKey] ) |
362 | ) { |
363 | $finalFiles[$dbKey] = ( $flags & FileRepo::NAME_AND_TIME_ONLY ) |
364 | ? [ 'title' => $dbKey, 'timestamp' => $file->getTimestamp() ] |
365 | : $file; |
366 | unset( $searchSet[$dbKey] ); |
367 | } |
368 | } |
369 | } |
370 | }; |
371 | |
372 | $dbr = $this->getReplicaDB(); |
373 | |
374 | // Query image table |
375 | $imgNames = []; |
376 | foreach ( $searchSet as $dbKey => $_ ) { |
377 | $imgNames[] = $this->getNameFromTitle( File::normalizeTitle( $dbKey ) ); |
378 | } |
379 | |
380 | if ( count( $imgNames ) ) { |
381 | $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr ); |
382 | $res = $queryBuilder->where( [ 'img_name' => $imgNames ] )->caller( __METHOD__ )->fetchResultSet(); |
383 | $applyMatchingFiles( $res, $searchSet, $finalFiles ); |
384 | } |
385 | |
386 | // Query old image table |
387 | $oiConds = []; // WHERE clause array for each file |
388 | foreach ( $searchSet as $dbKey => $search ) { |
389 | if ( isset( $search['time'] ) ) { |
390 | $oiConds[] = $dbr |
391 | ->expr( 'oi_name', '=', $this->getNameFromTitle( File::normalizeTitle( $dbKey ) ) ) |
392 | ->and( 'oi_timestamp', '=', $dbr->timestamp( $search['time'] ) ); |
393 | } |
394 | } |
395 | |
396 | if ( count( $oiConds ) ) { |
397 | $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr ); |
398 | |
399 | $res = $queryBuilder->where( $dbr->orExpr( $oiConds ) ) |
400 | ->caller( __METHOD__ )->fetchResultSet(); |
401 | $applyMatchingFiles( $res, $searchSet, $finalFiles ); |
402 | } |
403 | |
404 | // Check for redirects... |
405 | foreach ( $searchSet as $dbKey => $search ) { |
406 | if ( !empty( $search['ignoreRedirect'] ) ) { |
407 | continue; |
408 | } |
409 | |
410 | $title = File::normalizeTitle( $dbKey ); |
411 | $redir = $this->checkRedirect( $title ); // hopefully hits memcached |
412 | |
413 | if ( $redir && $redir->getNamespace() === NS_FILE ) { |
414 | $file = $this->newFile( $redir ); |
415 | if ( $file && $fileMatchesSearch( $file, $search ) ) { |
416 | $file->redirectedFrom( $title->getDBkey() ); |
417 | if ( $flags & FileRepo::NAME_AND_TIME_ONLY ) { |
418 | $finalFiles[$dbKey] = [ |
419 | 'title' => $file->getTitle()->getDBkey(), |
420 | 'timestamp' => $file->getTimestamp() |
421 | ]; |
422 | } else { |
423 | $finalFiles[$dbKey] = $file; |
424 | } |
425 | } |
426 | } |
427 | } |
428 | |
429 | return $finalFiles; |
430 | } |
431 | |
432 | /** |
433 | * Get an array or iterator of file objects for files that have a given |
434 | * SHA-1 content hash. |
435 | * |
436 | * @param string $hash A sha1 hash to look for |
437 | * @return LocalFile[] |
438 | */ |
439 | public function findBySha1( $hash ) { |
440 | $queryBuilder = FileSelectQueryBuilder::newForFile( $this->getReplicaDB() ); |
441 | $res = $queryBuilder->where( [ 'img_sha1' => $hash ] ) |
442 | ->orderBy( 'img_name' ) |
443 | ->caller( __METHOD__ )->fetchResultSet(); |
444 | |
445 | $result = []; |
446 | foreach ( $res as $row ) { |
447 | $result[] = $this->newFileFromRow( $row ); |
448 | } |
449 | $res->free(); |
450 | |
451 | return $result; |
452 | } |
453 | |
454 | /** |
455 | * Get an array of arrays or iterators of file objects for files that |
456 | * have the given SHA-1 content hashes. |
457 | * |
458 | * Overrides generic implementation in FileRepo for performance reason |
459 | * |
460 | * @param string[] $hashes An array of hashes |
461 | * @return File[][] An Array of arrays or iterators of file objects and the hash as key |
462 | */ |
463 | public function findBySha1s( array $hashes ) { |
464 | if ( $hashes === [] ) { |
465 | return []; // empty parameter |
466 | } |
467 | |
468 | $dbr = $this->getReplicaDB(); |
469 | $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr ); |
470 | |
471 | $queryBuilder->where( [ 'img_sha1' => $hashes ] ) |
472 | ->orderBy( 'img_name' ); |
473 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
474 | |
475 | $result = []; |
476 | foreach ( $res as $row ) { |
477 | $file = $this->newFileFromRow( $row ); |
478 | $result[$file->getSha1()][] = $file; |
479 | } |
480 | $res->free(); |
481 | |
482 | return $result; |
483 | } |
484 | |
485 | /** |
486 | * Return an array of files where the name starts with $prefix. |
487 | * |
488 | * @param string $prefix The prefix to search for |
489 | * @param int $limit The maximum amount of files to return |
490 | * @return LocalFile[] |
491 | */ |
492 | public function findFilesByPrefix( $prefix, $limit ) { |
493 | $dbr = $this->getReplicaDB(); |
494 | $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr ); |
495 | |
496 | $queryBuilder |
497 | ->where( $dbr->expr( 'img_name', IExpression::LIKE, new LikeValue( $prefix, $dbr->anyString() ) ) ) |
498 | ->orderBy( 'img_name' ) |
499 | ->limit( intval( $limit ) ); |
500 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
501 | |
502 | // Build file objects |
503 | $files = []; |
504 | foreach ( $res as $row ) { |
505 | $files[] = $this->newFileFromRow( $row ); |
506 | } |
507 | |
508 | return $files; |
509 | } |
510 | |
511 | /** |
512 | * Get a connection to the replica DB |
513 | * @return IReadableDatabase |
514 | */ |
515 | public function getReplicaDB() { |
516 | return $this->dbProvider->getReplicaDatabase(); |
517 | } |
518 | |
519 | /** |
520 | * Get a connection to the primary DB |
521 | * @return IDatabase |
522 | * @since 1.37 |
523 | */ |
524 | public function getPrimaryDB() { |
525 | return $this->dbProvider->getPrimaryDatabase(); |
526 | } |
527 | |
528 | /** |
529 | * Get a callback to get a DB handle given an index (DB_REPLICA/DB_PRIMARY) |
530 | * @return Closure |
531 | */ |
532 | protected function getDBFactory() { |
533 | // TODO: DB_REPLICA/DB_PRIMARY shouldn't be passed around |
534 | return static function ( $index ) { |
535 | if ( $index === DB_PRIMARY ) { |
536 | return MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
537 | } else { |
538 | return MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
539 | } |
540 | }; |
541 | } |
542 | |
543 | /** |
544 | * Check whether the repo has a shared cache, accessible from the current site context |
545 | * |
546 | * @return bool |
547 | * @since 1.35 |
548 | */ |
549 | protected function hasAcessibleSharedCache() { |
550 | return $this->hasAccessibleSharedCache; |
551 | } |
552 | |
553 | public function getSharedCacheKey( $kClassSuffix, ...$components ) { |
554 | // T267668: do not include the repo name in the key |
555 | return $this->hasAcessibleSharedCache() |
556 | ? $this->wanCache->makeGlobalKey( |
557 | 'filerepo-' . $kClassSuffix, |
558 | $this->dbDomain, |
559 | ...$components |
560 | ) |
561 | : false; |
562 | } |
563 | |
564 | /** |
565 | * Invalidates image redirect cache related to that image |
566 | * |
567 | * @param PageIdentity|LinkTarget $title Title of page |
568 | * @return void |
569 | */ |
570 | public function invalidateImageRedirect( $title ) { |
571 | $key = $this->getSharedCacheKey( 'file-redirect', md5( $title->getDBkey() ) ); |
572 | if ( $key ) { |
573 | $this->getPrimaryDB()->onTransactionPreCommitOrIdle( |
574 | function () use ( $key ) { |
575 | $this->wanCache->delete( $key ); |
576 | }, |
577 | __METHOD__ |
578 | ); |
579 | } |
580 | } |
581 | |
582 | public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
583 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
584 | } |
585 | |
586 | public function storeBatch( array $triplets, $flags = 0 ) { |
587 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
588 | } |
589 | |
590 | public function cleanupBatch( array $files, $flags = 0 ) { |
591 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
592 | } |
593 | |
594 | public function publish( |
595 | $src, |
596 | $dstRel, |
597 | $archiveRel, |
598 | $flags = 0, |
599 | array $options = [] |
600 | ) { |
601 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
602 | } |
603 | |
604 | public function publishBatch( array $ntuples, $flags = 0 ) { |
605 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
606 | } |
607 | |
608 | public function delete( $srcRel, $archiveRel ) { |
609 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
610 | } |
611 | |
612 | public function deleteBatch( array $sourceDestPairs ) { |
613 | return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() ); |
614 | } |
615 | |
616 | /** |
617 | * Skips the write operation if storage is sha1-based, executes it normally otherwise |
618 | * |
619 | * @param string $function |
620 | * @param array $args |
621 | * @return Status |
622 | */ |
623 | protected function skipWriteOperationIfSha1( $function, array $args ) { |
624 | $this->assertWritableRepo(); // fail out if read-only |
625 | |
626 | if ( $this->hasSha1Storage() ) { |
627 | wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths" ); |
628 | return Status::newGood(); |
629 | } else { |
630 | return parent::$function( ...$args ); |
631 | } |
632 | } |
633 | |
634 | /** |
635 | * Returns true if files should store metadata in JSON format. This |
636 | * requires metadata from all handlers to be JSON-serializable. |
637 | * |
638 | * To avoid breaking existing metadata, reading JSON metadata is always |
639 | * enabled regardless of this setting. |
640 | * |
641 | * @return bool |
642 | */ |
643 | public function isJsonMetadataEnabled() { |
644 | return $this->useJsonMetadata; |
645 | } |
646 | |
647 | /** |
648 | * Returns true if files should split up large metadata, storing parts of |
649 | * it in the BlobStore. |
650 | * |
651 | * @return bool |
652 | */ |
653 | public function isSplitMetadataEnabled() { |
654 | return $this->isJsonMetadataEnabled() && $this->useSplitMetadata; |
655 | } |
656 | |
657 | /** |
658 | * Get the threshold above which metadata items should be split into |
659 | * separate storage, or null if no splitting should be done. |
660 | * |
661 | * @return int |
662 | */ |
663 | public function getSplitMetadataThreshold() { |
664 | return $this->splitMetadataThreshold; |
665 | } |
666 | |
667 | public function isMetadataUpdateEnabled() { |
668 | return $this->updateCompatibleMetadata; |
669 | } |
670 | |
671 | public function isMetadataReserializeEnabled() { |
672 | return $this->reserializeMetadata; |
673 | } |
674 | |
675 | /** |
676 | * Get a BlobStore for storing and retrieving large metadata, or null if |
677 | * that can't be done. |
678 | */ |
679 | public function getBlobStore(): ?BlobStore { |
680 | if ( !$this->blobStore ) { |
681 | $this->blobStore = MediaWikiServices::getInstance()->getBlobStoreFactory() |
682 | ->newBlobStore( $this->dbDomain ); |
683 | } |
684 | return $this->blobStore; |
685 | } |
686 | } |
687 | |
688 | /** @deprecated class alias since 1.44 */ |
689 | class_alias( LocalRepo::class, 'LocalRepo' ); |