MediaWiki  1.27.2
FileBackendStore.php
Go to the documentation of this file.
1 <?php
38 abstract class FileBackendStore extends FileBackend {
40  protected $memCache;
42  protected $cheapCache;
44  protected $expensiveCache;
45 
47  protected $shardViaHashLevels = [];
48 
50  protected $mimeCallback;
51 
52  protected $maxFileSize = 4294967296; // integer bytes (4GiB)
53 
54  const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
55  const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
56  const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
57 
68  public function __construct( array $config ) {
69  parent::__construct( $config );
70  $this->mimeCallback = isset( $config['mimeCallback'] )
71  ? $config['mimeCallback']
72  : null;
73  $this->memCache = WANObjectCache::newEmpty(); // disabled by default
74  $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
75  $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
76  }
77 
85  final public function maxFileSizeInternal() {
86  return $this->maxFileSize;
87  }
88 
98  abstract public function isPathUsableInternal( $storagePath );
99 
118  final public function createInternal( array $params ) {
119  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
120  if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
121  $status = Status::newFatal( 'backend-fail-maxsize',
122  $params['dst'], $this->maxFileSizeInternal() );
123  } else {
124  $status = $this->doCreateInternal( $params );
125  $this->clearCache( [ $params['dst'] ] );
126  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
127  $this->deleteFileCache( $params['dst'] ); // persistent cache
128  }
129  }
130 
131  return $status;
132  }
133 
139  abstract protected function doCreateInternal( array $params );
140 
159  final public function storeInternal( array $params ) {
160  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
161  if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
162  $status = Status::newFatal( 'backend-fail-maxsize',
163  $params['dst'], $this->maxFileSizeInternal() );
164  } else {
165  $status = $this->doStoreInternal( $params );
166  $this->clearCache( [ $params['dst'] ] );
167  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
168  $this->deleteFileCache( $params['dst'] ); // persistent cache
169  }
170  }
171 
172  return $status;
173  }
174 
180  abstract protected function doStoreInternal( array $params );
181 
201  final public function copyInternal( array $params ) {
202  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
203  $status = $this->doCopyInternal( $params );
204  $this->clearCache( [ $params['dst'] ] );
205  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
206  $this->deleteFileCache( $params['dst'] ); // persistent cache
207  }
208 
209  return $status;
210  }
211 
217  abstract protected function doCopyInternal( array $params );
218 
233  final public function deleteInternal( array $params ) {
234  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
235  $status = $this->doDeleteInternal( $params );
236  $this->clearCache( [ $params['src'] ] );
237  $this->deleteFileCache( $params['src'] ); // persistent cache
238  return $status;
239  }
240 
246  abstract protected function doDeleteInternal( array $params );
247 
267  final public function moveInternal( array $params ) {
268  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
269  $status = $this->doMoveInternal( $params );
270  $this->clearCache( [ $params['src'], $params['dst'] ] );
271  $this->deleteFileCache( $params['src'] ); // persistent cache
272  if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
273  $this->deleteFileCache( $params['dst'] ); // persistent cache
274  }
275 
276  return $status;
277  }
278 
284  protected function doMoveInternal( array $params ) {
285  unset( $params['async'] ); // two steps, won't work here :)
286  $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
287  $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
288  // Copy source to dest
289  $status = $this->copyInternal( $params );
290  if ( $nsrc !== $ndst && $status->isOK() ) {
291  // Delete source (only fails due to races or network problems)
292  $status->merge( $this->deleteInternal( [ 'src' => $params['src'] ] ) );
293  $status->setResult( true, $status->value ); // ignore delete() errors
294  }
295 
296  return $status;
297  }
298 
313  final public function describeInternal( array $params ) {
314  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
315  if ( count( $params['headers'] ) ) {
316  $status = $this->doDescribeInternal( $params );
317  $this->clearCache( [ $params['src'] ] );
318  $this->deleteFileCache( $params['src'] ); // persistent cache
319  } else {
320  $status = Status::newGood(); // nothing to do
321  }
322 
323  return $status;
324  }
325 
331  protected function doDescribeInternal( array $params ) {
332  return Status::newGood();
333  }
334 
342  final public function nullInternal( array $params ) {
343  return Status::newGood();
344  }
345 
346  final public function concatenate( array $params ) {
347  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
349 
350  // Try to lock the source files for the scope of this function
351  $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
352  if ( $status->isOK() ) {
353  // Actually do the file concatenation...
354  $start_time = microtime( true );
355  $status->merge( $this->doConcatenate( $params ) );
356  $sec = microtime( true ) - $start_time;
357  if ( !$status->isOK() ) {
358  wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name}" .
359  " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
360  }
361  }
362 
363  return $status;
364  }
365 
371  protected function doConcatenate( array $params ) {
373  $tmpPath = $params['dst']; // convenience
374  unset( $params['latest'] ); // sanity
375 
376  // Check that the specified temp file is valid...
377  MediaWiki\suppressWarnings();
378  $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
379  MediaWiki\restoreWarnings();
380  if ( !$ok ) { // not present or not empty
381  $status->fatal( 'backend-fail-opentemp', $tmpPath );
382 
383  return $status;
384  }
385 
386  // Get local FS versions of the chunks needed for the concatenation...
387  $fsFiles = $this->getLocalReferenceMulti( $params );
388  foreach ( $fsFiles as $path => &$fsFile ) {
389  if ( !$fsFile ) { // chunk failed to download?
390  $fsFile = $this->getLocalReference( [ 'src' => $path ] );
391  if ( !$fsFile ) { // retry failed?
392  $status->fatal( 'backend-fail-read', $path );
393 
394  return $status;
395  }
396  }
397  }
398  unset( $fsFile ); // unset reference so we can reuse $fsFile
399 
400  // Get a handle for the destination temp file
401  $tmpHandle = fopen( $tmpPath, 'ab' );
402  if ( $tmpHandle === false ) {
403  $status->fatal( 'backend-fail-opentemp', $tmpPath );
404 
405  return $status;
406  }
407 
408  // Build up the temp file using the source chunks (in order)...
409  foreach ( $fsFiles as $virtualSource => $fsFile ) {
410  // Get a handle to the local FS version
411  $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
412  if ( $sourceHandle === false ) {
413  fclose( $tmpHandle );
414  $status->fatal( 'backend-fail-read', $virtualSource );
415 
416  return $status;
417  }
418  // Append chunk to file (pass chunk size to avoid magic quotes)
419  if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
420  fclose( $sourceHandle );
421  fclose( $tmpHandle );
422  $status->fatal( 'backend-fail-writetemp', $tmpPath );
423 
424  return $status;
425  }
426  fclose( $sourceHandle );
427  }
428  if ( !fclose( $tmpHandle ) ) {
429  $status->fatal( 'backend-fail-closetemp', $tmpPath );
430 
431  return $status;
432  }
433 
434  clearstatcache(); // temp file changed
435 
436  return $status;
437  }
438 
439  final protected function doPrepare( array $params ) {
440  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
442 
443  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
444  if ( $dir === null ) {
445  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
446 
447  return $status; // invalid storage path
448  }
449 
450  if ( $shard !== null ) { // confined to a single container/shard
451  $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
452  } else { // directory is on several shards
453  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
454  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
455  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
456  $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
457  }
458  }
459 
460  return $status;
461  }
462 
470  protected function doPrepareInternal( $container, $dir, array $params ) {
471  return Status::newGood();
472  }
473 
474  final protected function doSecure( array $params ) {
475  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
477 
478  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
479  if ( $dir === null ) {
480  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
481 
482  return $status; // invalid storage path
483  }
484 
485  if ( $shard !== null ) { // confined to a single container/shard
486  $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
487  } else { // directory is on several shards
488  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
489  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
490  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
491  $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
492  }
493  }
494 
495  return $status;
496  }
497 
505  protected function doSecureInternal( $container, $dir, array $params ) {
506  return Status::newGood();
507  }
508 
509  final protected function doPublish( array $params ) {
510  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
512 
513  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
514  if ( $dir === null ) {
515  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
516 
517  return $status; // invalid storage path
518  }
519 
520  if ( $shard !== null ) { // confined to a single container/shard
521  $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
522  } else { // directory is on several shards
523  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
524  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
525  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
526  $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
527  }
528  }
529 
530  return $status;
531  }
532 
540  protected function doPublishInternal( $container, $dir, array $params ) {
541  return Status::newGood();
542  }
543 
544  final protected function doClean( array $params ) {
545  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
547 
548  // Recursive: first delete all empty subdirs recursively
549  if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
550  $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
551  if ( $subDirsRel !== null ) { // no errors
552  foreach ( $subDirsRel as $subDirRel ) {
553  $subDir = $params['dir'] . "/{$subDirRel}"; // full path
554  $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
555  }
556  unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
557  }
558  }
559 
560  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
561  if ( $dir === null ) {
562  $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
563 
564  return $status; // invalid storage path
565  }
566 
567  // Attempt to lock this directory...
568  $filesLockEx = [ $params['dir'] ];
569  $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
570  if ( !$status->isOK() ) {
571  return $status; // abort
572  }
573 
574  if ( $shard !== null ) { // confined to a single container/shard
575  $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
576  $this->deleteContainerCache( $fullCont ); // purge cache
577  } else { // directory is on several shards
578  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
579  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
580  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
581  $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
582  $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
583  }
584  }
585 
586  return $status;
587  }
588 
596  protected function doCleanInternal( $container, $dir, array $params ) {
597  return Status::newGood();
598  }
599 
600  final public function fileExists( array $params ) {
601  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
602  $stat = $this->getFileStat( $params );
603 
604  return ( $stat === null ) ? null : (bool)$stat; // null => failure
605  }
606 
607  final public function getFileTimestamp( array $params ) {
608  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
609  $stat = $this->getFileStat( $params );
610 
611  return $stat ? $stat['mtime'] : false;
612  }
613 
614  final public function getFileSize( array $params ) {
615  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
616  $stat = $this->getFileStat( $params );
617 
618  return $stat ? $stat['size'] : false;
619  }
620 
621  final public function getFileStat( array $params ) {
622  $path = self::normalizeStoragePath( $params['src'] );
623  if ( $path === null ) {
624  return false; // invalid storage path
625  }
626  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
627  $latest = !empty( $params['latest'] ); // use latest data?
628  if ( !$latest && !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
629  $this->primeFileCache( [ $path ] ); // check persistent cache
630  }
631  if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
632  $stat = $this->cheapCache->get( $path, 'stat' );
633  // If we want the latest data, check that this cached
634  // value was in fact fetched with the latest available data.
635  if ( is_array( $stat ) ) {
636  if ( !$latest || $stat['latest'] ) {
637  return $stat;
638  }
639  } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ] ) ) {
640  if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
641  return false;
642  }
643  }
644  }
645  $stat = $this->doGetFileStat( $params );
646  if ( is_array( $stat ) ) { // file exists
647  // Strongly consistent backends can automatically set "latest"
648  $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
649  $this->cheapCache->set( $path, 'stat', $stat );
650  $this->setFileCache( $path, $stat ); // update persistent cache
651  if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
652  $this->cheapCache->set( $path, 'sha1',
653  [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
654  }
655  if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
656  $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
657  $this->cheapCache->set( $path, 'xattr',
658  [ 'map' => $stat['xattr'], 'latest' => $latest ] );
659  }
660  } elseif ( $stat === false ) { // file does not exist
661  $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
662  $this->cheapCache->set( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
663  $this->cheapCache->set( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
664  wfDebug( __METHOD__ . ": File $path does not exist.\n" );
665  } else { // an error occurred
666  wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
667  }
668 
669  return $stat;
670  }
671 
675  abstract protected function doGetFileStat( array $params );
676 
677  public function getFileContentsMulti( array $params ) {
678  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
679 
680  $params = $this->setConcurrencyFlags( $params );
681  $contents = $this->doGetFileContentsMulti( $params );
682 
683  return $contents;
684  }
685 
691  protected function doGetFileContentsMulti( array $params ) {
692  $contents = [];
693  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
694  MediaWiki\suppressWarnings();
695  $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
696  MediaWiki\restoreWarnings();
697  }
698 
699  return $contents;
700  }
701 
702  final public function getFileXAttributes( array $params ) {
703  $path = self::normalizeStoragePath( $params['src'] );
704  if ( $path === null ) {
705  return false; // invalid storage path
706  }
707  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
708  $latest = !empty( $params['latest'] ); // use latest data?
709  if ( $this->cheapCache->has( $path, 'xattr', self::CACHE_TTL ) ) {
710  $stat = $this->cheapCache->get( $path, 'xattr' );
711  // If we want the latest data, check that this cached
712  // value was in fact fetched with the latest available data.
713  if ( !$latest || $stat['latest'] ) {
714  return $stat['map'];
715  }
716  }
717  $fields = $this->doGetFileXAttributes( $params );
718  $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
719  $this->cheapCache->set( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
720 
721  return $fields;
722  }
723 
728  protected function doGetFileXAttributes( array $params ) {
729  return [ 'headers' => [], 'metadata' => [] ]; // not supported
730  }
731 
732  final public function getFileSha1Base36( array $params ) {
733  $path = self::normalizeStoragePath( $params['src'] );
734  if ( $path === null ) {
735  return false; // invalid storage path
736  }
737  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
738  $latest = !empty( $params['latest'] ); // use latest data?
739  if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
740  $stat = $this->cheapCache->get( $path, 'sha1' );
741  // If we want the latest data, check that this cached
742  // value was in fact fetched with the latest available data.
743  if ( !$latest || $stat['latest'] ) {
744  return $stat['hash'];
745  }
746  }
747  $hash = $this->doGetFileSha1Base36( $params );
748  $this->cheapCache->set( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
749 
750  return $hash;
751  }
752 
758  protected function doGetFileSha1Base36( array $params ) {
759  $fsFile = $this->getLocalReference( $params );
760  if ( !$fsFile ) {
761  return false;
762  } else {
763  return $fsFile->getSha1Base36();
764  }
765  }
766 
767  final public function getFileProps( array $params ) {
768  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
769  $fsFile = $this->getLocalReference( $params );
770  $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
771 
772  return $props;
773  }
774 
775  final public function getLocalReferenceMulti( array $params ) {
776  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
777 
778  $params = $this->setConcurrencyFlags( $params );
779 
780  $fsFiles = []; // (path => FSFile)
781  $latest = !empty( $params['latest'] ); // use latest data?
782  // Reuse any files already in process cache...
783  foreach ( $params['srcs'] as $src ) {
784  $path = self::normalizeStoragePath( $src );
785  if ( $path === null ) {
786  $fsFiles[$src] = null; // invalid storage path
787  } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
788  $val = $this->expensiveCache->get( $path, 'localRef' );
789  // If we want the latest data, check that this cached
790  // value was in fact fetched with the latest available data.
791  if ( !$latest || $val['latest'] ) {
792  $fsFiles[$src] = $val['object'];
793  }
794  }
795  }
796  // Fetch local references of any remaning files...
797  $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
798  foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
799  $fsFiles[$path] = $fsFile;
800  if ( $fsFile ) { // update the process cache...
801  $this->expensiveCache->set( $path, 'localRef',
802  [ 'object' => $fsFile, 'latest' => $latest ] );
803  }
804  }
805 
806  return $fsFiles;
807  }
808 
814  protected function doGetLocalReferenceMulti( array $params ) {
815  return $this->doGetLocalCopyMulti( $params );
816  }
817 
818  final public function getLocalCopyMulti( array $params ) {
819  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
820 
821  $params = $this->setConcurrencyFlags( $params );
822  $tmpFiles = $this->doGetLocalCopyMulti( $params );
823 
824  return $tmpFiles;
825  }
826 
832  abstract protected function doGetLocalCopyMulti( array $params );
833 
839  public function getFileHttpUrl( array $params ) {
840  return null; // not supported
841  }
842 
843  final public function streamFile( array $params ) {
844  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
846 
847  $info = $this->getFileStat( $params );
848  if ( !$info ) { // let StreamFile handle the 404
849  $status->fatal( 'backend-fail-notexists', $params['src'] );
850  }
851 
852  // Set output buffer and HTTP headers for stream
853  $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : [];
854  $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
855  if ( $res == StreamFile::NOT_MODIFIED ) {
856  // do nothing; client cache is up to date
857  } elseif ( $res == StreamFile::READY_STREAM ) {
858  $status = $this->doStreamFile( $params );
859  if ( !$status->isOK() ) {
860  // Per bug 41113, nasty things can happen if bad cache entries get
861  // stuck in cache. It's also possible that this error can come up
862  // with simple race conditions. Clear out the stat cache to be safe.
863  $this->clearCache( [ $params['src'] ] );
864  $this->deleteFileCache( $params['src'] );
865  trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
866  }
867  } else {
868  $status->fatal( 'backend-fail-stream', $params['src'] );
869  }
870 
871  return $status;
872  }
873 
879  protected function doStreamFile( array $params ) {
881 
882  $fsFile = $this->getLocalReference( $params );
883  if ( !$fsFile ) {
884  $status->fatal( 'backend-fail-stream', $params['src'] );
885  } elseif ( !readfile( $fsFile->getPath() ) ) {
886  $status->fatal( 'backend-fail-stream', $params['src'] );
887  }
888 
889  return $status;
890  }
891 
892  final public function directoryExists( array $params ) {
893  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
894  if ( $dir === null ) {
895  return false; // invalid storage path
896  }
897  if ( $shard !== null ) { // confined to a single container/shard
898  return $this->doDirectoryExists( $fullCont, $dir, $params );
899  } else { // directory is on several shards
900  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
901  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
902  $res = false; // response
903  foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
904  $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
905  if ( $exists ) {
906  $res = true;
907  break; // found one!
908  } elseif ( $exists === null ) { // error?
909  $res = null; // if we don't find anything, it is indeterminate
910  }
911  }
912 
913  return $res;
914  }
915  }
916 
925  abstract protected function doDirectoryExists( $container, $dir, array $params );
926 
927  final public function getDirectoryList( array $params ) {
928  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
929  if ( $dir === null ) { // invalid storage path
930  return null;
931  }
932  if ( $shard !== null ) {
933  // File listing is confined to a single container/shard
934  return $this->getDirectoryListInternal( $fullCont, $dir, $params );
935  } else {
936  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
937  // File listing spans multiple containers/shards
938  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
939 
940  return new FileBackendStoreShardDirIterator( $this,
941  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
942  }
943  }
944 
955  abstract public function getDirectoryListInternal( $container, $dir, array $params );
956 
957  final public function getFileList( array $params ) {
958  list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
959  if ( $dir === null ) { // invalid storage path
960  return null;
961  }
962  if ( $shard !== null ) {
963  // File listing is confined to a single container/shard
964  return $this->getFileListInternal( $fullCont, $dir, $params );
965  } else {
966  wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
967  // File listing spans multiple containers/shards
968  list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
969 
970  return new FileBackendStoreShardFileIterator( $this,
971  $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
972  }
973  }
974 
985  abstract public function getFileListInternal( $container, $dir, array $params );
986 
998  final public function getOperationsInternal( array $ops ) {
999  $supportedOps = [
1000  'store' => 'StoreFileOp',
1001  'copy' => 'CopyFileOp',
1002  'move' => 'MoveFileOp',
1003  'delete' => 'DeleteFileOp',
1004  'create' => 'CreateFileOp',
1005  'describe' => 'DescribeFileOp',
1006  'null' => 'NullFileOp'
1007  ];
1008 
1009  $performOps = []; // array of FileOp objects
1010  // Build up ordered array of FileOps...
1011  foreach ( $ops as $operation ) {
1012  $opName = $operation['op'];
1013  if ( isset( $supportedOps[$opName] ) ) {
1014  $class = $supportedOps[$opName];
1015  // Get params for this operation
1016  $params = $operation;
1017  // Append the FileOp class
1018  $performOps[] = new $class( $this, $params );
1019  } else {
1020  throw new FileBackendError( "Operation '$opName' is not supported." );
1021  }
1022  }
1023 
1024  return $performOps;
1025  }
1026 
1037  final public function getPathsToLockForOpsInternal( array $performOps ) {
1038  // Build up a list of files to lock...
1039  $paths = [ 'sh' => [], 'ex' => [] ];
1040  foreach ( $performOps as $fileOp ) {
1041  $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1042  $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1043  }
1044  // Optimization: if doing an EX lock anyway, don't also set an SH one
1045  $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1046  // Get a shared lock on the parent directory of each path changed
1047  $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1048 
1049  return [
1050  LockManager::LOCK_UW => $paths['sh'],
1051  LockManager::LOCK_EX => $paths['ex']
1052  ];
1053  }
1054 
1055  public function getScopedLocksForOps( array $ops, Status $status ) {
1056  $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1057 
1058  return $this->getScopedFileLocks( $paths, 'mixed', $status );
1059  }
1060 
1061  final protected function doOperationsInternal( array $ops, array $opts ) {
1062  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1064 
1065  // Fix up custom header name/value pairs...
1066  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1067 
1068  // Build up a list of FileOps...
1069  $performOps = $this->getOperationsInternal( $ops );
1070 
1071  // Acquire any locks as needed...
1072  if ( empty( $opts['nonLocking'] ) ) {
1073  // Build up a list of files to lock...
1074  $paths = $this->getPathsToLockForOpsInternal( $performOps );
1075  // Try to lock those files for the scope of this function...
1076 
1077  $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
1078  if ( !$status->isOK() ) {
1079  return $status; // abort
1080  }
1081  }
1082 
1083  // Clear any file cache entries (after locks acquired)
1084  if ( empty( $opts['preserveCache'] ) ) {
1085  $this->clearCache();
1086  }
1087 
1088  // Build the list of paths involved
1089  $paths = [];
1090  foreach ( $performOps as $op ) {
1091  $paths = array_merge( $paths, $op->storagePathsRead() );
1092  $paths = array_merge( $paths, $op->storagePathsChanged() );
1093  }
1094 
1095  // Enlarge the cache to fit the stat entries of these files
1096  $this->cheapCache->resize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
1097 
1098  // Load from the persistent container caches
1099  $this->primeContainerCache( $paths );
1100  // Get the latest stat info for all the files (having locked them)
1101  $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
1102 
1103  if ( $ok ) {
1104  // Actually attempt the operation batch...
1105  $opts = $this->setConcurrencyFlags( $opts );
1106  $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
1107  } else {
1108  // If we could not even stat some files, then bail out...
1109  $subStatus = Status::newFatal( 'backend-fail-internal', $this->name );
1110  foreach ( $ops as $i => $op ) { // mark each op as failed
1111  $subStatus->success[$i] = false;
1112  ++$subStatus->failCount;
1113  }
1114  wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name} " .
1115  " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
1116  }
1117 
1118  // Merge errors into status fields
1119  $status->merge( $subStatus );
1120  $status->success = $subStatus->success; // not done in merge()
1121 
1122  // Shrink the stat cache back to normal size
1123  $this->cheapCache->resize( self::CACHE_CHEAP_SIZE );
1124 
1125  return $status;
1126  }
1127 
1128  final protected function doQuickOperationsInternal( array $ops ) {
1129  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1131 
1132  // Fix up custom header name/value pairs...
1133  $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
1134 
1135  // Clear any file cache entries
1136  $this->clearCache();
1137 
1138  $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
1139  // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
1140  $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
1141  $maxConcurrency = $this->concurrency; // throttle
1142 
1143  $statuses = []; // array of (index => Status)
1144  $fileOpHandles = []; // list of (index => handle) arrays
1145  $curFileOpHandles = []; // current handle batch
1146  // Perform the sync-only ops and build up op handles for the async ops...
1147  foreach ( $ops as $index => $params ) {
1148  if ( !in_array( $params['op'], $supportedOps ) ) {
1149  throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
1150  }
1151  $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
1152  $subStatus = $this->$method( [ 'async' => $async ] + $params );
1153  if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1154  if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
1155  $fileOpHandles[] = $curFileOpHandles; // push this batch
1156  $curFileOpHandles = [];
1157  }
1158  $curFileOpHandles[$index] = $subStatus->value; // keep index
1159  } else { // error or completed
1160  $statuses[$index] = $subStatus; // keep index
1161  }
1162  }
1163  if ( count( $curFileOpHandles ) ) {
1164  $fileOpHandles[] = $curFileOpHandles; // last batch
1165  }
1166  // Do all the async ops that can be done concurrently...
1167  foreach ( $fileOpHandles as $fileHandleBatch ) {
1168  $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
1169  }
1170  // Marshall and merge all the responses...
1171  foreach ( $statuses as $index => $subStatus ) {
1172  $status->merge( $subStatus );
1173  if ( $subStatus->isOK() ) {
1174  $status->success[$index] = true;
1175  ++$status->successCount;
1176  } else {
1177  $status->success[$index] = false;
1178  ++$status->failCount;
1179  }
1180  }
1181 
1182  return $status;
1183  }
1184 
1195  final public function executeOpHandlesInternal( array $fileOpHandles ) {
1196  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1197 
1198  foreach ( $fileOpHandles as $fileOpHandle ) {
1199  if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1200  throw new FileBackendError( "Given a non-FileBackendStoreOpHandle object." );
1201  } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1202  throw new FileBackendError( "Given a FileBackendStoreOpHandle for the wrong backend." );
1203  }
1204  }
1205  $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1206  foreach ( $fileOpHandles as $fileOpHandle ) {
1207  $fileOpHandle->closeResources();
1208  }
1209 
1210  return $res;
1211  }
1212 
1221  protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1222  if ( count( $fileOpHandles ) ) {
1223  throw new FileBackendError( "This backend supports no asynchronous operations." );
1224  }
1225 
1226  return [];
1227  }
1228 
1240  protected function sanitizeOpHeaders( array $op ) {
1241  static $longs = [ 'content-disposition' ];
1242 
1243  if ( isset( $op['headers'] ) ) { // op sets HTTP headers
1244  $newHeaders = [];
1245  foreach ( $op['headers'] as $name => $value ) {
1246  $name = strtolower( $name );
1247  $maxHVLen = in_array( $name, $longs ) ? INF : 255;
1248  if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
1249  trigger_error( "Header '$name: $value' is too long." );
1250  } else {
1251  $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
1252  }
1253  }
1254  $op['headers'] = $newHeaders;
1255  }
1256 
1257  return $op;
1258  }
1259 
1260  final public function preloadCache( array $paths ) {
1261  $fullConts = []; // full container names
1262  foreach ( $paths as $path ) {
1263  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1264  $fullConts[] = $fullCont;
1265  }
1266  // Load from the persistent file and container caches
1267  $this->primeContainerCache( $fullConts );
1268  $this->primeFileCache( $paths );
1269  }
1270 
1271  final public function clearCache( array $paths = null ) {
1272  if ( is_array( $paths ) ) {
1273  $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1274  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1275  }
1276  if ( $paths === null ) {
1277  $this->cheapCache->clear();
1278  $this->expensiveCache->clear();
1279  } else {
1280  foreach ( $paths as $path ) {
1281  $this->cheapCache->clear( $path );
1282  $this->expensiveCache->clear( $path );
1283  }
1284  }
1285  $this->doClearCache( $paths );
1286  }
1287 
1295  protected function doClearCache( array $paths = null ) {
1296  }
1297 
1298  final public function preloadFileStat( array $params ) {
1299  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1300  $success = true; // no network errors
1301 
1302  $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
1303  $stats = $this->doGetFileStatMulti( $params );
1304  if ( $stats === null ) {
1305  return true; // not supported
1306  }
1307 
1308  $latest = !empty( $params['latest'] ); // use latest data?
1309  foreach ( $stats as $path => $stat ) {
1311  if ( $path === null ) {
1312  continue; // this shouldn't happen
1313  }
1314  if ( is_array( $stat ) ) { // file exists
1315  // Strongly consistent backends can automatically set "latest"
1316  $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
1317  $this->cheapCache->set( $path, 'stat', $stat );
1318  $this->setFileCache( $path, $stat ); // update persistent cache
1319  if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
1320  $this->cheapCache->set( $path, 'sha1',
1321  [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
1322  }
1323  if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
1324  $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
1325  $this->cheapCache->set( $path, 'xattr',
1326  [ 'map' => $stat['xattr'], 'latest' => $latest ] );
1327  }
1328  } elseif ( $stat === false ) { // file does not exist
1329  $this->cheapCache->set( $path, 'stat',
1330  $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
1331  $this->cheapCache->set( $path, 'xattr',
1332  [ 'map' => false, 'latest' => $latest ] );
1333  $this->cheapCache->set( $path, 'sha1',
1334  [ 'hash' => false, 'latest' => $latest ] );
1335  wfDebug( __METHOD__ . ": File $path does not exist.\n" );
1336  } else { // an error occurred
1337  $success = false;
1338  wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
1339  }
1340  }
1341 
1342  return $success;
1343  }
1344 
1356  protected function doGetFileStatMulti( array $params ) {
1357  return null; // not supported
1358  }
1359 
1367  abstract protected function directoriesAreVirtual();
1368 
1379  final protected static function isValidShortContainerName( $container ) {
1380  // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
1381  // might be used by subclasses. Reserve the dot character for sanity.
1382  // The only way dots end up in containers (e.g. resolveStoragePath)
1383  // is due to the wikiId container prefix or the above suffixes.
1384  return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
1385  }
1386 
1396  final protected static function isValidContainerName( $container ) {
1397  // This accounts for NTFS, Swift, and Ceph restrictions
1398  // and disallows directory separators or traversal characters.
1399  // Note that matching strings URL encode to the same string;
1400  // in Swift/Ceph, the length restriction is *after* URL encoding.
1401  return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1402  }
1403 
1417  final protected function resolveStoragePath( $storagePath ) {
1418  list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
1419  if ( $backend === $this->name ) { // must be for this backend
1420  $relPath = self::normalizeContainerPath( $relPath );
1421  if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
1422  // Get shard for the normalized path if this container is sharded
1423  $cShard = $this->getContainerShard( $shortCont, $relPath );
1424  // Validate and sanitize the relative path (backend-specific)
1425  $relPath = $this->resolveContainerPath( $shortCont, $relPath );
1426  if ( $relPath !== null ) {
1427  // Prepend any wiki ID prefix to the container name
1428  $container = $this->fullContainerName( $shortCont );
1429  if ( self::isValidContainerName( $container ) ) {
1430  // Validate and sanitize the container name (backend-specific)
1431  $container = $this->resolveContainerName( "{$container}{$cShard}" );
1432  if ( $container !== null ) {
1433  return [ $container, $relPath, $cShard ];
1434  }
1435  }
1436  }
1437  }
1438  }
1439 
1440  return [ null, null, null ];
1441  }
1442 
1458  final protected function resolveStoragePathReal( $storagePath ) {
1459  list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
1460  if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
1461  return [ $container, $relPath ];
1462  }
1463 
1464  return [ null, null ];
1465  }
1466 
1475  final protected function getContainerShard( $container, $relPath ) {
1476  list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
1477  if ( $levels == 1 || $levels == 2 ) {
1478  // Hash characters are either base 16 or 36
1479  $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1480  // Get a regex that represents the shard portion of paths.
1481  // The concatenation of the captures gives us the shard.
1482  if ( $levels === 1 ) { // 16 or 36 shards per container
1483  $hashDirRegex = '(' . $char . ')';
1484  } else { // 256 or 1296 shards per container
1485  if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1486  $hashDirRegex = $char . '/(' . $char . '{2})';
1487  } else { // short hash dir format (e.g. "a/b/c")
1488  $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1489  }
1490  }
1491  // Allow certain directories to be above the hash dirs so as
1492  // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1493  // They must be 2+ chars to avoid any hash directory ambiguity.
1494  $m = [];
1495  if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1496  return '.' . implode( '', array_slice( $m, 1 ) );
1497  }
1498 
1499  return null; // failed to match
1500  }
1501 
1502  return ''; // no sharding
1503  }
1504 
1513  final public function isSingleShardPathInternal( $storagePath ) {
1514  list( , , $shard ) = $this->resolveStoragePath( $storagePath );
1515 
1516  return ( $shard !== null );
1517  }
1518 
1527  final protected function getContainerHashLevels( $container ) {
1528  if ( isset( $this->shardViaHashLevels[$container] ) ) {
1529  $config = $this->shardViaHashLevels[$container];
1530  $hashLevels = (int)$config['levels'];
1531  if ( $hashLevels == 1 || $hashLevels == 2 ) {
1532  $hashBase = (int)$config['base'];
1533  if ( $hashBase == 16 || $hashBase == 36 ) {
1534  return [ $hashLevels, $hashBase, $config['repeat'] ];
1535  }
1536  }
1537  }
1538 
1539  return [ 0, 0, false ]; // no sharding
1540  }
1541 
1548  final protected function getContainerSuffixes( $container ) {
1549  $shards = [];
1550  list( $digits, $base ) = $this->getContainerHashLevels( $container );
1551  if ( $digits > 0 ) {
1552  $numShards = pow( $base, $digits );
1553  for ( $index = 0; $index < $numShards; $index++ ) {
1554  $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
1555  }
1556  }
1557 
1558  return $shards;
1559  }
1560 
1567  final protected function fullContainerName( $container ) {
1568  if ( $this->wikiId != '' ) {
1569  return "{$this->wikiId}-$container";
1570  } else {
1571  return $container;
1572  }
1573  }
1574 
1583  protected function resolveContainerName( $container ) {
1584  return $container;
1585  }
1586 
1597  protected function resolveContainerPath( $container, $relStoragePath ) {
1598  return $relStoragePath;
1599  }
1600 
1607  private function containerCacheKey( $container ) {
1608  return "filebackend:{$this->name}:{$this->wikiId}:container:{$container}";
1609  }
1610 
1617  final protected function setContainerCache( $container, array $val ) {
1618  $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
1619  }
1620 
1627  final protected function deleteContainerCache( $container ) {
1628  if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1629  trigger_error( "Unable to delete stat cache for container $container." );
1630  }
1631  }
1632 
1640  final protected function primeContainerCache( array $items ) {
1641  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1642 
1643  $paths = []; // list of storage paths
1644  $contNames = []; // (cache key => resolved container name)
1645  // Get all the paths/containers from the items...
1646  foreach ( $items as $item ) {
1647  if ( self::isStoragePath( $item ) ) {
1648  $paths[] = $item;
1649  } elseif ( is_string( $item ) ) { // full container name
1650  $contNames[$this->containerCacheKey( $item )] = $item;
1651  }
1652  }
1653  // Get all the corresponding cache keys for paths...
1654  foreach ( $paths as $path ) {
1655  list( $fullCont, , ) = $this->resolveStoragePath( $path );
1656  if ( $fullCont !== null ) { // valid path for this backend
1657  $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1658  }
1659  }
1660 
1661  $contInfo = []; // (resolved container name => cache value)
1662  // Get all cache entries for these container cache keys...
1663  $values = $this->memCache->getMulti( array_keys( $contNames ) );
1664  foreach ( $values as $cacheKey => $val ) {
1665  $contInfo[$contNames[$cacheKey]] = $val;
1666  }
1667 
1668  // Populate the container process cache for the backend...
1669  $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1670  }
1671 
1679  protected function doPrimeContainerCache( array $containerInfo ) {
1680  }
1681 
1688  private function fileCacheKey( $path ) {
1689  return "filebackend:{$this->name}:{$this->wikiId}:file:" . sha1( $path );
1690  }
1691 
1700  final protected function setFileCache( $path, array $val ) {
1702  if ( $path === null ) {
1703  return; // invalid storage path
1704  }
1705  $age = time() - wfTimestamp( TS_UNIX, $val['mtime'] );
1706  $ttl = min( 7 * 86400, max( 300, floor( .1 * $age ) ) );
1707  $key = $this->fileCacheKey( $path );
1708  // Set the cache unless it is currently salted.
1709  $this->memCache->set( $key, $val, $ttl );
1710  }
1711 
1720  final protected function deleteFileCache( $path ) {
1722  if ( $path === null ) {
1723  return; // invalid storage path
1724  }
1725  if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
1726  trigger_error( "Unable to delete stat cache for file $path." );
1727  }
1728  }
1729 
1737  final protected function primeFileCache( array $items ) {
1738  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1739 
1740  $paths = []; // list of storage paths
1741  $pathNames = []; // (cache key => storage path)
1742  // Get all the paths/containers from the items...
1743  foreach ( $items as $item ) {
1744  if ( self::isStoragePath( $item ) ) {
1745  $paths[] = FileBackend::normalizeStoragePath( $item );
1746  }
1747  }
1748  // Get rid of any paths that failed normalization...
1749  $paths = array_filter( $paths, 'strlen' ); // remove nulls
1750  // Get all the corresponding cache keys for paths...
1751  foreach ( $paths as $path ) {
1752  list( , $rel, ) = $this->resolveStoragePath( $path );
1753  if ( $rel !== null ) { // valid path for this backend
1754  $pathNames[$this->fileCacheKey( $path )] = $path;
1755  }
1756  }
1757  // Get all cache entries for these file cache keys...
1758  $values = $this->memCache->getMulti( array_keys( $pathNames ) );
1759  foreach ( $values as $cacheKey => $val ) {
1760  $path = $pathNames[$cacheKey];
1761  if ( is_array( $val ) ) {
1762  $val['latest'] = false; // never completely trust cache
1763  $this->cheapCache->set( $path, 'stat', $val );
1764  if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
1765  $this->cheapCache->set( $path, 'sha1',
1766  [ 'hash' => $val['sha1'], 'latest' => false ] );
1767  }
1768  if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
1769  $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
1770  $this->cheapCache->set( $path, 'xattr',
1771  [ 'map' => $val['xattr'], 'latest' => false ] );
1772  }
1773  }
1774  }
1775  }
1776 
1784  final protected static function normalizeXAttributes( array $xattr ) {
1785  $newXAttr = [ 'headers' => [], 'metadata' => [] ];
1786 
1787  foreach ( $xattr['headers'] as $name => $value ) {
1788  $newXAttr['headers'][strtolower( $name )] = $value;
1789  }
1790 
1791  foreach ( $xattr['metadata'] as $name => $value ) {
1792  $newXAttr['metadata'][strtolower( $name )] = $value;
1793  }
1794 
1795  return $newXAttr;
1796  }
1797 
1804  final protected function setConcurrencyFlags( array $opts ) {
1805  $opts['concurrency'] = 1; // off
1806  if ( $this->parallelize === 'implicit' ) {
1807  if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
1808  $opts['concurrency'] = $this->concurrency;
1809  }
1810  } elseif ( $this->parallelize === 'explicit' ) {
1811  if ( !empty( $opts['parallelize'] ) ) {
1812  $opts['concurrency'] = $this->concurrency;
1813  }
1814  }
1815 
1816  return $opts;
1817  }
1818 
1827  protected function getContentType( $storagePath, $content, $fsPath ) {
1828  if ( $this->mimeCallback ) {
1829  return call_user_func_array( $this->mimeCallback, func_get_args() );
1830  }
1831 
1832  $mime = null;
1833  if ( $fsPath !== null && function_exists( 'finfo_file' ) ) {
1834  $finfo = finfo_open( FILEINFO_MIME_TYPE );
1835  $mime = finfo_file( $finfo, $fsPath );
1836  finfo_close( $finfo );
1837  }
1838 
1839  return is_string( $mime ) ? $mime : 'unknown/unknown';
1840  }
1841 }
1842 
1853  public $params = []; // params to caller functions
1855  public $backend;
1857  public $resourcesToClose = [];
1858 
1859  public $call; // string; name that identifies the function called
1860 
1864  public function closeResources() {
1865  array_map( 'fclose', $this->resourcesToClose );
1866  }
1867 }
1868 
1875 abstract class FileBackendStoreShardListIterator extends FilterIterator {
1877  protected $backend;
1878 
1880  protected $params;
1881 
1883  protected $container;
1884 
1886  protected $directory;
1887 
1889  protected $multiShardPaths = []; // (rel path => 1)
1890 
1898  public function __construct(
1900  ) {
1901  $this->backend = $backend;
1902  $this->container = $container;
1903  $this->directory = $dir;
1904  $this->params = $params;
1905 
1906  $iter = new AppendIterator();
1907  foreach ( $suffixes as $suffix ) {
1908  $iter->append( $this->listFromShard( $this->container . $suffix ) );
1909  }
1910 
1911  parent::__construct( $iter );
1912  }
1913 
1914  public function accept() {
1915  $rel = $this->getInnerIterator()->current(); // path relative to given directory
1916  $path = $this->params['dir'] . "/{$rel}"; // full storage path
1917  if ( $this->backend->isSingleShardPathInternal( $path ) ) {
1918  return true; // path is only on one shard; no issue with duplicates
1919  } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
1920  // Don't keep listing paths that are on multiple shards
1921  return false;
1922  } else {
1923  $this->multiShardPaths[$rel] = 1;
1924 
1925  return true;
1926  }
1927  }
1928 
1929  public function rewind() {
1930  parent::rewind();
1931  $this->multiShardPaths = [];
1932  }
1933 
1940  abstract protected function listFromShard( $container );
1941 }
1942 
1947  protected function listFromShard( $container ) {
1948  $list = $this->backend->getDirectoryListInternal(
1949  $container, $this->directory, $this->params );
1950  if ( $list === null ) {
1951  return new ArrayIterator( [] );
1952  } else {
1953  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1954  }
1955  }
1956 }
1957 
1962  protected function listFromShard( $container ) {
1963  $list = $this->backend->getFileListInternal(
1964  $container, $this->directory, $this->params );
1965  if ( $list === null ) {
1966  return new ArrayIterator( [] );
1967  } else {
1968  return is_array( $list ) ? new ArrayIterator( $list ) : $list;
1969  }
1970  }
1971 }
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
doPublishInternal($container, $dir, array $params)
getFileHttpUrl(array $params)
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)
setConcurrencyFlags(array $opts)
Set the 'concurrency' option from a list of operation options.
the array() calling protocol came about after MediaWiki 1.4rc1.
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
getScopedLocksForOps(array $ops, Status $status)
if(count($args)==0) $dir
$success
setFileCache($path, array $val)
Set the cached stat info for a file path.
Iterator for listing regular files.
moveInternal(array $params)
Move a file from one storage path to another in the backend.
concatenate(array $params)
doPrepareInternal($container, $dir, array $params)
doGetLocalReferenceMulti(array $params)
resolveContainerName($container)
Resolve a container name, checking if it's allowed by the backend.
getPathsToLockForOpsInternal(array $performOps)
Get a list of storage paths to lock for a list of operations Returns an array with LockManager::LOCK_...
static instance()
Singleton.
Definition: Profiler.php:60
static normalizeXAttributes(array $xattr)
Normalize file headers/metadata to the FileBackend::getFileXAttributes() format.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
listFromShard($container)
Get the list for a given container shard.
doGetFileXAttributes(array $params)
getFileStat(array $params)
getContainerSuffixes($container)
Get a list of full container shard suffixes for a container.
$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
__construct(array $config)
if($ext== 'php'||$ext== 'php5') $mime
Definition: router.php:65
getName()
Get the unique backend name.
string $directory
Resolved relative path.
string $container
Full container name.
const READY_STREAM
Definition: StreamFile.php:27
ProcessCacheLRU $expensiveCache
Map of paths to large (RAM/disk) cache items.
fileCacheKey($path)
Get the cache key for a file path.
FileBackendStore helper function to handle listings that span container shards.
static isValidContainerName($container)
Check if a full container name is valid.
int $concurrency
How many operations can be done in parallel.
Definition: FileBackend.php:99
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
storeInternal(array $params)
Store a file into the backend from a file on disk.
array $shardViaHashLevels
Map of container names to sharding config.
doSecureInternal($container, $dir, array $params)
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
doCreateInternal(array $params)
static newEmpty()
Get an instance that wraps EmptyBagOStuff.
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
static prepareForStream($path, $info, $headers=[], $sendErrors=true)
Call this function used in preparation before streaming a file.
Definition: StreamFile.php:76
WANObjectCache $memCache
streamFile(array $params)
getFileSha1Base36(array $params)
const LOCK_UW
Definition: LockManager.php:61
doClean(array $params)
executeOpHandlesInternal(array $fileOpHandles)
Execute a list of FileBackendStoreOpHandle handles in parallel.
getFileSize(array $params)
doQuickOperationsInternal(array $ops)
File backend exception for checked exceptions (e.g.
isPathUsableInternal($storagePath)
Check if a file can be created or changed at a given storage path.
ProcessCacheLRU $cheapCache
Map of paths to small (RAM/disk) cache items.
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:62
$res
Definition: database.txt:21
doStreamFile(array $params)
getFileXAttributes(array $params)
nullInternal(array $params)
No-op file operation that does nothing.
static encode($value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:127
deleteInternal(array $params)
Delete a file at the storage path.
preloadCache(array $paths)
$params
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't exist.
Definition: FSFile.php:179
getOperationsInternal(array $ops)
Return a list of FileOp objects from a list of operations.
clearCache(array $paths=null)
callable $mimeCallback
Method to get the MIME type of files.
resolveStoragePathReal($storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
doPrepare(array $params)
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.
getContentType($storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
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:87
doGetFileContentsMulti(array $params)
Base class for all backends using particular storage medium.
preloadFileStat(array $params)
fullContainerName($container)
Get the full container name, including the wiki ID prefix.
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
getFileList(array $params)
closeResources()
Close all open file handles.
__construct(FileBackendStore $backend, $container, $dir, array $suffixes, array $params)
containerCacheKey($container)
Get the cache key for a container.
Iterator for listing directories.
getContainerHashLevels($container)
Get the sharding config for a container.
static normalizeStoragePath($storagePath)
Normalize a storage path by cleaning up directory separators.
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:57
resolveContainerPath($container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
setContainerCache($container, array $val)
Set the cached info for a container.
doConcatenate(array $params)
getContainerShard($container, $relPath)
Get the container name shard suffix for a given path.
getDirectoryListInternal($container, $dir, array $params)
Do not call this function from places outside FileBackend.
doPublish(array $params)
doPrimeContainerCache(array $containerInfo)
Fill the backend-specific process cache given an array of resolved container names and their correspo...
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition: hooks.txt:1004
doClearCache(array $paths=null)
Clears any additional stat caches for storage paths.
describeInternal(array $params)
Alter metadata for a file at the storage path.
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at name
Definition: design.txt:12
doStoreInternal(array $params)
isSingleShardPathInternal($storagePath)
Check if a storage path maps to a single shard.
Base class for all file backend classes (including multi-write backends).
Definition: FileBackend.php:85
doDeleteInternal(array $params)
getTopDirectoryList(array $params)
Same as FileBackend::getDirectoryList() except only lists directories that are immediately under the ...
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1004
deleteFileCache($path)
Delete the cached stat info for a file path.
const NOT_MODIFIED
Definition: StreamFile.php:28
const TS_UNIX
Unix time - the number of seconds since 1970-01-01 00:00:00 UTC.
doMoveInternal(array $params)
sanitizeOpHeaders(array $op)
Normalize and filter HTTP headers from a file operation.
doDescribeInternal(array $params)
static isValidShortContainerName($container)
Check if a short container name is valid.
doCleanInternal($container, $dir, array $params)
getLocalReferenceMulti(array $params)
FileBackendStore helper class for performing asynchronous file operations.
doGetFileSha1Base36(array $params)
deleteContainerCache($container)
Delete the cached info for a container.
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...
getFileListInternal($container, $dir, array $params)
Do not call this function from places outside FileBackend.
doCopyInternal(array $params)
getLocalReference(array $params)
Returns a file system file, identical to the file at a storage path.
getDirectoryList(array $params)
Handles per process caching of items.
getLocalCopyMulti(array $params)
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
getFileProps(array $params)
doDirectoryExists($container, $dir, array $params)
getScopedFileLocks(array $paths, $type, Status $status, $timeout=0)
Lock the files at the given storage paths in the backend.
resolveStoragePath($storagePath)
Splits a storage path into an internal container name, an internal relative file name, and a container shard suffix.
doExecuteOpHandlesInternal(array $fileOpHandles)