MediaWiki  master
FileBackendStore.php
Go to the documentation of this file.
1 <?php
24 
38 abstract class FileBackendStore extends FileBackend {
40  protected $memCache;
42  protected $srvCache;
44  protected $cheapCache;
46  protected $expensiveCache;
47 
49  protected $shardViaHashLevels = [];
50 
52  protected $mimeCallback;
53 
54  protected $maxFileSize = 4294967296; // integer bytes (4GiB)
55 
56  const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
57  const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
58  const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
59 
71  public function __construct( array $config ) {
72  parent::__construct( $config );
73  $this->mimeCallback = $config['mimeCallback'] ?? null;
74  $this->srvCache = new EmptyBagOStuff(); // disabled by default
75  $this->memCache = WANObjectCache::newEmpty(); // disabled by default
76  $this->cheapCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE );
77  $this->expensiveCache = new MapCacheLRU( self::CACHE_EXPENSIVE_SIZE );
78  }
79 
87  final public function maxFileSizeInternal() {
88  return $this->maxFileSize;
89  }
90 
100  abstract public function isPathUsableInternal( $storagePath );
101 
120  final public function createInternal( array $params ) {
121  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
122  if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
123  $status = $this->newStatus( 'backend-fail-maxsize',
124  $params['dst'], $this->maxFileSizeInternal() );
125  } else {
126  $status = $this->doCreateInternal( $params );
127  $this->clearCache( [ $params['dst'] ] );
128  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
129  $this->deleteFileCache( $params['dst'] ); // persistent cache
130  }
131  }
132 
133  return $status;
134  }
135 
141  abstract protected function doCreateInternal( array $params );
142 
161  final public function storeInternal( array $params ) {
162  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
163  if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
164  $status = $this->newStatus( 'backend-fail-maxsize',
165  $params['dst'], $this->maxFileSizeInternal() );
166  } else {
167  $status = $this->doStoreInternal( $params );
168  $this->clearCache( [ $params['dst'] ] );
169  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
170  $this->deleteFileCache( $params['dst'] ); // persistent cache
171  }
172  }
173 
174  return $status;
175  }
176 
182  abstract protected function doStoreInternal( array $params );
183 
203  final public function copyInternal( array $params ) {
204  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
205  $status = $this->doCopyInternal( $params );
206  $this->clearCache( [ $params['dst'] ] );
207  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
208  $this->deleteFileCache( $params['dst'] ); // persistent cache
209  }
210 
211  return $status;
212  }
213 
219  abstract protected function doCopyInternal( array $params );
220 
235  final public function deleteInternal( array $params ) {
236  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
237  $status = $this->doDeleteInternal( $params );
238  $this->clearCache( [ $params['src'] ] );
239  $this->deleteFileCache( $params['src'] ); // persistent cache
240  return $status;
241  }
242 
248  abstract protected function doDeleteInternal( array $params );
249 
269  final public function moveInternal( array $params ) {
270  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
271  $status = $this->doMoveInternal( $params );
272  $this->clearCache( [ $params['src'], $params['dst'] ] );
273  $this->deleteFileCache( $params['src'] ); // persistent cache
274  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
275  $this->deleteFileCache( $params['dst'] ); // persistent cache
276  }
277 
278  return $status;
279  }
280 
286  protected function doMoveInternal( array $params ) {
287  unset( $params['async'] ); // two steps, won't work here :)
288  $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
289  $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
290  // Copy source to dest
291  $status = $this->copyInternal( $params );
292  if ( $nsrc !== $ndst && $status->isOK() ) {
293  // Delete source (only fails due to races or network problems)
294  $status->merge( $this->deleteInternal( [ 'src' => $params['src'] ] ) );
295  $status->setResult( true, $status->value ); // ignore delete() errors
296  }
297 
298  return $status;
299  }
300 
315  final public function describeInternal( array $params ) {
316  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
317  if ( count( $params['headers'] ) ) {
318  $status = $this->doDescribeInternal( $params );
319  $this->clearCache( [ $params['src'] ] );
320  $this->deleteFileCache( $params['src'] ); // persistent cache
321  } else {
322  $status = $this->newStatus(); // nothing to do
323  }
324 
325  return $status;
326  }
327 
333  protected function doDescribeInternal( array $params ) {
334  return $this->newStatus();
335  }
336 
344  final public function nullInternal( array $params ) {
345  return $this->newStatus();
346  }
347 
348  final public function concatenate( array $params ) {
349  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
350  $status = $this->newStatus();
351 
352  // Try to lock the source files for the scope of this function
353  $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
354  if ( $status->isOK() ) {
355  // Actually do the file concatenation...
356  $start_time = microtime( true );
357  $status->merge( $this->doConcatenate( $params ) );
358  $sec = microtime( true ) - $start_time;
359  if ( !$status->isOK() ) {
360  $this->logger->error( static::class . "-{$this->name}" .
361  " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
362  }
363  }
364 
365  return $status;
366  }
367 
373  protected function doConcatenate( array $params ) {
374  $status = $this->newStatus();
375  $tmpPath = $params['dst']; // convenience
376  unset( $params['latest'] ); // sanity
377 
378  // Check that the specified temp file is valid...
379  Wikimedia\suppressWarnings();
380  $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
381  Wikimedia\restoreWarnings();
382  if ( !$ok ) { // not present or not empty
383  $status->fatal( 'backend-fail-opentemp', $tmpPath );
384 
385  return $status;
386  }
387 
388  // Get local FS versions of the chunks needed for the concatenation...
389  $fsFiles = $this->getLocalReferenceMulti( $params );
390  foreach ( $fsFiles as $path => &$fsFile ) {
391  if ( !$fsFile ) { // chunk failed to download?
392  $fsFile = $this->getLocalReference( [ 'src' => $path ] );
393  if ( !$fsFile ) { // retry failed?
394  $status->fatal( 'backend-fail-read', $path );
395 
396  return $status;
397  }
398  }
399  }
400  unset( $fsFile ); // unset reference so we can reuse $fsFile
401 
402  // Get a handle for the destination temp file
403  $tmpHandle = fopen( $tmpPath, 'ab' );
404  if ( $tmpHandle === false ) {
405  $status->fatal( 'backend-fail-opentemp', $tmpPath );
406 
407  return $status;
408  }
409 
410  // Build up the temp file using the source chunks (in order)...
411  foreach ( $fsFiles as $virtualSource => $fsFile ) {
412  // Get a handle to the local FS version
413  $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
414  if ( $sourceHandle === false ) {
415  fclose( $tmpHandle );
416  $status->fatal( 'backend-fail-read', $virtualSource );
417 
418  return $status;
419  }
420  // Append chunk to file (pass chunk size to avoid magic quotes)
421  if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
422  fclose( $sourceHandle );
423  fclose( $tmpHandle );
424  $status->fatal( 'backend-fail-writetemp', $tmpPath );
425 
426  return $status;
427  }
428  fclose( $sourceHandle );
429  }
430  if ( !fclose( $tmpHandle ) ) {
431  $status->fatal( 'backend-fail-closetemp', $tmpPath );
432 
433  return $status;
434  }
435 
436  clearstatcache(); // temp file changed
437 
438  return $status;
439  }
440 
441  final protected function doPrepare( array $params ) {
442  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
443  $status = $this->newStatus();
444 
445  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
446  if ( $dir === null ) {
447  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
448 
449  return $status; // invalid storage path
450  }
451 
452  if ( $shard !== null ) { // confined to a single container/shard
453  $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
454  } else { // directory is on several shards
455  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
456  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
457  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
458  $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
459  }
460  }
461 
462  return $status;
463  }
464 
472  protected function doPrepareInternal( $container, $dir, array $params ) {
473  return $this->newStatus();
474  }
475 
476  final protected function doSecure( array $params ) {
477  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
478  $status = $this->newStatus();
479 
480  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
481  if ( $dir === null ) {
482  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
483 
484  return $status; // invalid storage path
485  }
486 
487  if ( $shard !== null ) { // confined to a single container/shard
488  $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
489  } else { // directory is on several shards
490  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
491  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
492  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
493  $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
494  }
495  }
496 
497  return $status;
498  }
499 
507  protected function doSecureInternal( $container, $dir, array $params ) {
508  return $this->newStatus();
509  }
510 
511  final protected function doPublish( array $params ) {
512  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
513  $status = $this->newStatus();
514 
515  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
516  if ( $dir === null ) {
517  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
518 
519  return $status; // invalid storage path
520  }
521 
522  if ( $shard !== null ) { // confined to a single container/shard
523  $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
524  } else { // directory is on several shards
525  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
526  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
527  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
528  $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
529  }
530  }
531 
532  return $status;
533  }
534 
542  protected function doPublishInternal( $container, $dir, array $params ) {
543  return $this->newStatus();
544  }
545 
546  final protected function doClean( array $params ) {
547  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
548  $status = $this->newStatus();
549 
550  // Recursive: first delete all empty subdirs recursively
551  if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
552  $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
553  if ( $subDirsRel !== null ) { // no errors
554  foreach ( $subDirsRel as $subDirRel ) {
555  $subDir = $params['dir'] . "/{$subDirRel}"; // full path
556  $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
557  }
558  unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
559  }
560  }
561 
562  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
563  if ( $dir === null ) {
564  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
565 
566  return $status; // invalid storage path
567  }
568 
569  // Attempt to lock this directory...
570  $filesLockEx = [ $params['dir'] ];
571  $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
572  if ( !$status->isOK() ) {
573  return $status; // abort
574  }
575 
576  if ( $shard !== null ) { // confined to a single container/shard
577  $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
578  $this->deleteContainerCache( $fullCont ); // purge cache
579  } else { // directory is on several shards
580  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
581  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
582  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
583  $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
584  $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
585  }
586  }
587 
588  return $status;
589  }
590 
598  protected function doCleanInternal( $container, $dir, array $params ) {
599  return $this->newStatus();
600  }
601 
602  final public function fileExists( array $params ) {
603  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
604  $stat = $this->getFileStat( $params );
605 
606  return ( $stat === null ) ? null : (bool)$stat; // null => failure
607  }
608 
609  final public function getFileTimestamp( array $params ) {
610  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
611  $stat = $this->getFileStat( $params );
612 
613  return $stat ? $stat['mtime'] : false;
614  }
615 
616  final public function getFileSize( array $params ) {
617  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
618  $stat = $this->getFileStat( $params );
619 
620  return $stat ? $stat['size'] : false;
621  }
622 
623  final public function getFileStat( array $params ) {
624  $path = self::normalizeStoragePath( $params['src'] );
625  if ( $path === null ) {
626  return false; // invalid storage path
627  }
628  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
629  $latest = !empty( $params['latest'] ); // use latest data?
630  if ( !$latest && !$this->cheapCache->hasField( $path, 'stat', self::CACHE_TTL ) ) {
631  $this->primeFileCache( [ $path ] ); // check persistent cache
632  }
633  if ( $this->cheapCache->hasField( $path, 'stat', self::CACHE_TTL ) ) {
634  $stat = $this->cheapCache->getField( $path, 'stat' );
635  // If we want the latest data, check that this cached
636  // value was in fact fetched with the latest available data.
637  if ( is_array( $stat ) ) {
638  if ( !$latest || $stat['latest'] ) {
639  return $stat;
640  }
641  } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ] ) ) {
642  if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
643  return false;
644  }
645  }
646  }
647  $stat = $this->doGetFileStat( $params );
648  if ( is_array( $stat ) ) { // file exists
649  // Strongly consistent backends can automatically set "latest"
650  $stat['latest'] = $stat['latest'] ?? $latest;
651  $this->cheapCache->setField( $path, 'stat', $stat );
652  $this->setFileCache( $path, $stat ); // update persistent cache
653  if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
654  $this->cheapCache->setField( $path, 'sha1',
655  [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
656  }
657  if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
658  $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
659  $this->cheapCache->setField( $path, 'xattr',
660  [ 'map' => $stat['xattr'], 'latest' => $latest ] );
661  }
662  } elseif ( $stat === false ) { // file does not exist
663  $this->cheapCache->setField( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
664  $this->cheapCache->setField( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
665  $this->cheapCache->setField( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
666  $this->logger->debug( __METHOD__ . ": File $path does not exist.\n" );
667  } else { // an error occurred
668  $this->logger->warning( __METHOD__ . ": Could not stat file $path.\n" );
669  }
670 
671  return $stat;
672  }
673 
678  abstract protected function doGetFileStat( array $params );
679 
680  public function getFileContentsMulti( array $params ) {
681  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
682 
683  $params = $this->setConcurrencyFlags( $params );
684  $contents = $this->doGetFileContentsMulti( $params );
685 
686  return $contents;
687  }
688 
694  protected function doGetFileContentsMulti( array $params ) {
695  $contents = [];
696  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
697  Wikimedia\suppressWarnings();
698  $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
699  Wikimedia\restoreWarnings();
700  }
701 
702  return $contents;
703  }
704 
705  final public function getFileXAttributes( array $params ) {
706  $path = self::normalizeStoragePath( $params['src'] );
707  if ( $path === null ) {
708  return false; // invalid storage path
709  }
710  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
711  $latest = !empty( $params['latest'] ); // use latest data?
712  if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
713  $stat = $this->cheapCache->getField( $path, 'xattr' );
714  // If we want the latest data, check that this cached
715  // value was in fact fetched with the latest available data.
716  if ( !$latest || $stat['latest'] ) {
717  return $stat['map'];
718  }
719  }
720  $fields = $this->doGetFileXAttributes( $params );
721  $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
722  $this->cheapCache->setField( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
723 
724  return $fields;
725  }
726 
732  protected function doGetFileXAttributes( array $params ) {
733  return [ 'headers' => [], 'metadata' => [] ]; // not supported
734  }
735 
736  final public function getFileSha1Base36( array $params ) {
737  $path = self::normalizeStoragePath( $params['src'] );
738  if ( $path === null ) {
739  return false; // invalid storage path
740  }
741  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
742  $latest = !empty( $params['latest'] ); // use latest data?
743  if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
744  $stat = $this->cheapCache->getField( $path, 'sha1' );
745  // If we want the latest data, check that this cached
746  // value was in fact fetched with the latest available data.
747  if ( !$latest || $stat['latest'] ) {
748  return $stat['hash'];
749  }
750  }
751  $hash = $this->doGetFileSha1Base36( $params );
752  $this->cheapCache->setField( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
753 
754  return $hash;
755  }
756 
762  protected function doGetFileSha1Base36( array $params ) {
763  $fsFile = $this->getLocalReference( $params );
764  if ( !$fsFile ) {
765  return false;
766  } else {
767  return $fsFile->getSha1Base36();
768  }
769  }
770 
771  final public function getFileProps( array $params ) {
772  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
773  $fsFile = $this->getLocalReference( $params );
774  $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
775 
776  return $props;
777  }
778 
779  final public function getLocalReferenceMulti( array $params ) {
780  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
781 
782  $params = $this->setConcurrencyFlags( $params );
783 
784  $fsFiles = []; // (path => FSFile)
785  $latest = !empty( $params['latest'] ); // use latest data?
786  // Reuse any files already in process cache...
787  foreach ( $params['srcs'] as $src ) {
788  $path = self::normalizeStoragePath( $src );
789  if ( $path === null ) {
790  $fsFiles[$src] = null; // invalid storage path
791  } elseif ( $this->expensiveCache->hasField( $path, 'localRef' ) ) {
792  $val = $this->expensiveCache->getField( $path, 'localRef' );
793  // If we want the latest data, check that this cached
794  // value was in fact fetched with the latest available data.
795  if ( !$latest || $val['latest'] ) {
796  $fsFiles[$src] = $val['object'];
797  }
798  }
799  }
800  // Fetch local references of any remaning files...
801  $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
802  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
803  $fsFiles[$path] = $fsFile;
804  if ( $fsFile ) { // update the process cache...
805  $this->expensiveCache->setField( $path, 'localRef',
806  [ 'object' => $fsFile, 'latest' => $latest ] );
807  }
808  }
809 
810  return $fsFiles;
811  }
812 
818  protected function doGetLocalReferenceMulti( array $params ) {
819  return $this->doGetLocalCopyMulti( $params );
820  }
821 
822  final public function getLocalCopyMulti( array $params ) {
823  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
824 
825  $params = $this->setConcurrencyFlags( $params );
826  $tmpFiles = $this->doGetLocalCopyMulti( $params );
827 
828  return $tmpFiles;
829  }
830 
836  abstract protected function doGetLocalCopyMulti( array $params );
837 
843  public function getFileHttpUrl( array $params ) {
844  return null; // not supported
845  }
846 
847  final public function streamFile( array $params ) {
848  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
849  $status = $this->newStatus();
850 
851  // Always set some fields for subclass convenience
852  $params['options'] = $params['options'] ?? [];
853  $params['headers'] = $params['headers'] ?? [];
854 
855  // Don't stream it out as text/html if there was a PHP error
856  if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
857  print "Headers already sent, terminating.\n";
858  $status->fatal( 'backend-fail-stream', $params['src'] );
859  return $status;
860  }
861 
862  $status->merge( $this->doStreamFile( $params ) );
863 
864  return $status;
865  }
866 
872  protected function doStreamFile( array $params ) {
873  $status = $this->newStatus();
874 
875  $flags = 0;
876  $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0;
877  $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0;
878 
879  $fsFile = $this->getLocalReference( $params );
880  if ( $fsFile ) {
881  $streamer = new HTTPFileStreamer(
882  $fsFile->getPath(),
883  [
884  'obResetFunc' => $this->obResetFunc,
885  'streamMimeFunc' => $this->streamMimeFunc
886  ]
887  );
888  $res = $streamer->stream( $params['headers'], true, $params['options'], $flags );
889  } else {
890  $res = false;
891  HTTPFileStreamer::send404Message( $params['src'], $flags );
892  }
893 
894  if ( !$res ) {
895  $status->fatal( 'backend-fail-stream', $params['src'] );
896  }
897 
898  return $status;
899  }
900 
901  final public function directoryExists( array $params ) {
902  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
903  if ( $dir === null ) {
904  return false; // invalid storage path
905  }
906  if ( $shard !== null ) { // confined to a single container/shard
907  return $this->doDirectoryExists( $fullCont, $dir, $params );
908  } else { // directory is on several shards
909  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
910  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
911  $res = false; // response
912  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
913  $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
914  if ( $exists ) {
915  $res = true;
916  break; // found one!
917  } elseif ( $exists === null ) { // error?
918  $res = null; // if we don't find anything, it is indeterminate
919  }
920  }
921 
922  return $res;
923  }
924  }
925 
934  abstract protected function doDirectoryExists( $container, $dir, array $params );
935 
936  final public function getDirectoryList( array $params ) {
937  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
938  if ( $dir === null ) { // invalid storage path
939  return null;
940  }
941  if ( $shard !== null ) {
942  // File listing is confined to a single container/shard
943  return $this->getDirectoryListInternal( $fullCont, $dir, $params );
944  } else {
945  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
946  // File listing spans multiple containers/shards
947  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
948 
949  return new FileBackendStoreShardDirIterator( $this,
950  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
951  }
952  }
953 
964  abstract public function getDirectoryListInternal( $container, $dir, array $params );
965 
966  final public function getFileList( array $params ) {
967  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
968  if ( $dir === null ) { // invalid storage path
969  return null;
970  }
971  if ( $shard !== null ) {
972  // File listing is confined to a single container/shard
973  return $this->getFileListInternal( $fullCont, $dir, $params );
974  } else {
975  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
976  // File listing spans multiple containers/shards
977  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
978 
979  return new FileBackendStoreShardFileIterator( $this,
980  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
981  }
982  }
983 
994  abstract public function getFileListInternal( $container, $dir, array $params );
995 
1007  final public function getOperationsInternal( array $ops ) {
1008  $supportedOps = [
1009  'store' => StoreFileOp::class,
1010  'copy' => CopyFileOp::class,
1011  'move' => MoveFileOp::class,
1012  'delete' => DeleteFileOp::class,
1013  'create' => CreateFileOp::class,
1014  'describe' => DescribeFileOp::class,
1015  'null' => NullFileOp::class
1016  ];
1017 
1018  $performOps = []; // array of FileOp objects
1019  // Build up ordered array of FileOps...
1020  foreach ( $ops as $operation ) {
1021  $opName = $operation['op'];
1022  if ( isset( $supportedOps[$opName] ) ) {
1023  $class = $supportedOps[$opName];
1024  // Get params for this operation
1025  $params = $operation;
1026  // Append the FileOp class
1027  $performOps[] = new $class( $this, $params, $this->logger );
1028  } else {
1029  throw new FileBackendError( "Operation '$opName' is not supported." );
1030  }
1031  }
1032 
1033  return $performOps;
1034  }
1035 
1046  final public function getPathsToLockForOpsInternal( array $performOps ) {
1047  // Build up a list of files to lock...
1048  $paths = [ 'sh' => [], 'ex' => [] ];
1049  foreach ( $performOps as $fileOp ) {
1050  $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1051  $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1052  }
1053  // Optimization: if doing an EX lock anyway, don't also set an SH one
1054  $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1055  // Get a shared lock on the parent directory of each path changed
1056  $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1057 
1058  return [
1059  LockManager::LOCK_UW => $paths['sh'],
1060  LockManager::LOCK_EX => $paths['ex']
1061  ];
1062  }
1063 
1064  public function getScopedLocksForOps( array $ops, StatusValue $status ) {
1065  $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1066 
1067  return $this->getScopedFileLocks( $paths, 'mixed', $status );
1068  }
1069 
1070  final protected function doOperationsInternal( array $ops, array $opts ) {
1071  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1072  $status = $this->newStatus();
1073 
1074  // Fix up custom header name/value pairs...
1075  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1076 
1077  // Build up a list of FileOps...
1078  $performOps = $this->getOperationsInternal( $ops );
1079 
1080  // Acquire any locks as needed...
1081  if ( empty( $opts['nonLocking'] ) ) {
1082  // Build up a list of files to lock...
1083  $paths = $this->getPathsToLockForOpsInternal( $performOps );
1084  // Try to lock those files for the scope of this function...
1085 
1086  $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
1087  if ( !$status->isOK() ) {
1088  return $status; // abort
1089  }
1090  }
1091 
1092  // Clear any file cache entries (after locks acquired)
1093  if ( empty( $opts['preserveCache'] ) ) {
1094  $this->clearCache();
1095  }
1096 
1097  // Build the list of paths involved
1098  $paths = [];
1099  foreach ( $performOps as $performOp ) {
1100  $paths = array_merge( $paths, $performOp->storagePathsRead() );
1101  $paths = array_merge( $paths, $performOp->storagePathsChanged() );
1102  }
1103 
1104  // Enlarge the cache to fit the stat entries of these files
1105  $this->cheapCache->setMaxSize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
1106 
1107  // Load from the persistent container caches
1108  $this->primeContainerCache( $paths );
1109  // Get the latest stat info for all the files (having locked them)
1110  $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
1111 
1112  if ( $ok ) {
1113  // Actually attempt the operation batch...
1114  $opts = $this->setConcurrencyFlags( $opts );
1115  $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
1116  } else {
1117  // If we could not even stat some files, then bail out...
1118  $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
1119  foreach ( $ops as $i => $op ) { // mark each op as failed
1120  $subStatus->success[$i] = false;
1121  ++$subStatus->failCount;
1122  }
1123  $this->logger->error( static::class . "-{$this->name} " .
1124  " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
1125  }
1126 
1127  // Merge errors into StatusValue fields
1128  $status->merge( $subStatus );
1129  $status->success = $subStatus->success; // not done in merge()
1130 
1131  // Shrink the stat cache back to normal size
1132  $this->cheapCache->setMaxSize( self::CACHE_CHEAP_SIZE );
1133 
1134  return $status;
1135  }
1136 
1137  final protected function doQuickOperationsInternal( array $ops ) {
1138  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1139  $status = $this->newStatus();
1140 
1141  // Fix up custom header name/value pairs...
1142  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1143 
1144  // Clear any file cache entries
1145  $this->clearCache();
1146 
1147  $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
1148  // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
1149  $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
1150  $maxConcurrency = $this->concurrency; // throttle
1152  $statuses = []; // array of (index => StatusValue)
1153  $fileOpHandles = []; // list of (index => handle) arrays
1154  $curFileOpHandles = []; // current handle batch
1155  // Perform the sync-only ops and build up op handles for the async ops...
1156  foreach ( $ops as $index => $params ) {
1157  if ( !in_array( $params['op'], $supportedOps ) ) {
1158  throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
1159  }
1160  $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
1161  $subStatus = $this->$method( [ 'async' => $async ] + $params );
1162  if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1163  if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
1164  $fileOpHandles[] = $curFileOpHandles; // push this batch
1165  $curFileOpHandles = [];
1166  }
1167  $curFileOpHandles[$index] = $subStatus->value; // keep index
1168  } else { // error or completed
1169  $statuses[$index] = $subStatus; // keep index
1170  }
1171  }
1172  if ( count( $curFileOpHandles ) ) {
1173  $fileOpHandles[] = $curFileOpHandles; // last batch
1174  }
1175  // Do all the async ops that can be done concurrently...
1176  foreach ( $fileOpHandles as $fileHandleBatch ) {
1177  $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
1178  }
1179  // Marshall and merge all the responses...
1180  foreach ( $statuses as $index => $subStatus ) {
1181  $status->merge( $subStatus );
1182  if ( $subStatus->isOK() ) {
1183  $status->success[$index] = true;
1184  ++$status->successCount;
1185  } else {
1186  $status->success[$index] = false;
1187  ++$status->failCount;
1188  }
1189  }
1190 
1191  return $status;
1192  }
1193 
1203  final public function executeOpHandlesInternal( array $fileOpHandles ) {
1204  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1205 
1206  foreach ( $fileOpHandles as $fileOpHandle ) {
1207  if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1208  throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." );
1209  } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1210  throw new InvalidArgumentException( "Expected handle for this file backend." );
1211  }
1212  }
1213 
1214  $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1215  foreach ( $fileOpHandles as $fileOpHandle ) {
1216  $fileOpHandle->closeResources();
1217  }
1218 
1219  return $res;
1220  }
1221 
1230  protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1231  if ( count( $fileOpHandles ) ) {
1232  throw new LogicException( "Backend does not support asynchronous operations." );
1233  }
1234 
1235  return [];
1236  }
1237 
1249  protected function sanitizeOpHeaders( array $op ) {
1250  static $longs = [ 'content-disposition' ];
1251 
1252  if ( isset( $op['headers'] ) ) { // op sets HTTP headers
1253  $newHeaders = [];
1254  foreach ( $op['headers'] as $name => $value ) {
1255  $name = strtolower( $name );
1256  $maxHVLen = in_array( $name, $longs ) ? INF : 255;
1257  if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
1258  trigger_error( "Header '$name: $value' is too long." );
1259  } else {
1260  $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
1261  }
1262  }
1263  $op['headers'] = $newHeaders;
1264  }
1265 
1266  return $op;
1267  }
1268 
1269  final public function preloadCache( array $paths ) {
1270  $fullConts = []; // full container names
1271  foreach ( $paths as $path ) {
1272  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1273  $fullConts[] = $fullCont;
1274  }
1275  // Load from the persistent file and container caches
1276  $this->primeContainerCache( $fullConts );
1277  $this->primeFileCache( $paths );
1278  }
1279 
1280  final public function clearCache( array $paths = null ) {
1281  if ( is_array( $paths ) ) {
1282  $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1283  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1284  }
1285  if ( $paths === null ) {
1286  $this->cheapCache->clear();
1287  $this->expensiveCache->clear();
1288  } else {
1289  foreach ( $paths as $path ) {
1290  $this->cheapCache->clear( $path );
1291  $this->expensiveCache->clear( $path );
1292  }
1293  }
1294  $this->doClearCache( $paths );
1295  }
1296 
1304  protected function doClearCache( array $paths = null ) {
1305  }
1306 
1307  final public function preloadFileStat( array $params ) {
1308  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1309  $success = true; // no network errors
1310 
1311  $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
1312  $stats = $this->doGetFileStatMulti( $params );
1313  if ( $stats === null ) {
1314  return true; // not supported
1315  }
1316 
1317  $latest = !empty( $params['latest'] ); // use latest data?
1318  foreach ( $stats as $path => $stat ) {
1320  if ( $path === null ) {
1321  continue; // this shouldn't happen
1322  }
1323  if ( is_array( $stat ) ) { // file exists
1324  // Strongly consistent backends can automatically set "latest"
1325  $stat['latest'] = $stat['latest'] ?? $latest;
1326  $this->cheapCache->setField( $path, 'stat', $stat );
1327  $this->setFileCache( $path, $stat ); // update persistent cache
1328  if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
1329  $this->cheapCache->setField( $path, 'sha1',
1330  [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
1331  }
1332  if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
1333  $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
1334  $this->cheapCache->setField( $path, 'xattr',
1335  [ 'map' => $stat['xattr'], 'latest' => $latest ] );
1336  }
1337  } elseif ( $stat === false ) { // file does not exist
1338  $this->cheapCache->setField( $path, 'stat',
1339  $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
1340  $this->cheapCache->setField( $path, 'xattr',
1341  [ 'map' => false, 'latest' => $latest ] );
1342  $this->cheapCache->setField( $path, 'sha1',
1343  [ 'hash' => false, 'latest' => $latest ] );
1344  $this->logger->debug( __METHOD__ . ": File $path does not exist.\n" );
1345  } else { // an error occurred
1346  $success = false;
1347  $this->logger->warning( __METHOD__ . ": Could not stat file $path.\n" );
1348  }
1349  }
1350 
1351  return $success;
1352  }
1353 
1365  protected function doGetFileStatMulti( array $params ) {
1366  return null; // not supported
1367  }
1368 
1376  abstract protected function directoriesAreVirtual();
1377 
1388  final protected static function isValidShortContainerName( $container ) {
1389  // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
1390  // might be used by subclasses. Reserve the dot character for sanity.
1391  // The only way dots end up in containers (e.g. resolveStoragePath)
1392  // is due to the wikiId container prefix or the above suffixes.
1393  return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
1394  }
1395 
1405  final protected static function isValidContainerName( $container ) {
1406  // This accounts for NTFS, Swift, and Ceph restrictions
1407  // and disallows directory separators or traversal characters.
1408  // Note that matching strings URL encode to the same string;
1409  // in Swift/Ceph, the length restriction is *after* URL encoding.
1410  return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1411  }
1412 
1426  final protected function resolveStoragePath( $storagePath ) {
1427  list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
1428  if ( $backend === $this->name ) { // must be for this backend
1429  $relPath = self::normalizeContainerPath( $relPath );
1430  if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
1431  // Get shard for the normalized path if this container is sharded
1432  $cShard = $this->getContainerShard( $shortCont, $relPath );
1433  // Validate and sanitize the relative path (backend-specific)
1434  $relPath = $this->resolveContainerPath( $shortCont, $relPath );
1435  if ( $relPath !== null ) {
1436  // Prepend any wiki ID prefix to the container name
1437  $container = $this->fullContainerName( $shortCont );
1438  if ( self::isValidContainerName( $container ) ) {
1439  // Validate and sanitize the container name (backend-specific)
1440  $container = $this->resolveContainerName( "{$container}{$cShard}" );
1441  if ( $container !== null ) {
1442  return [ $container, $relPath, $cShard ];
1443  }
1444  }
1445  }
1446  }
1447  }
1448 
1449  return [ null, null, null ];
1450  }
1451 
1467  final protected function resolveStoragePathReal( $storagePath ) {
1468  list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
1469  if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
1470  return [ $container, $relPath ];
1471  }
1472 
1473  return [ null, null ];
1474  }
1475 
1484  final protected function getContainerShard( $container, $relPath ) {
1485  list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
1486  if ( $levels == 1 || $levels == 2 ) {
1487  // Hash characters are either base 16 or 36
1488  $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1489  // Get a regex that represents the shard portion of paths.
1490  // The concatenation of the captures gives us the shard.
1491  if ( $levels === 1 ) { // 16 or 36 shards per container
1492  $hashDirRegex = '(' . $char . ')';
1493  } else { // 256 or 1296 shards per container
1494  if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1495  $hashDirRegex = $char . '/(' . $char . '{2})';
1496  } else { // short hash dir format (e.g. "a/b/c")
1497  $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1498  }
1499  }
1500  // Allow certain directories to be above the hash dirs so as
1501  // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1502  // They must be 2+ chars to avoid any hash directory ambiguity.
1503  $m = [];
1504  if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1505  return '.' . implode( '', array_slice( $m, 1 ) );
1506  }
1507 
1508  return null; // failed to match
1509  }
1510 
1511  return ''; // no sharding
1512  }
1513 
1522  final public function isSingleShardPathInternal( $storagePath ) {
1523  list( , , $shard ) = $this->resolveStoragePath( $storagePath );
1524 
1525  return ( $shard !== null );
1526  }
1527 
1536  final protected function getContainerHashLevels( $container ) {
1537  if ( isset( $this->shardViaHashLevels[$container] ) ) {
1538  $config = $this->shardViaHashLevels[$container];
1539  $hashLevels = (int)$config['levels'];
1540  if ( $hashLevels == 1 || $hashLevels == 2 ) {
1541  $hashBase = (int)$config['base'];
1542  if ( $hashBase == 16 || $hashBase == 36 ) {
1543  return [ $hashLevels, $hashBase, $config['repeat'] ];
1544  }
1545  }
1546  }
1547 
1548  return [ 0, 0, false ]; // no sharding
1549  }
1550 
1557  final protected function getContainerSuffixes( $container ) {
1558  $shards = [];
1559  list( $digits, $base ) = $this->getContainerHashLevels( $container );
1560  if ( $digits > 0 ) {
1561  $numShards = $base ** $digits;
1562  for ( $index = 0; $index < $numShards; $index++ ) {
1563  $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
1564  }
1565  }
1566 
1567  return $shards;
1568  }
1569 
1576  final protected function fullContainerName( $container ) {
1577  if ( $this->domainId != '' ) {
1578  return "{$this->domainId}-$container";
1579  } else {
1580  return $container;
1581  }
1582  }
1583 
1592  protected function resolveContainerName( $container ) {
1593  return $container;
1594  }
1595 
1606  protected function resolveContainerPath( $container, $relStoragePath ) {
1607  return $relStoragePath;
1608  }
1609 
1616  private function containerCacheKey( $container ) {
1617  return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
1618  }
1619 
1626  final protected function setContainerCache( $container, array $val ) {
1627  $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
1628  }
1629 
1636  final protected function deleteContainerCache( $container ) {
1637  if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1638  trigger_error( "Unable to delete stat cache for container $container." );
1639  }
1640  }
1641 
1649  final protected function primeContainerCache( array $items ) {
1650  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1651 
1652  $paths = []; // list of storage paths
1653  $contNames = []; // (cache key => resolved container name)
1654  // Get all the paths/containers from the items...
1655  foreach ( $items as $item ) {
1656  if ( self::isStoragePath( $item ) ) {
1657  $paths[] = $item;
1658  } elseif ( is_string( $item ) ) { // full container name
1659  $contNames[$this->containerCacheKey( $item )] = $item;
1660  }
1661  }
1662  // Get all the corresponding cache keys for paths...
1663  foreach ( $paths as $path ) {
1664  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1665  if ( $fullCont !== null ) { // valid path for this backend
1666  $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1667  }
1668  }
1669 
1670  $contInfo = []; // (resolved container name => cache value)
1671  // Get all cache entries for these container cache keys...
1672  $values = $this->memCache->getMulti( array_keys( $contNames ) );
1673  foreach ( $values as $cacheKey => $val ) {
1674  $contInfo[$contNames[$cacheKey]] = $val;
1675  }
1676 
1677  // Populate the container process cache for the backend...
1678  $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1679  }
1680 
1688  protected function doPrimeContainerCache( array $containerInfo ) {
1689  }
1690 
1697  private function fileCacheKey( $path ) {
1698  return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path );
1699  }
1700 
1709  final protected function setFileCache( $path, array $val ) {
1711  if ( $path === null ) {
1712  return; // invalid storage path
1713  }
1714  $mtime = ConvertibleTimestamp::convert( TS_UNIX, $val['mtime'] );
1715  $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 );
1716  $key = $this->fileCacheKey( $path );
1717  // Set the cache unless it is currently salted.
1718  $this->memCache->set( $key, $val, $ttl );
1719  }
1720 
1729  final protected function deleteFileCache( $path ) {
1731  if ( $path === null ) {
1732  return; // invalid storage path
1733  }
1734  if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
1735  trigger_error( "Unable to delete stat cache for file $path." );
1736  }
1737  }
1738 
1746  final protected function primeFileCache( array $items ) {
1747  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1748 
1749  $paths = []; // list of storage paths
1750  $pathNames = []; // (cache key => storage path)
1751  // Get all the paths/containers from the items...
1752  foreach ( $items as $item ) {
1753  if ( self::isStoragePath( $item ) ) {
1754  $paths[] = FileBackend::normalizeStoragePath( $item );
1755  }
1756  }
1757  // Get rid of any paths that failed normalization...
1758  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1759  // Get all the corresponding cache keys for paths...
1760  foreach ( $paths as $path ) {
1761  list( , $rel, ) = $this->resolveStoragePath( $path );
1762  if ( $rel !== null ) { // valid path for this backend
1763  $pathNames[$this->fileCacheKey( $path )] = $path;
1764  }
1765  }
1766  // Get all cache entries for these file cache keys...
1767  $values = $this->memCache->getMulti( array_keys( $pathNames ) );
1768  foreach ( $values as $cacheKey => $val ) {
1769  $path = $pathNames[$cacheKey];
1770  if ( is_array( $val ) ) {
1771  $val['latest'] = false; // never completely trust cache
1772  $this->cheapCache->setField( $path, 'stat', $val );
1773  if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
1774  $this->cheapCache->setField( $path, 'sha1',
1775  [ 'hash' => $val['sha1'], 'latest' => false ] );
1776  }
1777  if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
1778  $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
1779  $this->cheapCache->setField( $path, 'xattr',
1780  [ 'map' => $val['xattr'], 'latest' => false ] );
1781  }
1782  }
1783  }
1784  }
1785 
1793  final protected static function normalizeXAttributes( array $xattr ) {
1794  $newXAttr = [ 'headers' => [], 'metadata' => [] ];
1795 
1796  foreach ( $xattr['headers'] as $name => $value ) {
1797  $newXAttr['headers'][strtolower( $name )] = $value;
1798  }
1799 
1800  foreach ( $xattr['metadata'] as $name => $value ) {
1801  $newXAttr['metadata'][strtolower( $name )] = $value;
1802  }
1803 
1804  return $newXAttr;
1805  }
1806 
1813  final protected function setConcurrencyFlags( array $opts ) {
1814  $opts['concurrency'] = 1; // off
1815  if ( $this->parallelize === 'implicit' ) {
1816  if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
1817  $opts['concurrency'] = $this->concurrency;
1818  }
1819  } elseif ( $this->parallelize === 'explicit' ) {
1820  if ( !empty( $opts['parallelize'] ) ) {
1821  $opts['concurrency'] = $this->concurrency;
1822  }
1823  }
1824 
1825  return $opts;
1826  }
1827 
1836  protected function getContentType( $storagePath, $content, $fsPath ) {
1837  if ( $this->mimeCallback ) {
1838  return call_user_func_array( $this->mimeCallback, func_get_args() );
1839  }
1840 
1841  $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false;
1842  return $mime ?: 'unknown/unknown';
1843  }
1844 }
1845 
1856  public $params = []; // params to caller functions
1858  public $backend;
1860  public $resourcesToClose = [];
1861 
1862  public $call; // string; name that identifies the function called
1863 
1867  public function closeResources() {
1868  array_map( 'fclose', $this->resourcesToClose );
1869  }
1870 }
1871 
1878 abstract class FileBackendStoreShardListIterator extends FilterIterator {
1880  protected $backend;
1881 
1883  protected $params;
1884 
1886  protected $container;
1887 
1889  protected $directory;
1890 
1892  protected $multiShardPaths = []; // (rel path => 1)
1893 
1901  public function __construct(
1902  FileBackendStore $backend, $container, $dir, array $suffixes, array $params
1903  ) {
1904  $this->backend = $backend;
1905  $this->container = $container;
1906  $this->directory = $dir;
1907  $this->params = $params;
1908 
1909  $iter = new AppendIterator();
1910  foreach ( $suffixes as $suffix ) {
1911  $iter->append( $this->listFromShard( $this->container . $suffix ) );
1912  }
1913 
1914  parent::__construct( $iter );
1915  }
1916 
1917  public function accept() {
1918  $rel = $this->getInnerIterator()->current(); // path relative to given directory
1919  $path = $this->params['dir'] . "/{$rel}"; // full storage path
1920  if ( $this->backend->isSingleShardPathInternal( $path ) ) {
1921  return true; // path is only on one shard; no issue with duplicates
1922  } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
1923  // Don't keep listing paths that are on multiple shards
1924  return false;
1925  } else {
1926  $this->multiShardPaths[$rel] = 1;
1927 
1928  return true;
1929  }
1930  }
1931 
1932  public function rewind() {
1933  parent::rewind();
1934  $this->multiShardPaths = [];
1935  }
1936 
1943  abstract protected function listFromShard( $container );
1944 }
1945 
1950  protected function listFromShard( $container ) {
1951  $list = $this->backend->getDirectoryListInternal(
1952  $container, $this->directory, $this->params );
1953  if ( $list === null ) {
1954  return new ArrayIterator( [] );
1955  } else {
1956  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1957  }
1958  }
1959 }
1960 
1965  protected function listFromShard( $container ) {
1966  $list = $this->backend->getFileListInternal(
1967  $container, $this->directory, $this->params );
1968  if ( $list === null ) {
1969  return new ArrayIterator( [] );
1970  } else {
1971  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1972  }
1973  }
1974 }
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
doDirectoryExists( $container, $dir, array $params)
getContainerHashLevels( $container)
Get the sharding config for a container.
newStatus()
Yields the result of the status wrapper callback on either:
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
getFileHttpUrl(array $params)
setFileCache( $path, array $val)
Set the cached stat info for a file path.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
doOperationsInternal(array $ops, array $opts)
MapCacheLRU $cheapCache
Map of paths to small (RAM/disk) cache items.
setConcurrencyFlags(array $opts)
Set the &#39;concurrency&#39; option from a list of operation options.
getContainerShard( $container, $relPath)
Get the container name shard suffix for a given path.
$success
Iterator for listing regular files.
moveInternal(array $params)
Move a file from one storage path to another in the backend.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
concatenate(array $params)
doGetLocalReferenceMulti(array $params)
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
doSecureInternal( $container, $dir, array $params)
scopedProfileSection( $section)
getPathsToLockForOpsInternal(array $performOps)
Get a list of storage paths to lock for a list of operations Returns an array with LockManager::LOCK_...
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it&#39;s allowed by the backend.
doPublishInternal( $container, $dir, array $params)
static normalizeXAttributes(array $xattr)
Normalize file headers/metadata to the FileBackend::getFileXAttributes() format.
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1315
doGetFileXAttributes(array $params)
getFileStat(array $params)
$value
The most up to date schema for the tables in the database will always be tables sql in the maintenance directory
Definition: schema.txt:2
setContainerCache( $container, array $val)
Set the cached info for a container.
__construct(array $config)
getName()
Get the unique backend name.
string $directory
Resolved relative path.
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
fileCacheKey( $path)
Get the cache key for a file path.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
string $container
Full container name.
getDirectoryListInternal( $container, $dir, array $params)
Do not call this function from places outside FileBackend.
FileBackendStore helper function to handle listings that span container shards.
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
int $concurrency
How many operations can be done in parallel.
storeInternal(array $params)
Store a file into the backend from a file on disk.
array $shardViaHashLevels
Map of container names to sharding config.
and how to run hooks for an and one after Each event has a name
Definition: hooks.txt:6
doCreateInternal(array $params)
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
if( $ext=='php'|| $ext=='php5') $mime
Definition: router.php:59
WANObjectCache $memCache
streamFile(array $params)
getFileSha1Base36(array $params)
const LOCK_UW
Definition: LockManager.php:68
doClean(array $params)
MapCacheLRU $expensiveCache
Map of paths to large (RAM/disk) cache items.
executeOpHandlesInternal(array $fileOpHandles)
Execute a list of FileBackendStoreOpHandle handles in parallel.
getContentType( $storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
getFileSize(array $params)
doQuickOperationsInternal(array $ops)
doPrepareInternal( $container, $dir, array $params)
File backend exception for checked exceptions (e.g.
Functions related to the output of file content.
createInternal(array $params)
Create a file in the backend with the given contents.
fileExists(array $params)
directoryExists(array $params)
const LOCK_EX
Definition: LockManager.php:69
$res
Definition: database.txt:21
doStreamFile(array $params)
getFileXAttributes(array $params)
nullInternal(array $params)
No-op file operation that does nothing.
deleteInternal(array $params)
Delete a file at the storage path.
preloadCache(array $paths)
$params
callable $obResetFunc
doGetLocalCopyMulti(array $params)
getFileContentsMulti(array $params)
getFileTimestamp(array $params)
maxFileSizeInternal()
Get the maximum allowable file size given backend medium restrictions and basic performance constrain...
static placeholderProps()
Placeholder file properties to use for files that don&#39;t exist.
Definition: FSFile.php:143
getOperationsInternal(array $ops)
Return a list of FileOp objects from a list of operations.
callable $streamMimeFunc
clearCache(array $paths=null)
deleteFileCache( $path)
Delete the cached stat info for a file path.
callable $mimeCallback
Method to get the MIME type of files.
getFileListInternal( $container, $dir, array $params)
Do not call this function from places outside FileBackend.
static send404Message( $fname, $flags=0)
Send out a standard 404 message for a file.
static isValidShortContainerName( $container)
Check if a short container name is valid.
doPrepare(array $params)
isSingleShardPathInternal( $storagePath)
Check if a storage path maps to a single shard.
static isValidContainerName( $container)
Check if a full container name is valid.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
doGetFileStatMulti(array $params)
Get file stat information (concurrently if possible) for several files.
copyInternal(array $params)
Copy a file from one storage path to another in the backend.
doGetFileStat(array $params)
string $name
Unique backend name.
Definition: FileBackend.php:94
doGetFileContentsMulti(array $params)
Base class for all backends using particular storage medium.
resolveContainerName( $container)
Resolve a container name, checking if it&#39;s allowed by the backend.
preloadFileStat(array $params)
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path.
getFileList(array $params)
closeResources()
Close all open file handles.
__construct(FileBackendStore $backend, $container, $dir, array $suffixes, array $params)
Iterator for listing directories.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
doSecure(array $params)
static attempt(array $performOps, array $opts, FileJournal $journal)
Attempt to perform a series of file operations.
Definition: FileOpBatch.php:56
resolveStoragePath( $storagePath)
Splits a storage path into an internal container name, an internal relative file name, and a container shard suffix.
doConcatenate(array $params)
doPublish(array $params)
deleteContainerCache( $container)
Delete the cached info for a container.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
doPrimeContainerCache(array $containerInfo)
Fill the backend-specific process cache given an array of resolved container names and their correspo...
doClearCache(array $paths=null)
Clears any additional stat caches for storage paths.
describeInternal(array $params)
Alter metadata for a file at the storage path.
doStoreInternal(array $params)
Base class for all file backend classes (including multi-write backends).
Definition: FileBackend.php:92
doDeleteInternal(array $params)
getTopDirectoryList(array $params)
Same as FileBackend::getDirectoryList() except only lists directories that are immediately under the ...
doCleanInternal( $container, $dir, array $params)
getScopedLocksForOps(array $ops, StatusValue $status)
fullContainerName( $container)
Get the full container name, including the wiki ID prefix.
doMoveInternal(array $params)
sanitizeOpHeaders(array $op)
Normalize and filter HTTP headers from a file operation.
doDescribeInternal(array $params)
getLocalReferenceMulti(array $params)
$content
Definition: pageupdater.txt:72
FileBackendStore helper class for performing asynchronous file operations.
doGetFileSha1Base36(array $params)
primeFileCache(array $items)
Do a batch lookup from cache for file stats for all paths used in a list of storage paths or FileOp o...
doCopyInternal(array $params)
getLocalReference(array $params)
Returns a file system file, identical to the file at a storage path.
getContainerSuffixes( $container)
Get a list of full container shard suffixes for a container.
getDirectoryList(array $params)
getLocalCopyMulti(array $params)
getFileProps(array $params)
containerCacheKey( $container)
Get the cache key for a container.
doExecuteOpHandlesInternal(array $fileOpHandles)