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