MediaWiki REL1_39
LocalRepo.php
Go to the documentation of this file.
1<?php
29
39class LocalRepo extends FileRepo {
41 protected $fileFactory = [ LocalFile::class, 'newFromTitle' ];
43 protected $fileFactoryKey = [ LocalFile::class, 'newFromKey' ];
45 protected $fileFromRowFactory = [ LocalFile::class, 'newFromRow' ];
47 protected $oldFileFromRowFactory = [ OldLocalFile::class, 'newFromRow' ];
49 protected $oldFileFactory = [ OldLocalFile::class, 'newFromTitle' ];
51 protected $oldFileFactoryKey = [ OldLocalFile::class, 'newFromKey' ];
52
54 protected $dbDomain;
57
59 protected $blobStore;
60
62 protected $useJsonMetadata = true;
63
65 protected $useSplitMetadata = false;
66
68 protected $splitMetadataThreshold = 1000;
69
71 protected $updateCompatibleMetadata = false;
72
74 protected $reserializeMetadata = false;
75
76 public function __construct( array $info = null ) {
77 parent::__construct( $info );
78
79 $this->dbDomain = WikiMap::getCurrentWikiDbDomain();
80 $this->hasAccessibleSharedCache = true;
81
82 $this->hasSha1Storage = ( $info['storageLayout'] ?? null ) === 'sha1';
83
84 if ( $this->hasSha1Storage() ) {
85 $this->backend = new FileBackendDBRepoWrapper( [
86 'backend' => $this->backend,
87 'repoName' => $this->name,
88 'dbHandleFactory' => $this->getDBFactory()
89 ] );
90 }
91
92 foreach (
93 [
94 'useJsonMetadata',
95 'useSplitMetadata',
96 'splitMetadataThreshold',
97 'updateCompatibleMetadata',
98 'reserializeMetadata',
99 ] as $option
100 ) {
101 if ( isset( $info[$option] ) ) {
102 $this->$option = $info[$option];
103 }
104 }
105 }
106
112 public function newFileFromRow( $row ) {
113 if ( isset( $row->img_name ) ) {
114 return call_user_func( $this->fileFromRowFactory, $row, $this );
115 } elseif ( isset( $row->oi_name ) ) {
116 return call_user_func( $this->oldFileFromRowFactory, $row, $this );
117 } else {
118 throw new MWException( __METHOD__ . ': invalid row' );
119 }
120 }
121
127 public function newFromArchiveName( $title, $archiveName ) {
128 $title = File::normalizeTitle( $title );
129 return OldLocalFile::newFromArchiveName( $title, $this, $archiveName );
130 }
131
142 public function cleanupDeletedBatch( array $storageKeys ) {
143 if ( $this->hasSha1Storage() ) {
144 wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths" );
145 return Status::newGood();
146 }
147
148 $backend = $this->backend; // convenience
149 $root = $this->getZonePath( 'deleted' );
150 $dbw = $this->getPrimaryDB();
151 $status = $this->newGood();
152 $storageKeys = array_unique( $storageKeys );
153 foreach ( $storageKeys as $key ) {
154 $hashPath = $this->getDeletedHashPath( $key );
155 $path = "$root/$hashPath$key";
156 $dbw->startAtomic( __METHOD__ );
157 // Check for usage in deleted/hidden files and preemptively
158 // lock the key to avoid any future use until we are finished.
159 $deleted = $this->deletedFileHasKey( $key, 'lock' );
160 $hidden = $this->hiddenFileHasKey( $key, 'lock' );
161 if ( !$deleted && !$hidden ) { // not in use now
162 wfDebug( __METHOD__ . ": deleting $key" );
163 $op = [ 'op' => 'delete', 'src' => $path ];
164 if ( !$backend->doOperation( $op )->isOK() ) {
165 $status->error( 'undelete-cleanup-error', $path );
166 $status->failCount++;
167 }
168 } else {
169 wfDebug( __METHOD__ . ": $key still in use" );
170 $status->successCount++;
171 }
172 $dbw->endAtomic( __METHOD__ );
173 }
174
175 return $status;
176 }
177
185 protected function deletedFileHasKey( $key, $lock = null ) {
186 $dbw = $this->getPrimaryDB();
187 return (bool)$dbw->selectField( 'filearchive', '1',
188 [ 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ],
189 __METHOD__,
190 $lock === 'lock' ? [ 'FOR UPDATE' ] : []
191 );
192 }
193
201 protected function hiddenFileHasKey( $key, $lock = null ) {
202 $sha1 = self::getHashFromKey( $key );
203 $ext = File::normalizeExtension( substr( $key, strcspn( $key, '.' ) + 1 ) );
204
205 $dbw = $this->getPrimaryDB();
206 return (bool)$dbw->selectField( 'oldimage', '1',
207 [
208 'oi_sha1' => $sha1,
209 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ),
210 $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE,
211 ],
212 __METHOD__,
213 $lock === 'lock' ? [ 'FOR UPDATE' ] : []
214 );
215 }
216
223 public static function getHashFromKey( $key ) {
224 $sha1 = strtok( $key, '.' );
225 if ( is_string( $sha1 ) && strlen( $sha1 ) === 32 && $sha1[0] === '0' ) {
226 $sha1 = substr( $sha1, 1 );
227 }
228 return $sha1;
229 }
230
237 public function checkRedirect( $title ) {
238 $title = File::normalizeTitle( $title, 'exception' );
239
240 $memcKey = $this->getSharedCacheKey( 'file-redirect', md5( $title->getDBkey() ) );
241 if ( $memcKey === false ) {
242 $memcKey = $this->getLocalCacheKey( 'file-redirect', md5( $title->getDBkey() ) );
243 $expiry = 300; // no invalidation, 5 minutes
244 } else {
245 $expiry = 86400; // has invalidation, 1 day
246 }
247
248 $method = __METHOD__;
249 $redirDbKey = $this->wanCache->getWithSetCallback(
250 $memcKey,
251 $expiry,
252 function ( $oldValue, &$ttl, array &$setOpts ) use ( $method, $title ) {
253 $dbr = $this->getReplicaDB(); // possibly remote DB
254
255 $setOpts += Database::getCacheSetOptions( $dbr );
256
257 $row = $dbr->selectRow(
258 [ 'page', 'redirect' ],
259 [ 'rd_namespace', 'rd_title' ],
260 [
261 'page_namespace' => $title->getNamespace(),
262 'page_title' => $title->getDBkey(),
263 'rd_from = page_id'
264 ],
265 $method
266 );
267
268 return ( $row && $row->rd_namespace == NS_FILE )
269 ? Title::makeTitle( $row->rd_namespace, $row->rd_title )->getDBkey()
270 : ''; // negative cache
271 },
272 [ 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
273 );
274
275 // @note: also checks " " for b/c
276 if ( $redirDbKey !== ' ' && strval( $redirDbKey ) !== '' ) {
277 // Page is a redirect to another file
278 return Title::newFromText( $redirDbKey, NS_FILE );
279 }
280
281 return false; // no redirect
282 }
283
284 public function findFiles( array $items, $flags = 0 ) {
285 $finalFiles = []; // map of (DB key => corresponding File) for matches
286
287 $searchSet = []; // map of (normalized DB key => search params)
288 foreach ( $items as $item ) {
289 if ( is_array( $item ) ) {
290 $title = File::normalizeTitle( $item['title'] );
291 if ( $title ) {
292 $searchSet[$title->getDBkey()] = $item;
293 }
294 } else {
295 $title = File::normalizeTitle( $item );
296 if ( $title ) {
297 $searchSet[$title->getDBkey()] = [];
298 }
299 }
300 }
301
302 $fileMatchesSearch = static function ( File $file, array $search ) {
303 // Note: file name comparison done elsewhere (to handle redirects)
304
305 // Fallback to RequestContext::getMain should be replaced with a better
306 // way of setting the user that should be used; currently it needs to be
307 // set for each file individually. See T263033#6477586
308 $contextPerformer = RequestContext::getMain()->getAuthority();
309 $performer = ( !empty( $search['private'] ) && $search['private'] instanceof Authority )
310 ? $search['private']
311 : $contextPerformer;
312
313 return (
314 $file->exists() &&
315 (
316 ( empty( $search['time'] ) && !$file->isOld() ) ||
317 ( !empty( $search['time'] ) && $search['time'] === $file->getTimestamp() )
318 ) &&
319 ( !empty( $search['private'] ) || !$file->isDeleted( File::DELETED_FILE ) ) &&
320 $file->userCan( File::DELETED_FILE, $performer )
321 );
322 };
323
324 $applyMatchingFiles = function ( IResultWrapper $res, &$searchSet, &$finalFiles )
325 use ( $fileMatchesSearch, $flags )
326 {
327 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
328 $info = $this->getInfo();
329 foreach ( $res as $row ) {
330 $file = $this->newFileFromRow( $row );
331 // There must have been a search for this DB key, but this has to handle the
332 // cases were title capitalization is different on the client and repo wikis.
333 $dbKeysLook = [ strtr( $file->getName(), ' ', '_' ) ];
334 if ( !empty( $info['initialCapital'] ) ) {
335 // Search keys for "hi.png" and "Hi.png" should use the "Hi.png file"
336 $dbKeysLook[] = $contLang->lcfirst( $file->getName() );
337 }
338 foreach ( $dbKeysLook as $dbKey ) {
339 if ( isset( $searchSet[$dbKey] )
340 && $fileMatchesSearch( $file, $searchSet[$dbKey] )
341 ) {
342 $finalFiles[$dbKey] = ( $flags & FileRepo::NAME_AND_TIME_ONLY )
343 ? [ 'title' => $dbKey, 'timestamp' => $file->getTimestamp() ]
344 : $file;
345 unset( $searchSet[$dbKey] );
346 }
347 }
348 }
349 };
350
351 $dbr = $this->getReplicaDB();
352
353 // Query image table
354 $imgNames = [];
355 foreach ( array_keys( $searchSet ) as $dbKey ) {
356 $imgNames[] = $this->getNameFromTitle( File::normalizeTitle( $dbKey ) );
357 }
358
359 if ( count( $imgNames ) ) {
360 $fileQuery = LocalFile::getQueryInfo();
361 $res = $dbr->select( $fileQuery['tables'], $fileQuery['fields'], [ 'img_name' => $imgNames ],
362 __METHOD__, [], $fileQuery['joins'] );
363 $applyMatchingFiles( $res, $searchSet, $finalFiles );
364 }
365
366 // Query old image table
367 $oiConds = []; // WHERE clause array for each file
368 foreach ( $searchSet as $dbKey => $search ) {
369 if ( isset( $search['time'] ) ) {
370 $oiConds[] = $dbr->makeList(
371 [
372 'oi_name' => $this->getNameFromTitle( File::normalizeTitle( $dbKey ) ),
373 'oi_timestamp' => $dbr->timestamp( $search['time'] )
374 ],
376 );
377 }
378 }
379
380 if ( count( $oiConds ) ) {
381 $fileQuery = OldLocalFile::getQueryInfo();
382 $res = $dbr->select( $fileQuery['tables'], $fileQuery['fields'],
383 $dbr->makeList( $oiConds, LIST_OR ),
384 __METHOD__, [], $fileQuery['joins'] );
385 $applyMatchingFiles( $res, $searchSet, $finalFiles );
386 }
387
388 // Check for redirects...
389 foreach ( $searchSet as $dbKey => $search ) {
390 if ( !empty( $search['ignoreRedirect'] ) ) {
391 continue;
392 }
393
394 $title = File::normalizeTitle( $dbKey );
395 $redir = $this->checkRedirect( $title ); // hopefully hits memcached
396
397 if ( $redir && $redir->getNamespace() === NS_FILE ) {
398 $file = $this->newFile( $redir );
399 if ( $file && $fileMatchesSearch( $file, $search ) ) {
400 $file->redirectedFrom( $title->getDBkey() );
401 if ( $flags & FileRepo::NAME_AND_TIME_ONLY ) {
402 $finalFiles[$dbKey] = [
403 'title' => $file->getTitle()->getDBkey(),
404 'timestamp' => $file->getTimestamp()
405 ];
406 } else {
407 $finalFiles[$dbKey] = $file;
408 }
409 }
410 }
411 }
412
413 return $finalFiles;
414 }
415
423 public function findBySha1( $hash ) {
424 $dbr = $this->getReplicaDB();
425 $fileQuery = LocalFile::getQueryInfo();
426 $res = $dbr->select(
427 $fileQuery['tables'],
428 $fileQuery['fields'],
429 [ 'img_sha1' => $hash ],
430 __METHOD__,
431 [ 'ORDER BY' => 'img_name' ],
432 $fileQuery['joins']
433 );
434
435 $result = [];
436 foreach ( $res as $row ) {
437 $result[] = $this->newFileFromRow( $row );
438 }
439 $res->free();
440
441 return $result;
442 }
443
453 public function findBySha1s( array $hashes ) {
454 if ( $hashes === [] ) {
455 return []; // empty parameter
456 }
457
458 $dbr = $this->getReplicaDB();
459 $fileQuery = LocalFile::getQueryInfo();
460 $res = $dbr->select(
461 $fileQuery['tables'],
462 $fileQuery['fields'],
463 [ 'img_sha1' => $hashes ],
464 __METHOD__,
465 [ 'ORDER BY' => 'img_name' ],
466 $fileQuery['joins']
467 );
468
469 $result = [];
470 foreach ( $res as $row ) {
471 $file = $this->newFileFromRow( $row );
472 $result[$file->getSha1()][] = $file;
473 }
474 $res->free();
475
476 return $result;
477 }
478
486 public function findFilesByPrefix( $prefix, $limit ) {
487 $selectOptions = [ 'ORDER BY' => 'img_name', 'LIMIT' => intval( $limit ) ];
488
489 // Query database
490 $dbr = $this->getReplicaDB();
491 $fileQuery = LocalFile::getQueryInfo();
492 $res = $dbr->select(
493 $fileQuery['tables'],
494 $fileQuery['fields'],
495 'img_name ' . $dbr->buildLike( $prefix, $dbr->anyString() ),
496 __METHOD__,
497 $selectOptions,
498 $fileQuery['joins']
499 );
500
501 // Build file objects
502 $files = [];
503 foreach ( $res as $row ) {
504 $files[] = $this->newFileFromRow( $row );
505 }
506
507 return $files;
508 }
509
514 public function getReplicaDB() {
515 return wfGetDB( DB_REPLICA );
516 }
517
523 public function getPrimaryDB() {
524 return wfGetDB( DB_PRIMARY );
525 }
526
532 public function getMasterDB() {
533 wfDeprecated( __METHOD__, '1.37' );
534 return $this->getPrimaryDB();
535 }
536
541 protected function getDBFactory() {
542 return static function ( $index ) {
543 return wfGetDB( $index );
544 };
545 }
546
553 protected function hasAcessibleSharedCache() {
554 return $this->hasAccessibleSharedCache;
555 }
556
557 public function getSharedCacheKey( $kClassSuffix, ...$components ) {
558 // T267668: do not include the repo name in the key
559 return $this->hasAcessibleSharedCache()
560 ? $this->wanCache->makeGlobalKey(
561 'filerepo-' . $kClassSuffix,
562 $this->dbDomain,
563 ...$components
564 )
565 : false;
566 }
567
574 public function invalidateImageRedirect( $title ) {
575 $key = $this->getSharedCacheKey( 'file-redirect', md5( $title->getDBkey() ) );
576 if ( $key ) {
577 $this->getPrimaryDB()->onTransactionPreCommitOrIdle(
578 function () use ( $key ) {
579 $this->wanCache->delete( $key );
580 },
581 __METHOD__
582 );
583 }
584 }
585
586 public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
587 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
588 }
589
590 public function storeBatch( array $triplets, $flags = 0 ) {
591 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
592 }
593
594 public function cleanupBatch( array $files, $flags = 0 ) {
595 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
596 }
597
598 public function publish(
599 $src,
600 $dstRel,
601 $archiveRel,
602 $flags = 0,
603 array $options = []
604 ) {
605 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
606 }
607
608 public function publishBatch( array $ntuples, $flags = 0 ) {
609 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
610 }
611
612 public function delete( $srcRel, $archiveRel ) {
613 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
614 }
615
616 public function deleteBatch( array $sourceDestPairs ) {
617 return $this->skipWriteOperationIfSha1( __FUNCTION__, func_get_args() );
618 }
619
627 protected function skipWriteOperationIfSha1( $function, array $args ) {
628 $this->assertWritableRepo(); // fail out if read-only
629
630 if ( $this->hasSha1Storage() ) {
631 wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths" );
632 return Status::newGood();
633 } else {
634 return parent::$function( ...$args );
635 }
636 }
637
647 public function isJsonMetadataEnabled() {
648 return $this->useJsonMetadata;
649 }
650
657 public function isSplitMetadataEnabled() {
658 return $this->isJsonMetadataEnabled() && $this->useSplitMetadata;
659 }
660
667 public function getSplitMetadataThreshold() {
668 return $this->splitMetadataThreshold;
669 }
670
671 public function isMetadataUpdateEnabled() {
672 return $this->updateCompatibleMetadata;
673 }
674
676 return $this->reserializeMetadata;
677 }
678
685 public function getBlobStore(): ?BlobStore {
686 if ( !$this->blobStore ) {
687 $this->blobStore = MediaWikiServices::getInstance()->getBlobStoreFactory()
688 ->newBlobStore( $this->dbDomain );
689 }
690 return $this->blobStore;
691 }
692}
const NS_FILE
Definition Defines.php:70
const LIST_OR
Definition Defines.php:46
const LIST_AND
Definition Defines.php:43
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Proxy backend that manages file layout rewriting for FileRepo.
doOperation(array $op, array $opts=[])
Same as doOperations() except it takes a single operation.
Base class for file repositories.
Definition FileRepo.php:47
assertWritableRepo()
Throw an exception if this repo is read-only by design.
newGood( $value=null)
Create a new good result.
const NAME_AND_TIME_ONLY
Definition FileRepo.php:53
getLocalCacheKey( $kClassSuffix,... $components)
Get a site-local, repository-qualified, WAN cache key.
hasSha1Storage()
Returns whether or not storage is SHA-1 based.
FileBackend $backend
Definition FileRepo.php:70
getZonePath( $zone)
Get the storage path corresponding to one of the zones.
Definition FileRepo.php:395
getDeletedHashPath( $key)
Get a relative path for a deletion archive key, e.g.
getNameFromTitle( $title)
Get the name of a file from its title.
Definition FileRepo.php:713
newFile( $title, $time=false)
Create a new File object from the local repository.
Definition FileRepo.php:419
getInfo()
Return information about the repository.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:67
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:39
skipWriteOperationIfSha1( $function, array $args)
Skips the write operation if storage is sha1-based, executes it normally otherwise.
int null $splitMetadataThreshold
Definition LocalRepo.php:68
getDBFactory()
Get a callback to get a DB handle given an index (DB_REPLICA/DB_PRIMARY)
getSharedCacheKey( $kClassSuffix,... $components)
Get a global, repository-qualified, WAN cache key.
isMetadataUpdateEnabled()
newFileFromRow( $row)
isSplitMetadataEnabled()
Returns true if files should split up large metadata, storing parts of it in the BlobStore.
deletedFileHasKey( $key, $lock=null)
Check if a deleted (filearchive) file has this sha1 key.
callable $oldFileFactoryKey
Definition LocalRepo.php:51
callable $oldFileFactory
Definition LocalRepo.php:49
isJsonMetadataEnabled()
Returns true if files should store metadata in JSON format.
cleanupBatch(array $files, $flags=0)
Deletes a batch of files.
publishBatch(array $ntuples, $flags=0)
Publish a batch of files.
findFiles(array $items, $flags=0)
Find many files at once.
findFilesByPrefix( $prefix, $limit)
Return an array of files where the name starts with $prefix.
findBySha1s(array $hashes)
Get an array of arrays or iterators of file objects for files that have the given SHA-1 content hashe...
getBlobStore()
Get a BlobStore for storing and retrieving large metadata, or null if that can't be done.
callable $oldFileFromRowFactory
Definition LocalRepo.php:47
string $dbDomain
DB domain of the repo wiki.
Definition LocalRepo.php:54
BlobStore $blobStore
Definition LocalRepo.php:59
bool $useJsonMetadata
Definition LocalRepo.php:62
invalidateImageRedirect( $title)
Invalidates image redirect cache related to that image.
cleanupDeletedBatch(array $storageKeys)
Delete files in the deleted directory if they are not referenced in the filearchive table.
bool $updateCompatibleMetadata
Definition LocalRepo.php:71
getPrimaryDB()
Get a connection to the primary DB.
checkRedirect( $title)
Checks if there is a redirect named as $title.
hasAcessibleSharedCache()
Check whether the repo has a shared cache, accessible from the current site context.
bool $hasAccessibleSharedCache
Whether shared cache keys are exposed/accessible.
Definition LocalRepo.php:56
callable $fileFactoryKey
Definition LocalRepo.php:43
getReplicaDB()
Get a connection to the replica DB.
store( $srcPath, $dstZone, $dstRel, $flags=0)
Store a file to a given destination.
publish( $src, $dstRel, $archiveRel, $flags=0, array $options=[])
Copy or move a file either from a storage path, virtual URL, or file system path, into this repositor...
getMasterDB()
Get a connection to the primary DB.
storeBatch(array $triplets, $flags=0)
Store a batch of files.
getSplitMetadataThreshold()
Get the threshold above which metadata items should be split into separate storage,...
callable $fileFromRowFactory
Definition LocalRepo.php:45
__construct(array $info=null)
Definition LocalRepo.php:76
deleteBatch(array $sourceDestPairs)
Move a group of files to the deletion archive.
hiddenFileHasKey( $key, $lock=null)
Check if a hidden (revision delete) file has this sha1 key.
static getHashFromKey( $key)
Gets the SHA1 hash from a storage key.
newFromArchiveName( $title, $archiveName)
bool $reserializeMetadata
Definition LocalRepo.php:74
isMetadataReserializeEnabled()
callable $fileFactory
Definition LocalRepo.php:41
bool $useSplitMetadata
Definition LocalRepo.php:65
findBySha1( $hash)
Get an array or iterator of file objects for files that have a given SHA-1 content hash.
MediaWiki exception.
Service locator for MediaWiki core services.
Interface for objects (potentially) representing an editable wiki page.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
Service for loading and storing data blobs.
Definition BlobStore.php:35
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
Result wrapper for grabbing data queried from an IDatabase object.
if( $line===false) $args
Definition mcc.php:124
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48