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', [
667  'path' => $path,
668  ] );
669  } else { // an error occurred
670  $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
671  'path' => $path,
672  ] );
673  }
674 
675  return $stat;
676  }
677 
682  abstract protected function doGetFileStat( array $params );
683 
684  public function getFileContentsMulti( array $params ) {
685  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
686 
687  $params = $this->setConcurrencyFlags( $params );
688  $contents = $this->doGetFileContentsMulti( $params );
689 
690  return $contents;
691  }
692 
698  protected function doGetFileContentsMulti( array $params ) {
699  $contents = [];
700  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
701  Wikimedia\suppressWarnings();
702  $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
703  Wikimedia\restoreWarnings();
704  }
705 
706  return $contents;
707  }
708 
709  final public function getFileXAttributes( array $params ) {
710  $path = self::normalizeStoragePath( $params['src'] );
711  if ( $path === null ) {
712  return false; // invalid storage path
713  }
714  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
715  $latest = !empty( $params['latest'] ); // use latest data?
716  if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
717  $stat = $this->cheapCache->getField( $path, 'xattr' );
718  // If we want the latest data, check that this cached
719  // value was in fact fetched with the latest available data.
720  if ( !$latest || $stat['latest'] ) {
721  return $stat['map'];
722  }
723  }
724  $fields = $this->doGetFileXAttributes( $params );
725  $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
726  $this->cheapCache->setField( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
727 
728  return $fields;
729  }
730 
736  protected function doGetFileXAttributes( array $params ) {
737  return [ 'headers' => [], 'metadata' => [] ]; // not supported
738  }
739 
740  final public function getFileSha1Base36( array $params ) {
741  $path = self::normalizeStoragePath( $params['src'] );
742  if ( $path === null ) {
743  return false; // invalid storage path
744  }
745  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
746  $latest = !empty( $params['latest'] ); // use latest data?
747  if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
748  $stat = $this->cheapCache->getField( $path, 'sha1' );
749  // If we want the latest data, check that this cached
750  // value was in fact fetched with the latest available data.
751  if ( !$latest || $stat['latest'] ) {
752  return $stat['hash'];
753  }
754  }
755  $hash = $this->doGetFileSha1Base36( $params );
756  $this->cheapCache->setField( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
757 
758  return $hash;
759  }
760 
766  protected function doGetFileSha1Base36( array $params ) {
767  $fsFile = $this->getLocalReference( $params );
768  if ( !$fsFile ) {
769  return false;
770  } else {
771  return $fsFile->getSha1Base36();
772  }
773  }
774 
775  final public function getFileProps( array $params ) {
776  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
777  $fsFile = $this->getLocalReference( $params );
778  $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
779 
780  return $props;
781  }
782 
783  final public function getLocalReferenceMulti( array $params ) {
784  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
785 
786  $params = $this->setConcurrencyFlags( $params );
787 
788  $fsFiles = []; // (path => FSFile)
789  $latest = !empty( $params['latest'] ); // use latest data?
790  // Reuse any files already in process cache...
791  foreach ( $params['srcs'] as $src ) {
792  $path = self::normalizeStoragePath( $src );
793  if ( $path === null ) {
794  $fsFiles[$src] = null; // invalid storage path
795  } elseif ( $this->expensiveCache->hasField( $path, 'localRef' ) ) {
796  $val = $this->expensiveCache->getField( $path, 'localRef' );
797  // If we want the latest data, check that this cached
798  // value was in fact fetched with the latest available data.
799  if ( !$latest || $val['latest'] ) {
800  $fsFiles[$src] = $val['object'];
801  }
802  }
803  }
804  // Fetch local references of any remaning files...
805  $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
806  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
807  $fsFiles[$path] = $fsFile;
808  if ( $fsFile ) { // update the process cache...
809  $this->expensiveCache->setField( $path, 'localRef',
810  [ 'object' => $fsFile, 'latest' => $latest ] );
811  }
812  }
813 
814  return $fsFiles;
815  }
816 
822  protected function doGetLocalReferenceMulti( array $params ) {
823  return $this->doGetLocalCopyMulti( $params );
824  }
825 
826  final public function getLocalCopyMulti( array $params ) {
827  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
828 
829  $params = $this->setConcurrencyFlags( $params );
830  $tmpFiles = $this->doGetLocalCopyMulti( $params );
831 
832  return $tmpFiles;
833  }
834 
840  abstract protected function doGetLocalCopyMulti( array $params );
841 
847  public function getFileHttpUrl( array $params ) {
848  return null; // not supported
849  }
850 
851  final public function streamFile( array $params ) {
852  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
853  $status = $this->newStatus();
854 
855  // Always set some fields for subclass convenience
856  $params['options'] = $params['options'] ?? [];
857  $params['headers'] = $params['headers'] ?? [];
858 
859  // Don't stream it out as text/html if there was a PHP error
860  if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
861  print "Headers already sent, terminating.\n";
862  $status->fatal( 'backend-fail-stream', $params['src'] );
863  return $status;
864  }
865 
866  $status->merge( $this->doStreamFile( $params ) );
867 
868  return $status;
869  }
870 
876  protected function doStreamFile( array $params ) {
877  $status = $this->newStatus();
878 
879  $flags = 0;
880  $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0;
881  $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0;
882 
883  $fsFile = $this->getLocalReference( $params );
884  if ( $fsFile ) {
885  $streamer = new HTTPFileStreamer(
886  $fsFile->getPath(),
887  [
888  'obResetFunc' => $this->obResetFunc,
889  'streamMimeFunc' => $this->streamMimeFunc
890  ]
891  );
892  $res = $streamer->stream( $params['headers'], true, $params['options'], $flags );
893  } else {
894  $res = false;
895  HTTPFileStreamer::send404Message( $params['src'], $flags );
896  }
897 
898  if ( !$res ) {
899  $status->fatal( 'backend-fail-stream', $params['src'] );
900  }
901 
902  return $status;
903  }
904 
905  final public function directoryExists( array $params ) {
906  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
907  if ( $dir === null ) {
908  return false; // invalid storage path
909  }
910  if ( $shard !== null ) { // confined to a single container/shard
911  return $this->doDirectoryExists( $fullCont, $dir, $params );
912  } else { // directory is on several shards
913  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
914  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
915  $res = false; // response
916  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
917  $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
918  if ( $exists ) {
919  $res = true;
920  break; // found one!
921  } elseif ( $exists === null ) { // error?
922  $res = null; // if we don't find anything, it is indeterminate
923  }
924  }
925 
926  return $res;
927  }
928  }
929 
938  abstract protected function doDirectoryExists( $container, $dir, array $params );
939 
940  final public function getDirectoryList( array $params ) {
941  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
942  if ( $dir === null ) { // invalid storage path
943  return null;
944  }
945  if ( $shard !== null ) {
946  // File listing is confined to a single container/shard
947  return $this->getDirectoryListInternal( $fullCont, $dir, $params );
948  } else {
949  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
950  // File listing spans multiple containers/shards
951  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
952 
953  return new FileBackendStoreShardDirIterator( $this,
954  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
955  }
956  }
957 
968  abstract public function getDirectoryListInternal( $container, $dir, array $params );
969 
970  final public function getFileList( array $params ) {
971  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
972  if ( $dir === null ) { // invalid storage path
973  return null;
974  }
975  if ( $shard !== null ) {
976  // File listing is confined to a single container/shard
977  return $this->getFileListInternal( $fullCont, $dir, $params );
978  } else {
979  $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
980  // File listing spans multiple containers/shards
981  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
982 
983  return new FileBackendStoreShardFileIterator( $this,
984  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
985  }
986  }
987 
998  abstract public function getFileListInternal( $container, $dir, array $params );
999 
1011  final public function getOperationsInternal( array $ops ) {
1012  $supportedOps = [
1013  'store' => StoreFileOp::class,
1014  'copy' => CopyFileOp::class,
1015  'move' => MoveFileOp::class,
1016  'delete' => DeleteFileOp::class,
1017  'create' => CreateFileOp::class,
1018  'describe' => DescribeFileOp::class,
1019  'null' => NullFileOp::class
1020  ];
1021 
1022  $performOps = []; // array of FileOp objects
1023  // Build up ordered array of FileOps...
1024  foreach ( $ops as $operation ) {
1025  $opName = $operation['op'];
1026  if ( isset( $supportedOps[$opName] ) ) {
1027  $class = $supportedOps[$opName];
1028  // Get params for this operation
1029  $params = $operation;
1030  // Append the FileOp class
1031  $performOps[] = new $class( $this, $params, $this->logger );
1032  } else {
1033  throw new FileBackendError( "Operation '$opName' is not supported." );
1034  }
1035  }
1036 
1037  return $performOps;
1038  }
1039 
1050  final public function getPathsToLockForOpsInternal( array $performOps ) {
1051  // Build up a list of files to lock...
1052  $paths = [ 'sh' => [], 'ex' => [] ];
1053  foreach ( $performOps as $fileOp ) {
1054  $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1055  $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1056  }
1057  // Optimization: if doing an EX lock anyway, don't also set an SH one
1058  $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1059  // Get a shared lock on the parent directory of each path changed
1060  $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1061 
1062  return [
1063  LockManager::LOCK_UW => $paths['sh'],
1064  LockManager::LOCK_EX => $paths['ex']
1065  ];
1066  }
1067 
1068  public function getScopedLocksForOps( array $ops, StatusValue $status ) {
1069  $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1070 
1071  return $this->getScopedFileLocks( $paths, 'mixed', $status );
1072  }
1073 
1074  final protected function doOperationsInternal( array $ops, array $opts ) {
1075  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1076  $status = $this->newStatus();
1077 
1078  // Fix up custom header name/value pairs...
1079  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1080 
1081  // Build up a list of FileOps...
1082  $performOps = $this->getOperationsInternal( $ops );
1083 
1084  // Acquire any locks as needed...
1085  if ( empty( $opts['nonLocking'] ) ) {
1086  // Build up a list of files to lock...
1087  $paths = $this->getPathsToLockForOpsInternal( $performOps );
1088  // Try to lock those files for the scope of this function...
1089 
1090  $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
1091  if ( !$status->isOK() ) {
1092  return $status; // abort
1093  }
1094  }
1095 
1096  // Clear any file cache entries (after locks acquired)
1097  if ( empty( $opts['preserveCache'] ) ) {
1098  $this->clearCache();
1099  }
1100 
1101  // Build the list of paths involved
1102  $paths = [];
1103  foreach ( $performOps as $performOp ) {
1104  $paths = array_merge( $paths, $performOp->storagePathsRead() );
1105  $paths = array_merge( $paths, $performOp->storagePathsChanged() );
1106  }
1107 
1108  // Enlarge the cache to fit the stat entries of these files
1109  $this->cheapCache->setMaxSize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
1110 
1111  // Load from the persistent container caches
1112  $this->primeContainerCache( $paths );
1113  // Get the latest stat info for all the files (having locked them)
1114  $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
1115 
1116  if ( $ok ) {
1117  // Actually attempt the operation batch...
1118  $opts = $this->setConcurrencyFlags( $opts );
1119  $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
1120  } else {
1121  // If we could not even stat some files, then bail out...
1122  $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
1123  foreach ( $ops as $i => $op ) { // mark each op as failed
1124  $subStatus->success[$i] = false;
1125  ++$subStatus->failCount;
1126  }
1127  $this->logger->error( static::class . "-{$this->name} " .
1128  " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
1129  }
1130 
1131  // Merge errors into StatusValue fields
1132  $status->merge( $subStatus );
1133  $status->success = $subStatus->success; // not done in merge()
1134 
1135  // Shrink the stat cache back to normal size
1136  $this->cheapCache->setMaxSize( self::CACHE_CHEAP_SIZE );
1137 
1138  return $status;
1139  }
1140 
1141  final protected function doQuickOperationsInternal( array $ops ) {
1142  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1143  $status = $this->newStatus();
1144 
1145  // Fix up custom header name/value pairs...
1146  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1147 
1148  // Clear any file cache entries
1149  $this->clearCache();
1150 
1151  $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
1152  // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
1153  $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
1154  $maxConcurrency = $this->concurrency; // throttle
1156  $statuses = []; // array of (index => StatusValue)
1157  $fileOpHandles = []; // list of (index => handle) arrays
1158  $curFileOpHandles = []; // current handle batch
1159  // Perform the sync-only ops and build up op handles for the async ops...
1160  foreach ( $ops as $index => $params ) {
1161  if ( !in_array( $params['op'], $supportedOps ) ) {
1162  throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
1163  }
1164  $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
1165  $subStatus = $this->$method( [ 'async' => $async ] + $params );
1166  if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1167  if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
1168  $fileOpHandles[] = $curFileOpHandles; // push this batch
1169  $curFileOpHandles = [];
1170  }
1171  $curFileOpHandles[$index] = $subStatus->value; // keep index
1172  } else { // error or completed
1173  $statuses[$index] = $subStatus; // keep index
1174  }
1175  }
1176  if ( count( $curFileOpHandles ) ) {
1177  $fileOpHandles[] = $curFileOpHandles; // last batch
1178  }
1179  // Do all the async ops that can be done concurrently...
1180  foreach ( $fileOpHandles as $fileHandleBatch ) {
1181  $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
1182  }
1183  // Marshall and merge all the responses...
1184  foreach ( $statuses as $index => $subStatus ) {
1185  $status->merge( $subStatus );
1186  if ( $subStatus->isOK() ) {
1187  $status->success[$index] = true;
1188  ++$status->successCount;
1189  } else {
1190  $status->success[$index] = false;
1191  ++$status->failCount;
1192  }
1193  }
1194 
1195  return $status;
1196  }
1197 
1207  final public function executeOpHandlesInternal( array $fileOpHandles ) {
1208  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1209 
1210  foreach ( $fileOpHandles as $fileOpHandle ) {
1211  if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1212  throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." );
1213  } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1214  throw new InvalidArgumentException( "Expected handle for this file backend." );
1215  }
1216  }
1217 
1218  $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1219  foreach ( $fileOpHandles as $fileOpHandle ) {
1220  $fileOpHandle->closeResources();
1221  }
1222 
1223  return $res;
1224  }
1225 
1234  protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1235  if ( count( $fileOpHandles ) ) {
1236  throw new LogicException( "Backend does not support asynchronous operations." );
1237  }
1238 
1239  return [];
1240  }
1241 
1253  protected function sanitizeOpHeaders( array $op ) {
1254  static $longs = [ 'content-disposition' ];
1255 
1256  if ( isset( $op['headers'] ) ) { // op sets HTTP headers
1257  $newHeaders = [];
1258  foreach ( $op['headers'] as $name => $value ) {
1259  $name = strtolower( $name );
1260  $maxHVLen = in_array( $name, $longs ) ? INF : 255;
1261  if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
1262  trigger_error( "Header '$name: $value' is too long." );
1263  } else {
1264  $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
1265  }
1266  }
1267  $op['headers'] = $newHeaders;
1268  }
1269 
1270  return $op;
1271  }
1272 
1273  final public function preloadCache( array $paths ) {
1274  $fullConts = []; // full container names
1275  foreach ( $paths as $path ) {
1276  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1277  $fullConts[] = $fullCont;
1278  }
1279  // Load from the persistent file and container caches
1280  $this->primeContainerCache( $fullConts );
1281  $this->primeFileCache( $paths );
1282  }
1283 
1284  final public function clearCache( array $paths = null ) {
1285  if ( is_array( $paths ) ) {
1286  $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1287  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1288  }
1289  if ( $paths === null ) {
1290  $this->cheapCache->clear();
1291  $this->expensiveCache->clear();
1292  } else {
1293  foreach ( $paths as $path ) {
1294  $this->cheapCache->clear( $path );
1295  $this->expensiveCache->clear( $path );
1296  }
1297  }
1298  $this->doClearCache( $paths );
1299  }
1300 
1308  protected function doClearCache( array $paths = null ) {
1309  }
1310 
1311  final public function preloadFileStat( array $params ) {
1312  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1313  $success = true; // no network errors
1314 
1315  $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
1316  $stats = $this->doGetFileStatMulti( $params );
1317  if ( $stats === null ) {
1318  return true; // not supported
1319  }
1320 
1321  $latest = !empty( $params['latest'] ); // use latest data?
1322  foreach ( $stats as $path => $stat ) {
1324  if ( $path === null ) {
1325  continue; // this shouldn't happen
1326  }
1327  if ( is_array( $stat ) ) { // file exists
1328  // Strongly consistent backends can automatically set "latest"
1329  $stat['latest'] = $stat['latest'] ?? $latest;
1330  $this->cheapCache->setField( $path, 'stat', $stat );
1331  $this->setFileCache( $path, $stat ); // update persistent cache
1332  if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
1333  $this->cheapCache->setField( $path, 'sha1',
1334  [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
1335  }
1336  if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
1337  $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
1338  $this->cheapCache->setField( $path, 'xattr',
1339  [ 'map' => $stat['xattr'], 'latest' => $latest ] );
1340  }
1341  } elseif ( $stat === false ) { // file does not exist
1342  $this->cheapCache->setField( $path, 'stat',
1343  $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
1344  $this->cheapCache->setField( $path, 'xattr',
1345  [ 'map' => false, 'latest' => $latest ] );
1346  $this->cheapCache->setField( $path, 'sha1',
1347  [ 'hash' => false, 'latest' => $latest ] );
1348  $this->logger->debug( __METHOD__ . ': File {path} does not exist', [
1349  'path' => $path,
1350  ] );
1351  } else { // an error occurred
1352  $success = false;
1353  $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
1354  'path' => $path,
1355  ] );
1356  }
1357  }
1358 
1359  return $success;
1360  }
1361 
1373  protected function doGetFileStatMulti( array $params ) {
1374  return null; // not supported
1375  }
1376 
1384  abstract protected function directoriesAreVirtual();
1385 
1396  final protected static function isValidShortContainerName( $container ) {
1397  // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
1398  // might be used by subclasses. Reserve the dot character for sanity.
1399  // The only way dots end up in containers (e.g. resolveStoragePath)
1400  // is due to the wikiId container prefix or the above suffixes.
1401  return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
1402  }
1403 
1413  final protected static function isValidContainerName( $container ) {
1414  // This accounts for NTFS, Swift, and Ceph restrictions
1415  // and disallows directory separators or traversal characters.
1416  // Note that matching strings URL encode to the same string;
1417  // in Swift/Ceph, the length restriction is *after* URL encoding.
1418  return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1419  }
1420 
1434  final protected function resolveStoragePath( $storagePath ) {
1435  list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
1436  if ( $backend === $this->name ) { // must be for this backend
1437  $relPath = self::normalizeContainerPath( $relPath );
1438  if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
1439  // Get shard for the normalized path if this container is sharded
1440  $cShard = $this->getContainerShard( $shortCont, $relPath );
1441  // Validate and sanitize the relative path (backend-specific)
1442  $relPath = $this->resolveContainerPath( $shortCont, $relPath );
1443  if ( $relPath !== null ) {
1444  // Prepend any wiki ID prefix to the container name
1445  $container = $this->fullContainerName( $shortCont );
1446  if ( self::isValidContainerName( $container ) ) {
1447  // Validate and sanitize the container name (backend-specific)
1448  $container = $this->resolveContainerName( "{$container}{$cShard}" );
1449  if ( $container !== null ) {
1450  return [ $container, $relPath, $cShard ];
1451  }
1452  }
1453  }
1454  }
1455  }
1456 
1457  return [ null, null, null ];
1458  }
1459 
1475  final protected function resolveStoragePathReal( $storagePath ) {
1476  list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
1477  if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
1478  return [ $container, $relPath ];
1479  }
1480 
1481  return [ null, null ];
1482  }
1483 
1492  final protected function getContainerShard( $container, $relPath ) {
1493  list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
1494  if ( $levels == 1 || $levels == 2 ) {
1495  // Hash characters are either base 16 or 36
1496  $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1497  // Get a regex that represents the shard portion of paths.
1498  // The concatenation of the captures gives us the shard.
1499  if ( $levels === 1 ) { // 16 or 36 shards per container
1500  $hashDirRegex = '(' . $char . ')';
1501  } else { // 256 or 1296 shards per container
1502  if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1503  $hashDirRegex = $char . '/(' . $char . '{2})';
1504  } else { // short hash dir format (e.g. "a/b/c")
1505  $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1506  }
1507  }
1508  // Allow certain directories to be above the hash dirs so as
1509  // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1510  // They must be 2+ chars to avoid any hash directory ambiguity.
1511  $m = [];
1512  if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1513  return '.' . implode( '', array_slice( $m, 1 ) );
1514  }
1515 
1516  return null; // failed to match
1517  }
1518 
1519  return ''; // no sharding
1520  }
1521 
1530  final public function isSingleShardPathInternal( $storagePath ) {
1531  list( , , $shard ) = $this->resolveStoragePath( $storagePath );
1532 
1533  return ( $shard !== null );
1534  }
1535 
1544  final protected function getContainerHashLevels( $container ) {
1545  if ( isset( $this->shardViaHashLevels[$container] ) ) {
1546  $config = $this->shardViaHashLevels[$container];
1547  $hashLevels = (int)$config['levels'];
1548  if ( $hashLevels == 1 || $hashLevels == 2 ) {
1549  $hashBase = (int)$config['base'];
1550  if ( $hashBase == 16 || $hashBase == 36 ) {
1551  return [ $hashLevels, $hashBase, $config['repeat'] ];
1552  }
1553  }
1554  }
1555 
1556  return [ 0, 0, false ]; // no sharding
1557  }
1558 
1565  final protected function getContainerSuffixes( $container ) {
1566  $shards = [];
1567  list( $digits, $base ) = $this->getContainerHashLevels( $container );
1568  if ( $digits > 0 ) {
1569  $numShards = $base ** $digits;
1570  for ( $index = 0; $index < $numShards; $index++ ) {
1571  $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
1572  }
1573  }
1574 
1575  return $shards;
1576  }
1577 
1584  final protected function fullContainerName( $container ) {
1585  if ( $this->domainId != '' ) {
1586  return "{$this->domainId}-$container";
1587  } else {
1588  return $container;
1589  }
1590  }
1591 
1600  protected function resolveContainerName( $container ) {
1601  return $container;
1602  }
1603 
1614  protected function resolveContainerPath( $container, $relStoragePath ) {
1615  return $relStoragePath;
1616  }
1617 
1624  private function containerCacheKey( $container ) {
1625  return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
1626  }
1627 
1634  final protected function setContainerCache( $container, array $val ) {
1635  $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
1636  }
1637 
1644  final protected function deleteContainerCache( $container ) {
1645  if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1646  trigger_error( "Unable to delete stat cache for container $container." );
1647  }
1648  }
1649 
1657  final protected function primeContainerCache( array $items ) {
1658  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1659 
1660  $paths = []; // list of storage paths
1661  $contNames = []; // (cache key => resolved container name)
1662  // Get all the paths/containers from the items...
1663  foreach ( $items as $item ) {
1664  if ( self::isStoragePath( $item ) ) {
1665  $paths[] = $item;
1666  } elseif ( is_string( $item ) ) { // full container name
1667  $contNames[$this->containerCacheKey( $item )] = $item;
1668  }
1669  }
1670  // Get all the corresponding cache keys for paths...
1671  foreach ( $paths as $path ) {
1672  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1673  if ( $fullCont !== null ) { // valid path for this backend
1674  $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1675  }
1676  }
1677 
1678  $contInfo = []; // (resolved container name => cache value)
1679  // Get all cache entries for these container cache keys...
1680  $values = $this->memCache->getMulti( array_keys( $contNames ) );
1681  foreach ( $values as $cacheKey => $val ) {
1682  $contInfo[$contNames[$cacheKey]] = $val;
1683  }
1684 
1685  // Populate the container process cache for the backend...
1686  $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1687  }
1688 
1696  protected function doPrimeContainerCache( array $containerInfo ) {
1697  }
1698 
1705  private function fileCacheKey( $path ) {
1706  return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path );
1707  }
1708 
1717  final protected function setFileCache( $path, array $val ) {
1719  if ( $path === null ) {
1720  return; // invalid storage path
1721  }
1722  $mtime = ConvertibleTimestamp::convert( TS_UNIX, $val['mtime'] );
1723  $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 );
1724  $key = $this->fileCacheKey( $path );
1725  // Set the cache unless it is currently salted.
1726  $this->memCache->set( $key, $val, $ttl );
1727  }
1728 
1737  final protected function deleteFileCache( $path ) {
1739  if ( $path === null ) {
1740  return; // invalid storage path
1741  }
1742  if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
1743  trigger_error( "Unable to delete stat cache for file $path." );
1744  }
1745  }
1746 
1754  final protected function primeFileCache( array $items ) {
1755  $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1756 
1757  $paths = []; // list of storage paths
1758  $pathNames = []; // (cache key => storage path)
1759  // Get all the paths/containers from the items...
1760  foreach ( $items as $item ) {
1761  if ( self::isStoragePath( $item ) ) {
1762  $paths[] = FileBackend::normalizeStoragePath( $item );
1763  }
1764  }
1765  // Get rid of any paths that failed normalization...
1766  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1767  // Get all the corresponding cache keys for paths...
1768  foreach ( $paths as $path ) {
1769  list( , $rel, ) = $this->resolveStoragePath( $path );
1770  if ( $rel !== null ) { // valid path for this backend
1771  $pathNames[$this->fileCacheKey( $path )] = $path;
1772  }
1773  }
1774  // Get all cache entries for these file cache keys...
1775  $values = $this->memCache->getMulti( array_keys( $pathNames ) );
1776  foreach ( $values as $cacheKey => $val ) {
1777  $path = $pathNames[$cacheKey];
1778  if ( is_array( $val ) ) {
1779  $val['latest'] = false; // never completely trust cache
1780  $this->cheapCache->setField( $path, 'stat', $val );
1781  if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
1782  $this->cheapCache->setField( $path, 'sha1',
1783  [ 'hash' => $val['sha1'], 'latest' => false ] );
1784  }
1785  if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
1786  $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
1787  $this->cheapCache->setField( $path, 'xattr',
1788  [ 'map' => $val['xattr'], 'latest' => false ] );
1789  }
1790  }
1791  }
1792  }
1793 
1801  final protected static function normalizeXAttributes( array $xattr ) {
1802  $newXAttr = [ 'headers' => [], 'metadata' => [] ];
1803 
1804  foreach ( $xattr['headers'] as $name => $value ) {
1805  $newXAttr['headers'][strtolower( $name )] = $value;
1806  }
1807 
1808  foreach ( $xattr['metadata'] as $name => $value ) {
1809  $newXAttr['metadata'][strtolower( $name )] = $value;
1810  }
1811 
1812  return $newXAttr;
1813  }
1814 
1821  final protected function setConcurrencyFlags( array $opts ) {
1822  $opts['concurrency'] = 1; // off
1823  if ( $this->parallelize === 'implicit' ) {
1824  if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
1825  $opts['concurrency'] = $this->concurrency;
1826  }
1827  } elseif ( $this->parallelize === 'explicit' ) {
1828  if ( !empty( $opts['parallelize'] ) ) {
1829  $opts['concurrency'] = $this->concurrency;
1830  }
1831  }
1832 
1833  return $opts;
1834  }
1835 
1844  protected function getContentType( $storagePath, $content, $fsPath ) {
1845  if ( $this->mimeCallback ) {
1846  return call_user_func_array( $this->mimeCallback, func_get_args() );
1847  }
1848 
1849  $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false;
1850  return $mime ?: 'unknown/unknown';
1851  }
1852 }
1853 
1864  public $params = []; // params to caller functions
1866  public $backend;
1868  public $resourcesToClose = [];
1869 
1870  public $call; // string; name that identifies the function called
1871 
1875  public function closeResources() {
1876  array_map( 'fclose', $this->resourcesToClose );
1877  }
1878 }
1879 
1886 abstract class FileBackendStoreShardListIterator extends FilterIterator {
1888  protected $backend;
1889 
1891  protected $params;
1892 
1894  protected $container;
1895 
1897  protected $directory;
1898 
1900  protected $multiShardPaths = []; // (rel path => 1)
1901 
1909  public function __construct(
1910  FileBackendStore $backend, $container, $dir, array $suffixes, array $params
1911  ) {
1912  $this->backend = $backend;
1913  $this->container = $container;
1914  $this->directory = $dir;
1915  $this->params = $params;
1916 
1917  $iter = new AppendIterator();
1918  foreach ( $suffixes as $suffix ) {
1919  $iter->append( $this->listFromShard( $this->container . $suffix ) );
1920  }
1921 
1922  parent::__construct( $iter );
1923  }
1924 
1925  public function accept() {
1926  $rel = $this->getInnerIterator()->current(); // path relative to given directory
1927  $path = $this->params['dir'] . "/{$rel}"; // full storage path
1928  if ( $this->backend->isSingleShardPathInternal( $path ) ) {
1929  return true; // path is only on one shard; no issue with duplicates
1930  } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
1931  // Don't keep listing paths that are on multiple shards
1932  return false;
1933  } else {
1934  $this->multiShardPaths[$rel] = 1;
1935 
1936  return true;
1937  }
1938  }
1939 
1940  public function rewind() {
1941  parent::rewind();
1942  $this->multiShardPaths = [];
1943  }
1944 
1951  abstract protected function listFromShard( $container );
1952 }
1953 
1958  protected function listFromShard( $container ) {
1959  $list = $this->backend->getDirectoryListInternal(
1960  $container, $this->directory, $this->params );
1961  if ( $list === null ) {
1962  return new ArrayIterator( [] );
1963  } else {
1964  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1965  }
1966  }
1967 }
1968 
1973  protected function listFromShard( $container ) {
1974  $list = $this->backend->getFileListInternal(
1975  $container, $this->directory, $this->params );
1976  if ( $list === null ) {
1977  return new ArrayIterator( [] );
1978  } else {
1979  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1980  }
1981  }
1982 }
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:1277
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)