MediaWiki master
FileBackendStore.php
Go to the documentation of this file.
1<?php
10namespace Wikimedia\FileBackend;
11
12use InvalidArgumentException;
13use Shellbox\Command\BoxedCommand;
14use StatusValue;
15use Traversable;
33use Wikimedia\Timestamp\ConvertibleTimestamp;
34use Wikimedia\Timestamp\TimestampFormat as TS;
35
50abstract class FileBackendStore extends FileBackend {
59
63 protected $memCache;
64
66 protected $shardViaHashLevels = [];
67
69 protected $mimeCallback;
70
72 protected $maxFileSize = 32 * 1024 * 1024 * 1024;
73
74 protected const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
75 protected const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
76 protected const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
77
79 protected const RES_ABSENT = false;
81 protected const RES_ERROR = null;
82
84 protected const ABSENT_NORMAL = 'FNE-N';
86 protected const ABSENT_LATEST = 'FNE-L';
87
101 public function __construct( array $config ) {
102 parent::__construct( $config );
103 $this->mimeCallback = $config['mimeCallback'] ?? null;
104 $this->srvCache = $config['srvCache'] ?? new EmptyBagOStuff();
105 $this->wanCache = $config['wanCache'] ?? WANObjectCache::newEmpty();
106 $this->wanStatCache = WANObjectCache::newEmpty(); // disabled by default
107 $this->memCache =& $this->wanStatCache; // compatability alias
108 $this->cheapCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE );
109 $this->expensiveCache = new MapCacheLRU( self::CACHE_EXPENSIVE_SIZE );
110 }
111
119 final public function maxFileSizeInternal() {
120 return min( $this->maxFileSize, PHP_INT_MAX );
121 }
122
133 abstract public function isPathUsableInternal( $storagePath );
134
153 final public function createInternal( array $params ) {
155 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
156
157 if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
158 $status = $this->newStatus( 'backend-fail-maxsize',
159 $params['dst'], $this->maxFileSizeInternal() );
160 } else {
161 $status = $this->doCreateInternal( $params );
162 $this->clearCache( [ $params['dst'] ] );
163 if ( $params['dstExists'] ?? true ) {
164 $this->deleteFileCache( $params['dst'] ); // persistent cache
165 }
166 }
167
168 return $status;
169 }
170
176 abstract protected function doCreateInternal( array $params );
177
196 final public function storeInternal( array $params ) {
198 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
199
200 if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
201 $status = $this->newStatus( 'backend-fail-maxsize',
202 $params['dst'], $this->maxFileSizeInternal() );
203 } else {
204 $status = $this->doStoreInternal( $params );
205 $this->clearCache( [ $params['dst'] ] );
206 if ( $params['dstExists'] ?? true ) {
207 $this->deleteFileCache( $params['dst'] ); // persistent cache
208 }
209 }
210
211 return $status;
212 }
213
219 abstract protected function doStoreInternal( array $params );
220
240 final public function copyInternal( array $params ) {
242 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
243
244 $status = $this->doCopyInternal( $params );
245 $this->clearCache( [ $params['dst'] ] );
246 if ( $params['dstExists'] ?? true ) {
247 $this->deleteFileCache( $params['dst'] ); // persistent cache
248 }
249
250 return $status;
251 }
252
258 abstract protected function doCopyInternal( array $params );
259
274 final public function deleteInternal( array $params ) {
276 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
277
278 $status = $this->doDeleteInternal( $params );
279 $this->clearCache( [ $params['src'] ] );
280 $this->deleteFileCache( $params['src'] ); // persistent cache
281 return $status;
282 }
283
289 abstract protected function doDeleteInternal( array $params );
290
310 final public function moveInternal( array $params ) {
312 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
313
314 $status = $this->doMoveInternal( $params );
315 $this->clearCache( [ $params['src'], $params['dst'] ] );
316 $this->deleteFileCache( $params['src'] ); // persistent cache
317 if ( $params['dstExists'] ?? true ) {
318 $this->deleteFileCache( $params['dst'] ); // persistent cache
319 }
320
321 return $status;
322 }
323
329 abstract protected function doMoveInternal( array $params );
330
345 final public function describeInternal( array $params ) {
347 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
348
349 if ( count( $params['headers'] ) ) {
350 $status = $this->doDescribeInternal( $params );
351 $this->clearCache( [ $params['src'] ] );
352 $this->deleteFileCache( $params['src'] ); // persistent cache
353 } else {
354 $status = $this->newStatus(); // nothing to do
355 }
356
357 return $status;
358 }
359
366 protected function doDescribeInternal( array $params ) {
367 return $this->newStatus();
368 }
369
377 final public function nullInternal( array $params ) {
378 return $this->newStatus();
379 }
380
382 final public function concatenate( array $params ) {
384 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
385 $status = $this->newStatus();
386
387 // Try to lock the source files for the scope of this function
389 $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
390 if ( $status->isOK() ) {
391 // Actually do the file concatenation...
392 $hrStart = hrtime( true );
393 $status->merge( $this->doConcatenate( $params ) );
394 $sec = ( hrtime( true ) - $hrStart ) / 1e9;
395 if ( !$status->isOK() ) {
396 $this->logger->error( static::class . "-{$this->name}" .
397 " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
398 }
399 }
400
401 return $status;
402 }
403
410 protected function doConcatenate( array $params ) {
411 $status = $this->newStatus();
412 $tmpPath = $params['dst'];
413 unset( $params['latest'] );
414
415 // Check that the specified temp file is valid...
416 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
417 $ok = ( @is_file( $tmpPath ) && @filesize( $tmpPath ) == 0 );
418 if ( !$ok ) { // not present or not empty
419 $status->fatal( 'backend-fail-opentemp', $tmpPath );
420
421 return $status;
422 }
423
424 // Get local FS versions of the chunks needed for the concatenation...
425 $fsFiles = $this->getLocalReferenceMulti( $params );
426 foreach ( $fsFiles as $path => &$fsFile ) {
427 if ( !$fsFile ) { // chunk failed to download?
428 $fsFile = $this->getLocalReference( [ 'src' => $path ] );
429 if ( !$fsFile ) { // retry failed?
430 $status->fatal(
431 $fsFile === self::RES_ERROR ? 'backend-fail-read' : 'backend-fail-notexists',
432 $path
433 );
434
435 return $status;
436 }
437 }
438 }
439 unset( $fsFile ); // unset reference so we can reuse $fsFile
440
441 // Get a handle for the destination temp file
442 $tmpHandle = fopen( $tmpPath, 'ab' );
443 if ( $tmpHandle === false ) {
444 $status->fatal( 'backend-fail-opentemp', $tmpPath );
445
446 return $status;
447 }
448
449 // Build up the temp file using the source chunks (in order)...
450 foreach ( $fsFiles as $virtualSource => $fsFile ) {
451 // Get a handle to the local FS version
452 $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
453 if ( $sourceHandle === false ) {
454 fclose( $tmpHandle );
455 $status->fatal( 'backend-fail-read', $virtualSource );
456
457 return $status;
458 }
459 // Append chunk to file (pass chunk size to avoid magic quotes)
460 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
461 fclose( $sourceHandle );
462 fclose( $tmpHandle );
463 $status->fatal( 'backend-fail-writetemp', $tmpPath );
464
465 return $status;
466 }
467 fclose( $sourceHandle );
468 }
469 if ( !fclose( $tmpHandle ) ) {
470 $status->fatal( 'backend-fail-closetemp', $tmpPath );
471
472 return $status;
473 }
474
475 clearstatcache(); // temp file changed
476
477 return $status;
478 }
479
483 final protected function doPrepare( array $params ) {
485 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
486 $status = $this->newStatus();
487
488 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
489 if ( $dir === null ) {
490 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
491
492 return $status; // invalid storage path
493 }
494
495 if ( $shard !== null ) { // confined to a single container/shard
496 $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
497 } else { // directory is on several shards
498 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
499 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
500 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
501 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
502 }
503 }
504
505 return $status;
506 }
507
516 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
517 return $this->newStatus();
518 }
519
521 final protected function doSecure( array $params ) {
523 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
524 $status = $this->newStatus();
525
526 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
527 if ( $dir === null ) {
528 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
529
530 return $status; // invalid storage path
531 }
532
533 if ( $shard !== null ) { // confined to a single container/shard
534 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
535 } else { // directory is on several shards
536 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
537 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
538 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
539 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
540 }
541 }
542
543 return $status;
544 }
545
554 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
555 return $this->newStatus();
556 }
557
559 final protected function doPublish( array $params ) {
561 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
562 $status = $this->newStatus();
563
564 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
565 if ( $dir === null ) {
566 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
567
568 return $status; // invalid storage path
569 }
570
571 if ( $shard !== null ) { // confined to a single container/shard
572 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
573 } else { // directory is on several shards
574 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
575 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
576 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
577 $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
578 }
579 }
580
581 return $status;
582 }
583
592 protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
593 return $this->newStatus();
594 }
595
597 final protected function doClean( array $params ) {
599 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
600 $status = $this->newStatus();
601
602 // Recursive: first delete all empty subdirs recursively
603 if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
604 $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
605 if ( $subDirsRel !== null ) { // no errors
606 foreach ( $subDirsRel as $subDirRel ) {
607 $subDir = $params['dir'] . "/{$subDirRel}"; // full path
608 $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
609 }
610 unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
611 }
612 }
613
614 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
615 if ( $dir === null ) {
616 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
617
618 return $status; // invalid storage path
619 }
620
621 // Attempt to lock this directory...
622 $filesLockEx = [ $params['dir'] ];
624 $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
625 if ( !$status->isOK() ) {
626 return $status; // abort
627 }
628
629 if ( $shard !== null ) { // confined to a single container/shard
630 $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
631 $this->deleteContainerCache( $fullCont ); // purge cache
632 } else { // directory is on several shards
633 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
634 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
635 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
636 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
637 $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
638 }
639 }
640
641 return $status;
642 }
643
652 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
653 return $this->newStatus();
654 }
655
657 final public function fileExists( array $params ) {
659 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
660
661 $stat = $this->getFileStat( $params );
662 if ( is_array( $stat ) ) {
663 return true;
664 }
665
666 return $stat === self::RES_ABSENT ? false : self::EXISTENCE_ERROR;
667 }
668
670 final public function getFileTimestamp( array $params ) {
672 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
673
674 $stat = $this->getFileStat( $params );
675 if ( is_array( $stat ) ) {
676 return $stat['mtime'];
677 }
678
679 return self::TIMESTAMP_FAIL; // all failure cases
680 }
681
683 final public function getFileSize( array $params ) {
685 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
686
687 $stat = $this->getFileStat( $params );
688 if ( is_array( $stat ) ) {
689 return $stat['size'];
690 }
691
692 return self::SIZE_FAIL; // all failure cases
693 }
694
696 final public function getFileStat( array $params ) {
698 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
699
700 $path = self::normalizeStoragePath( $params['src'] );
701 if ( $path === null ) {
702 return self::STAT_ERROR; // invalid storage path
703 }
704
705 // Whether to bypass cache except for process cache entries loaded directly from
706 // high consistency backend queries (caller handles any cache flushing and locking)
707 $latest = !empty( $params['latest'] );
708 // Whether to ignore cache entries missing the SHA-1 field for existing files
709 $requireSHA1 = !empty( $params['requireSHA1'] );
710
711 $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
712 // Load the persistent stat cache into process cache if needed
713 if ( !$latest ) {
714 if (
715 // File stat is not in process cache
716 $stat === null ||
717 // Key/value store backends might opportunistically set file stat process
718 // cache entries from object listings that do not include the SHA-1. In that
719 // case, loading the persistent stat cache will likely yield the SHA-1.
720 ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) )
721 ) {
722 $this->primeFileCache( [ $path ] );
723 // Get any newly process-cached entry
724 $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
725 }
726 }
727
728 if ( is_array( $stat ) ) {
729 if (
730 ( !$latest || !empty( $stat['latest'] ) ) &&
731 ( !$requireSHA1 || isset( $stat['sha1'] ) )
732 ) {
733 return $stat;
734 }
735 } elseif ( $stat === self::ABSENT_LATEST ) {
736 return self::STAT_ABSENT;
737 } elseif ( $stat === self::ABSENT_NORMAL ) {
738 if ( !$latest ) {
739 return self::STAT_ABSENT;
740 }
741 }
742
743 // Load the file stat from the backend and update caches
744 $stat = $this->doGetFileStat( $params );
745 $this->ingestFreshFileStats( [ $path => $stat ], $latest );
746
747 if ( is_array( $stat ) ) {
748 return $stat;
749 }
750
751 return $stat === self::RES_ERROR ? self::STAT_ERROR : self::STAT_ABSENT;
752 }
753
761 final protected function ingestFreshFileStats( array $stats, $latest ) {
762 $success = true;
763
764 foreach ( $stats as $path => $stat ) {
765 if ( is_array( $stat ) ) {
766 // Strongly consistent backends might automatically set this flag
767 $stat['latest'] ??= $latest;
768
769 $this->cheapCache->setField( $path, 'stat', $stat );
770 if ( isset( $stat['sha1'] ) ) {
771 // Some backends store the SHA-1 hash as metadata
772 $this->cheapCache->setField(
773 $path,
774 'sha1',
775 [ 'hash' => $stat['sha1'], 'latest' => $latest ]
776 );
777 }
778 if ( isset( $stat['xattr'] ) ) {
779 // Some backends store custom headers/metadata
780 $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
781 $this->cheapCache->setField(
782 $path,
783 'xattr',
784 [ 'map' => $stat['xattr'], 'latest' => $latest ]
785 );
786 }
787 // Update persistent cache (@TODO: set all entries in one batch)
788 $this->setFileCache( $path, $stat );
789 } elseif ( $stat === self::RES_ABSENT ) {
790 $this->cheapCache->setField(
791 $path,
792 'stat',
793 $latest ? self::ABSENT_LATEST : self::ABSENT_NORMAL
794 );
795 $this->cheapCache->setField(
796 $path,
797 'xattr',
798 [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
799 );
800 $this->cheapCache->setField(
801 $path,
802 'sha1',
803 [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
804 );
805 $this->logger->debug(
806 __METHOD__ . ': File {path} does not exist',
807 [ 'path' => $path ]
808 );
809 } else {
810 $success = false;
811 $this->logger->error(
812 __METHOD__ . ': Could not stat file {path}',
813 [ 'path' => $path ]
814 );
815 }
816 }
817
818 return $success;
819 }
820
826 abstract protected function doGetFileStat( array $params );
827
829 public function getFileContentsMulti( array $params ) {
831 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
832
833 $params = $this->setConcurrencyFlags( $params );
834 $contents = $this->doGetFileContentsMulti( $params );
835 foreach ( $contents as $path => $content ) {
836 if ( !is_string( $content ) ) {
837 $contents[$path] = self::CONTENT_FAIL; // used for all failure cases
838 }
839 }
840
841 return $contents;
842 }
843
850 protected function doGetFileContentsMulti( array $params ) {
851 $contents = [];
852 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
853 if ( $fsFile instanceof FSFile ) {
854 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
855 $content = @file_get_contents( $fsFile->getPath() );
856 $contents[$path] = is_string( $content ) ? $content : self::RES_ERROR;
857 } else {
858 // self::RES_ERROR or self::RES_ABSENT
859 $contents[$path] = $fsFile;
860 }
861 }
862
863 return $contents;
864 }
865
867 final public function getFileXAttributes( array $params ) {
869 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
870
871 $path = self::normalizeStoragePath( $params['src'] );
872 if ( $path === null ) {
873 return self::XATTRS_FAIL; // invalid storage path
874 }
875 $latest = !empty( $params['latest'] ); // use latest data?
876 if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
877 $stat = $this->cheapCache->getField( $path, 'xattr' );
878 // If we want the latest data, check that this cached
879 // value was in fact fetched with the latest available data.
880 if ( !$latest || $stat['latest'] ) {
881 return $stat['map'];
882 }
883 }
884 $fields = $this->doGetFileXAttributes( $params );
885 if ( is_array( $fields ) ) {
886 $fields = self::normalizeXAttributes( $fields );
887 $this->cheapCache->setField(
888 $path,
889 'xattr',
890 [ 'map' => $fields, 'latest' => $latest ]
891 );
892 } elseif ( $fields === self::RES_ABSENT ) {
893 $this->cheapCache->setField(
894 $path,
895 'xattr',
896 [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
897 );
898 } else {
899 $fields = self::XATTRS_FAIL; // used for all failure cases
900 }
901
902 return $fields;
903 }
904
911 protected function doGetFileXAttributes( array $params ) {
912 return [ 'headers' => [], 'metadata' => [] ]; // not supported
913 }
914
916 final public function getFileSha1Base36( array $params ) {
918 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
919
920 $path = self::normalizeStoragePath( $params['src'] );
921 if ( $path === null ) {
922 return self::SHA1_FAIL; // invalid storage path
923 }
924 $latest = !empty( $params['latest'] ); // use latest data?
925 if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
926 $stat = $this->cheapCache->getField( $path, 'sha1' );
927 // If we want the latest data, check that this cached
928 // value was in fact fetched with the latest available data.
929 if ( !$latest || $stat['latest'] ) {
930 return $stat['hash'];
931 }
932 }
933 $sha1 = $this->doGetFileSha1Base36( $params );
934 if ( is_string( $sha1 ) ) {
935 $this->cheapCache->setField(
936 $path,
937 'sha1',
938 [ 'hash' => $sha1, 'latest' => $latest ]
939 );
940 } elseif ( $sha1 === self::RES_ABSENT ) {
941 $this->cheapCache->setField(
942 $path,
943 'sha1',
944 [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
945 );
946 } else {
947 $sha1 = self::SHA1_FAIL; // used for all failure cases
948 }
949
950 return $sha1;
951 }
952
959 protected function doGetFileSha1Base36( array $params ) {
960 $fsFile = $this->getLocalReference( $params );
961 if ( $fsFile instanceof FSFile ) {
962 $sha1 = $fsFile->getSha1Base36();
963
964 return is_string( $sha1 ) ? $sha1 : self::RES_ERROR;
965 }
966
967 return $fsFile === self::RES_ERROR ? self::RES_ERROR : self::RES_ABSENT;
968 }
969
971 final public function getFileProps( array $params ) {
973 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
974
975 $fsFile = $this->getLocalReference( $params );
976
977 return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
978 }
979
981 final public function getLocalReferenceMulti( array $params ) {
983 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
984
985 $params = $this->setConcurrencyFlags( $params );
986
987 $fsFiles = []; // (path => FSFile)
988 $latest = !empty( $params['latest'] ); // use latest data?
989 // Reuse any files already in process cache...
990 foreach ( $params['srcs'] as $src ) {
992 if ( $path === null ) {
993 $fsFiles[$src] = self::RES_ERROR; // invalid storage path
994 } elseif ( $this->expensiveCache->hasField( $path, 'localRef' ) ) {
995 $val = $this->expensiveCache->getField( $path, 'localRef' );
996 // If we want the latest data, check that this cached
997 // value was in fact fetched with the latest available data.
998 if ( !$latest || $val['latest'] ) {
999 $fsFiles[$src] = $val['object'];
1000 }
1001 }
1002 }
1003 // Fetch local references of any remaining files...
1004 $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
1005 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
1006 $fsFiles[$path] = $fsFile;
1007 if ( $fsFile instanceof FSFile ) {
1008 $this->expensiveCache->setField(
1009 $path,
1010 'localRef',
1011 [ 'object' => $fsFile, 'latest' => $latest ]
1012 );
1013 }
1014 }
1015
1016 return $fsFiles;
1017 }
1018
1025 protected function doGetLocalReferenceMulti( array $params ) {
1026 return $this->doGetLocalCopyMulti( $params );
1027 }
1028
1030 final public function getLocalCopyMulti( array $params ) {
1032 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1033
1034 $params = $this->setConcurrencyFlags( $params );
1035
1036 return $this->doGetLocalCopyMulti( $params );
1037 }
1038
1044 abstract protected function doGetLocalCopyMulti( array $params );
1045
1052 public function getFileHttpUrl( array $params ) {
1053 return self::TEMPURL_ERROR; // not supported
1054 }
1055
1057 public function addShellboxInputFile( BoxedCommand $command, string $boxedName,
1058 array $params
1059 ) {
1060 $ref = $this->getLocalReference( [ 'src' => $params['src'] ] );
1061 if ( $ref === false ) {
1062 return $this->newStatus( 'backend-fail-notexists', $params['src'] );
1063 } elseif ( $ref === null ) {
1064 return $this->newStatus( 'backend-fail-read', $params['src'] );
1065 } else {
1066 $file = $command->newInputFileFromFile( $ref->getPath() )
1067 ->userData( __CLASS__, $ref );
1068 $command->inputFile( $boxedName, $file );
1069 return $this->newStatus();
1070 }
1071 }
1072
1074 final public function streamFile( array $params ) {
1076 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1077 $status = $this->newStatus();
1078
1079 // Always set some fields for subclass convenience
1080 $params['options'] ??= [];
1081 $params['headers'] ??= [];
1082
1083 // Don't stream it out as text/html if there was a PHP error
1084 if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
1085 print "Headers already sent, terminating.\n";
1086 $status->fatal( 'backend-fail-stream', $params['src'] );
1087 return $status;
1088 }
1089
1090 $status->merge( $this->doStreamFile( $params ) );
1091
1092 return $status;
1093 }
1094
1101 protected function doStreamFile( array $params ) {
1102 $status = $this->newStatus();
1103
1104 $flags = 0;
1105 $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0;
1106 $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0;
1107
1108 $fsFile = $this->getLocalReference( $params );
1109 if ( $fsFile ) {
1110 $streamer = new HTTPFileStreamer(
1111 $fsFile->getPath(),
1112 $this->getStreamerOptions()
1113 );
1114 $res = $streamer->stream( $params['headers'], true, $params['options'], $flags );
1115 } else {
1116 $res = false;
1117 HTTPFileStreamer::send404Message( $params['src'], $flags );
1118 }
1119
1120 if ( !$res ) {
1121 $status->fatal( 'backend-fail-stream', $params['src'] );
1122 }
1123
1124 return $status;
1125 }
1126
1128 final public function directoryExists( array $params ) {
1129 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1130 if ( $dir === null ) {
1131 return self::EXISTENCE_ERROR; // invalid storage path
1132 }
1133 if ( $shard !== null ) { // confined to a single container/shard
1134 return $this->doDirectoryExists( $fullCont, $dir, $params );
1135 } else { // directory is on several shards
1136 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1137 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1138 $res = false; // response
1139 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
1140 $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
1141 if ( $exists === true ) {
1142 $res = true;
1143 break; // found one!
1144 } elseif ( $exists === self::RES_ERROR ) {
1145 $res = self::EXISTENCE_ERROR;
1146 }
1147 }
1148
1149 return $res;
1150 }
1151 }
1152
1161 abstract protected function doDirectoryExists( $fullCont, $dirRel, array $params );
1162
1164 final public function getDirectoryList( array $params ) {
1165 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1166 if ( $dir === null ) {
1167 return self::EXISTENCE_ERROR; // invalid storage path
1168 }
1169 if ( $shard !== null ) {
1170 // File listing is confined to a single container/shard
1171 return $this->getDirectoryListInternal( $fullCont, $dir, $params );
1172 } else {
1173 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1174 // File listing spans multiple containers/shards
1175 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1176
1177 return new FileBackendStoreShardDirIterator( $this,
1178 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
1179 }
1180 }
1181
1192 abstract public function getDirectoryListInternal( $fullCont, $dirRel, array $params );
1193
1195 final public function getFileList( array $params ) {
1196 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1197 if ( $dir === null ) {
1198 return self::LIST_ERROR; // invalid storage path
1199 }
1200 if ( $shard !== null ) {
1201 // File listing is confined to a single container/shard
1202 return $this->getFileListInternal( $fullCont, $dir, $params );
1203 } else {
1204 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1205 // File listing spans multiple containers/shards
1206 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1207
1208 return new FileBackendStoreShardFileIterator( $this,
1209 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
1210 }
1211 }
1212
1223 abstract public function getFileListInternal( $fullCont, $dirRel, array $params );
1224
1236 final public function getOperationsInternal( array $ops ) {
1237 $supportedOps = [
1238 'store' => StoreFileOp::class,
1239 'copy' => CopyFileOp::class,
1240 'move' => MoveFileOp::class,
1241 'delete' => DeleteFileOp::class,
1242 'create' => CreateFileOp::class,
1243 'describe' => DescribeFileOp::class,
1244 'null' => NullFileOp::class
1245 ];
1246
1247 $performOps = []; // array of FileOp objects
1248 // Build up ordered array of FileOps...
1249 foreach ( $ops as $operation ) {
1250 $opName = $operation['op'];
1251 if ( isset( $supportedOps[$opName] ) ) {
1252 $class = $supportedOps[$opName];
1253 // Get params for this operation
1254 $params = $operation;
1255 // Append the FileOp class
1256 $performOps[] = new $class( $this, $params, $this->logger );
1257 } else {
1258 throw new FileBackendError( "Operation '$opName' is not supported." );
1259 }
1260 }
1261
1262 return $performOps;
1263 }
1264
1275 final public function getPathsToLockForOpsInternal( array $performOps ) {
1276 // Build up a list of files to lock...
1277 $paths = [ 'sh' => [], 'ex' => [] ];
1278 foreach ( $performOps as $fileOp ) {
1279 $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1280 $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1281 }
1282 // Optimization: if doing an EX lock anyway, don't also set an SH one
1283 $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1284 // Get a shared lock on the parent directory of each path changed
1285 $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1286
1287 return [
1288 LockManager::LOCK_UW => $paths['sh'],
1289 LockManager::LOCK_EX => $paths['ex']
1290 ];
1291 }
1292
1294 public function getScopedLocksForOps( array $ops, StatusValue $status ) {
1295 $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1296
1297 return $this->getScopedFileLocks( $paths, 'mixed', $status );
1298 }
1299
1301 final protected function doOperationsInternal( array $ops, array $opts ) {
1303 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1304 $status = $this->newStatus();
1305
1306 // Fix up custom header name/value pairs
1307 $ops = array_map( $this->sanitizeOpHeaders( ... ), $ops );
1308 // Build up a list of FileOps and involved paths
1309 $fileOps = $this->getOperationsInternal( $ops );
1310 $pathsUsed = [];
1311 foreach ( $fileOps as $fileOp ) {
1312 $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() );
1313 }
1314
1315 // Acquire any locks as needed for the scope of this function
1316 if ( empty( $opts['nonLocking'] ) ) {
1317 $pathsByLockType = $this->getPathsToLockForOpsInternal( $fileOps );
1319 $scopeLock = $this->getScopedFileLocks( $pathsByLockType, 'mixed', $status );
1320 if ( !$status->isOK() ) {
1321 return $status; // abort
1322 }
1323 }
1324
1325 // Clear any file cache entries (after locks acquired)
1326 if ( empty( $opts['preserveCache'] ) ) {
1327 $this->clearCache( $pathsUsed );
1328 }
1329
1330 // Enlarge the cache to fit the stat entries of these files
1331 $this->cheapCache->setMaxSize( max( 2 * count( $pathsUsed ), self::CACHE_CHEAP_SIZE ) );
1332
1333 // Load from the persistent container caches
1334 $this->primeContainerCache( $pathsUsed );
1335 // Get the latest stat info for all the files (having locked them)
1336 $ok = $this->preloadFileStat( [ 'srcs' => $pathsUsed, 'latest' => true ] );
1337
1338 if ( $ok ) {
1339 // Actually attempt the operation batch...
1340 $opts = $this->setConcurrencyFlags( $opts );
1341 $subStatus = FileOpBatch::attempt( $fileOps, $opts );
1342 } else {
1343 // If we could not even stat some files, then bail out
1344 $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
1345 foreach ( $ops as $i => $op ) { // mark each op as failed
1346 $subStatus->success[$i] = false;
1347 ++$subStatus->failCount;
1348 }
1349 $this->logger->error( static::class . "-{$this->name} stat failure",
1350 [ 'aborted_operations' => $ops ]
1351 );
1352 }
1353
1354 // Merge errors into StatusValue fields
1355 $status->merge( $subStatus );
1356 $status->success = $subStatus->success; // not done in merge()
1357
1358 // Shrink the stat cache back to normal size
1359 $this->cheapCache->setMaxSize( self::CACHE_CHEAP_SIZE );
1360
1361 return $status;
1362 }
1363
1365 final protected function doQuickOperationsInternal( array $ops, array $opts ) {
1367 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1368 $status = $this->newStatus();
1369
1370 // Fix up custom header name/value pairs
1371 $ops = array_map( $this->sanitizeOpHeaders( ... ), $ops );
1372 // Build up a list of FileOps and involved paths
1373 $fileOps = $this->getOperationsInternal( $ops );
1374 $pathsUsed = [];
1375 foreach ( $fileOps as $fileOp ) {
1376 $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() );
1377 }
1378
1379 // Clear any file cache entries for involved paths
1380 $this->clearCache( $pathsUsed );
1381
1382 // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
1383 $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
1384 $maxConcurrency = $this->concurrency; // throttle
1386 $statuses = []; // array of (index => StatusValue)
1388 $batch = [];
1389 foreach ( $fileOps as $index => $fileOp ) {
1390 $subStatus = $async
1391 ? $fileOp->attemptAsyncQuick()
1392 : $fileOp->attemptQuick();
1393 if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1394 if ( count( $batch ) >= $maxConcurrency ) {
1395 // Execute this batch. Don't queue any more ops since they contain
1396 // open filehandles which are a limited resource (T230245).
1397 $statuses += $this->executeOpHandlesInternal( $batch );
1398 $batch = [];
1399 }
1400 $batch[$index] = $subStatus->value; // keep index
1401 } else { // error or completed
1402 $statuses[$index] = $subStatus; // keep index
1403 }
1404 }
1405 if ( count( $batch ) ) {
1406 $statuses += $this->executeOpHandlesInternal( $batch );
1407 }
1408 // Marshall and merge all the responses...
1409 foreach ( $statuses as $index => $subStatus ) {
1410 $status->merge( $subStatus );
1411 if ( $subStatus->isOK() ) {
1412 $status->success[$index] = true;
1413 ++$status->successCount;
1414 } else {
1415 $status->success[$index] = false;
1416 ++$status->failCount;
1417 }
1418 }
1419
1420 $this->clearCache( $pathsUsed );
1421
1422 return $status;
1423 }
1424
1434 final public function executeOpHandlesInternal( array $fileOpHandles ) {
1436 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1437
1438 foreach ( $fileOpHandles as $fileOpHandle ) {
1439 if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1440 throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." );
1441 } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1442 throw new InvalidArgumentException( "Expected handle for this file backend." );
1443 }
1444 }
1445
1446 $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1447 foreach ( $fileOpHandles as $fileOpHandle ) {
1448 $fileOpHandle->closeResources();
1449 }
1450
1451 return $statuses;
1452 }
1453
1463 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1464 if ( count( $fileOpHandles ) ) {
1465 throw new FileBackendError( "Backend does not support asynchronous operations." );
1466 }
1467
1468 return [];
1469 }
1470
1482 protected function sanitizeOpHeaders( array $op ) {
1483 static $longs = [ 'content-disposition' ];
1484
1485 if ( isset( $op['headers'] ) ) { // op sets HTTP headers
1486 $newHeaders = [];
1487 foreach ( $op['headers'] as $name => $value ) {
1488 $name = strtolower( $name );
1489 $maxHVLen = in_array( $name, $longs ) ? INF : 255;
1490 if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
1491 $this->logger->error( "Header '{header}' is too long.", [
1492 'filebackend' => $this->name,
1493 'header' => "$name: $value",
1494 ] );
1495 } else {
1496 $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
1497 }
1498 }
1499 $op['headers'] = $newHeaders;
1500 }
1501
1502 return $op;
1503 }
1504
1505 final public function preloadCache( array $paths ) {
1506 $fullConts = []; // full container names
1507 foreach ( $paths as $path ) {
1508 [ $fullCont, , ] = $this->resolveStoragePath( $path );
1509 $fullConts[] = $fullCont;
1510 }
1511 // Load from the persistent file and container caches
1512 $this->primeContainerCache( $fullConts );
1513 $this->primeFileCache( $paths );
1514 }
1515
1516 final public function clearCache( ?array $paths = null ) {
1517 if ( is_array( $paths ) ) {
1518 $paths = array_map( FileBackend::normalizeStoragePath( ... ), $paths );
1519 $paths = array_filter( $paths, 'strlen' ); // remove nulls
1520 }
1521 if ( $paths === null ) {
1522 $this->cheapCache->clear();
1523 $this->expensiveCache->clear();
1524 } else {
1525 foreach ( $paths as $path ) {
1526 $this->cheapCache->clear( $path );
1527 $this->expensiveCache->clear( $path );
1528 }
1529 }
1530 $this->doClearCache( $paths );
1531 }
1532
1541 protected function doClearCache( ?array $paths = null ) {
1542 }
1543
1545 final public function preloadFileStat( array $params ) {
1547 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1548
1549 $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
1550 $stats = $this->doGetFileStatMulti( $params );
1551 if ( $stats === null ) {
1552 return true; // not supported
1553 }
1554
1555 // Whether this queried the backend in high consistency mode
1556 $latest = !empty( $params['latest'] );
1557
1558 return $this->ingestFreshFileStats( $stats, $latest );
1559 }
1560
1574 protected function doGetFileStatMulti( array $params ) {
1575 return null; // not supported
1576 }
1577
1585 abstract protected function directoriesAreVirtual();
1586
1597 final protected static function isValidShortContainerName( $container ) {
1598 // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
1599 // might be used by subclasses. Reserve the dot character.
1600 // The only way dots end up in containers (e.g. resolveStoragePath)
1601 // is due to the wikiId container prefix or the above suffixes.
1602 return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
1603 }
1604
1614 final protected static function isValidContainerName( $container ) {
1615 // This accounts for NTFS, Swift, and Ceph restrictions
1616 // and disallows directory separators or traversal characters.
1617 // Note that matching strings URL encode to the same string;
1618 // in Swift/Ceph, the length restriction is *after* URL encoding.
1619 return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1620 }
1621
1635 final protected function resolveStoragePath( $storagePath ) {
1636 [ $backend, $shortCont, $relPath ] = self::splitStoragePath( $storagePath );
1637 if ( $backend === $this->name && $relPath !== null ) { // must be for this backend
1638 $relPath = self::normalizeContainerPath( $relPath );
1639 if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
1640 // Get shard for the normalized path if this container is sharded
1641 $cShard = $this->getContainerShard( $shortCont, $relPath );
1642 // Validate and sanitize the relative path (backend-specific)
1643 $relPath = $this->resolveContainerPath( $shortCont, $relPath );
1644 if ( $relPath !== null ) {
1645 // Prepend any domain ID prefix to the container name
1646 $container = $this->fullContainerName( $shortCont );
1647 if ( self::isValidContainerName( $container ) ) {
1648 // Validate and sanitize the container name (backend-specific)
1649 $container = $this->resolveContainerName( "{$container}{$cShard}" );
1650 if ( $container !== null ) {
1651 return [ $container, $relPath, $cShard ];
1652 }
1653 }
1654 }
1655 }
1656 }
1657
1658 return [ null, null, null ];
1659 }
1660
1676 final protected function resolveStoragePathReal( $storagePath ) {
1677 [ $container, $relPath, $cShard ] = $this->resolveStoragePath( $storagePath );
1678 if ( $cShard !== null && !str_ends_with( $relPath, '/' ) ) {
1679 return [ $container, $relPath ];
1680 }
1681
1682 return [ null, null ];
1683 }
1684
1693 final protected function getContainerShard( $container, $relPath ) {
1694 [ $levels, $base, $repeat ] = $this->getContainerHashLevels( $container );
1695 if ( $levels == 1 || $levels == 2 ) {
1696 // Hash characters are either base 16 or 36
1697 $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1698 // Get a regex that represents the shard portion of paths.
1699 // The concatenation of the captures gives us the shard.
1700 if ( $levels === 1 ) { // 16 or 36 shards per container
1701 $hashDirRegex = '(' . $char . ')';
1702 } else { // 256 or 1296 shards per container
1703 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1704 $hashDirRegex = $char . '/(' . $char . '{2})';
1705 } else { // short hash dir format (e.g. "a/b/c")
1706 $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1707 }
1708 }
1709 // Allow certain directories to be above the hash dirs so as
1710 // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1711 // They must be 2+ chars to avoid any hash directory ambiguity.
1712 $m = [];
1713 if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1714 return '.' . implode( '', array_slice( $m, 1 ) );
1715 }
1716
1717 return null; // failed to match
1718 }
1719
1720 return ''; // no sharding
1721 }
1722
1731 final public function isSingleShardPathInternal( $storagePath ) {
1732 [ , , $shard ] = $this->resolveStoragePath( $storagePath );
1733
1734 return ( $shard !== null );
1735 }
1736
1745 final protected function getContainerHashLevels( $container ) {
1746 if ( isset( $this->shardViaHashLevels[$container] ) ) {
1747 $config = $this->shardViaHashLevels[$container];
1748 $hashLevels = (int)$config['levels'];
1749 if ( $hashLevels == 1 || $hashLevels == 2 ) {
1750 $hashBase = (int)$config['base'];
1751 if ( $hashBase == 16 || $hashBase == 36 ) {
1752 return [ $hashLevels, $hashBase, $config['repeat'] ];
1753 }
1754 }
1755 }
1756
1757 return [ 0, 0, false ]; // no sharding
1758 }
1759
1766 final protected function getContainerSuffixes( $container ) {
1767 $shards = [];
1768 [ $digits, $base ] = $this->getContainerHashLevels( $container );
1769 if ( $digits > 0 ) {
1770 $numShards = $base ** $digits;
1771 for ( $index = 0; $index < $numShards; $index++ ) {
1772 $shards[] = '.' . \Wikimedia\base_convert( (string)$index, 10, $base, $digits );
1773 }
1774 }
1775
1776 return $shards;
1777 }
1778
1785 final protected function fullContainerName( $container ) {
1786 if ( $this->domainId != '' ) {
1787 return "{$this->domainId}-$container";
1788 } else {
1789 return $container;
1790 }
1791 }
1792
1802 protected function resolveContainerName( $container ) {
1803 return $container;
1804 }
1805
1817 protected function resolveContainerPath( $container, $relStoragePath ) {
1818 return $relStoragePath;
1819 }
1820
1827 private function containerCacheKey( $container ) {
1828 return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
1829 }
1830
1837 final protected function setContainerCache( $container, array $val ) {
1838 if ( !$this->wanStatCache->set(
1839 $this->containerCacheKey( $container ),
1840 $val,
1841 14 * 86400
1842 ) ) {
1843 $this->logger->warning( "Unable to set stat cache for container {container}.",
1844 [ 'filebackend' => $this->name, 'container' => $container ]
1845 );
1846 }
1847 }
1848
1855 final protected function deleteContainerCache( $container ) {
1856 if ( !$this->wanStatCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1857 $this->logger->warning( "Unable to delete stat cache for container {container}.",
1858 [ 'filebackend' => $this->name, 'container' => $container ]
1859 );
1860 }
1861 }
1862
1868 final protected function primeContainerCache( array $items ) {
1870 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1871
1872 $paths = []; // list of storage paths
1873 $contNames = []; // (cache key => resolved container name)
1874 // Get all the paths/containers from the items...
1875 foreach ( $items as $item ) {
1876 if ( self::isStoragePath( $item ) ) {
1877 $paths[] = $item;
1878 } elseif ( is_string( $item ) ) { // full container name
1879 $contNames[$this->containerCacheKey( $item )] = $item;
1880 }
1881 }
1882 // Get all the corresponding cache keys for paths...
1883 foreach ( $paths as $path ) {
1884 [ $fullCont, , ] = $this->resolveStoragePath( $path );
1885 if ( $fullCont !== null ) { // valid path for this backend
1886 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1887 }
1888 }
1889
1890 $contInfo = []; // (resolved container name => cache value)
1891 // Get all cache entries for these container cache keys...
1892 $values = $this->wanStatCache->getMulti( array_keys( $contNames ) );
1893 foreach ( $values as $cacheKey => $val ) {
1894 $contInfo[$contNames[$cacheKey]] = $val;
1895 }
1896
1897 // Populate the container process cache for the backend...
1898 $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1899 }
1900
1909 protected function doPrimeContainerCache( array $containerInfo ) {
1910 }
1911
1918 private function fileCacheKey( $path ) {
1919 return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path );
1920 }
1921
1930 final protected function setFileCache( $path, array $val ) {
1932 if ( $path === null ) {
1933 return; // invalid storage path
1934 }
1935 $mtime = (int)ConvertibleTimestamp::convert( TS::UNIX, $val['mtime'] );
1936 $ttl = $this->wanStatCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 );
1937 // Set the cache unless it is currently salted.
1938 if ( !$this->wanStatCache->set( $this->fileCacheKey( $path ), $val, $ttl ) ) {
1939 $this->logger->warning( "Unable to set stat cache for file {path}.",
1940 [ 'filebackend' => $this->name, 'path' => $path ]
1941 );
1942 }
1943 }
1944
1953 final protected function deleteFileCache( $path ) {
1955 if ( $path === null ) {
1956 return; // invalid storage path
1957 }
1958 if ( !$this->wanStatCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
1959 $this->logger->warning( "Unable to delete stat cache for file {path}.",
1960 [ 'filebackend' => $this->name, 'path' => $path ]
1961 );
1962 }
1963 }
1964
1972 final protected function primeFileCache( array $items ) {
1974 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1975
1976 $paths = []; // list of storage paths
1977 $pathNames = []; // (cache key => storage path)
1978 // Get all the paths/containers from the items...
1979 foreach ( $items as $item ) {
1980 if ( self::isStoragePath( $item ) ) {
1982 if ( $path !== null ) {
1983 $paths[] = $path;
1984 }
1985 }
1986 }
1987 // Get all the corresponding cache keys for paths...
1988 foreach ( $paths as $path ) {
1989 [ , $rel, ] = $this->resolveStoragePath( $path );
1990 if ( $rel !== null ) { // valid path for this backend
1991 $pathNames[$this->fileCacheKey( $path )] = $path;
1992 }
1993 }
1994 // Get all cache entries for these file cache keys.
1995 // Note that negatives are not cached by getFileStat()/preloadFileStat().
1996 $values = $this->wanStatCache->getMulti( array_keys( $pathNames ) );
1997 // Load all of the results into process cache...
1998 foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) {
1999 $path = $pathNames[$cacheKey];
2000 // This flag only applies to stat info loaded directly
2001 // from a high consistency backend query to the process cache
2002 unset( $stat['latest'] );
2003
2004 $this->cheapCache->setField( $path, 'stat', $stat );
2005 if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) {
2006 // Some backends store SHA-1 as metadata
2007 $this->cheapCache->setField(
2008 $path,
2009 'sha1',
2010 [ 'hash' => $stat['sha1'], 'latest' => false ]
2011 );
2012 }
2013 if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) {
2014 // Some backends store custom headers/metadata
2015 $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
2016 $this->cheapCache->setField(
2017 $path,
2018 'xattr',
2019 [ 'map' => $stat['xattr'], 'latest' => false ]
2020 );
2021 }
2022 }
2023 }
2024
2032 final protected static function normalizeXAttributes( array $xattr ) {
2033 $newXAttr = [ 'headers' => [], 'metadata' => [] ];
2034
2035 foreach ( $xattr['headers'] as $name => $value ) {
2036 $newXAttr['headers'][strtolower( $name )] = $value;
2037 }
2038
2039 foreach ( $xattr['metadata'] as $name => $value ) {
2040 $newXAttr['metadata'][strtolower( $name )] = $value;
2041 }
2042
2043 return $newXAttr;
2044 }
2045
2052 final protected function setConcurrencyFlags( array $opts ) {
2053 $opts['concurrency'] = 1; // off
2054 if ( $this->parallelize === 'implicit' ) {
2055 if ( $opts['parallelize'] ?? true ) {
2056 $opts['concurrency'] = $this->concurrency;
2057 }
2058 } elseif ( $this->parallelize === 'explicit' ) {
2059 if ( !empty( $opts['parallelize'] ) ) {
2060 $opts['concurrency'] = $this->concurrency;
2061 }
2062 }
2063
2064 return $opts;
2065 }
2066
2076 protected function getContentType( $storagePath, $content, $fsPath ) {
2077 if ( $this->mimeCallback ) {
2078 return ( $this->mimeCallback )( $storagePath, $content, $fsPath );
2079 }
2080
2081 $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false;
2082 return $mime ?: 'unknown/unknown';
2083 }
2084}
2085
2087class_alias( FileBackendStore::class, 'FileBackendStore' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Class representing a non-directory file on the file system.
Definition FSFile.php:20
File backend exception for checked exceptions (e.g.
Base class for all backends using particular storage medium.
getContainerHashLevels( $container)
Get the sharding config for a container.
createInternal(array $params)
Create a file in the backend with the given contents.
static isValidContainerName( $container)
Check if a full container name is valid.
doCleanInternal( $fullCont, $dirRel, array $params)
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
preloadCache(array $paths)
Preload persistent file stat cache and property cache into in-process cache.
getLocalCopyMulti(array $params)
Like getLocalCopy() except it takes an array of storage paths and yields an order preserved-map of st...
getFileXAttributes(array $params)
Get metadata about a file at a storage path in the backend.If the file does not exist,...
getFileList(array $params)
Get an iterator to list all stored files under a storage directory.If the directory is of the form "m...
getContentType( $storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
doOperationsInternal(array $ops, array $opts)
FileBackend::doOperations() StatusValue
ingestFreshFileStats(array $stats, $latest)
Ingest file stat entries that just came from querying the backend (not cache)
moveInternal(array $params)
Move a file from one storage path to another in the backend.
getContainerSuffixes( $container)
Get a list of full container shard suffixes for a container.
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
getPathsToLockForOpsInternal(array $performOps)
Get a list of storage paths to lock for a list of operations Returns an array with LockManager::LOCK_...
getContainerShard( $container, $relPath)
Get the container name shard suffix for a given path.
doSecureInternal( $fullCont, $dirRel, array $params)
doSecure(array $params)
FileBackend::secure() StatusValue
executeOpHandlesInternal(array $fileOpHandles)
Execute a list of FileBackendStoreOpHandle handles in parallel.
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...
setConcurrencyFlags(array $opts)
Set the 'concurrency' option from a list of operation options.
getScopedLocksForOps(array $ops, StatusValue $status)
Get an array of scoped locks needed for a batch of file operations.Normally, FileBackend::doOperation...
concatenate(array $params)
Concatenate a list of storage files into a single file system file.The target path should refer to a ...
describeInternal(array $params)
Alter metadata for a file at the storage path.
MapCacheLRU $cheapCache
In-memory map of paths to small (RAM/disk) cache items.
static normalizeXAttributes(array $xattr)
Normalize file headers/metadata to the FileBackend::getFileXAttributes() format.
MapCacheLRU $expensiveCache
In-memory map of paths to large (RAM/disk) cache items.
directoryExists(array $params)
Check if a directory exists at a given storage path.For backends using key/value stores,...
getFileContentsMulti(array $params)
Like getFileContents() except it takes an array of storage paths and returns an order preserved map o...
doQuickOperationsInternal(array $ops, array $opts)
FileBackend::doQuickOperations() StatusValue 1.20
setFileCache( $path, array $val)
Set the cached stat info for a file path.
doPrimeContainerCache(array $containerInfo)
Fill the backend-specific process cache given an array of resolved container names and their correspo...
static isValidShortContainerName( $container)
Check if a short container name is valid.
isSingleShardPathInternal( $storagePath)
Check if a storage path maps to a single shard.
doDirectoryExists( $fullCont, $dirRel, array $params)
storeInternal(array $params)
Store a file into the backend from a file on disk.
deleteInternal(array $params)
Delete a file at the storage path.
doGetFileStatMulti(array $params)
Get file stat information (concurrently if possible) for several files.
getFileProps(array $params)
Get the properties of the content of the file at a storage path in the backend.This gives the result ...
setContainerCache( $container, array $val)
Set the cached info for a container.
WANObjectCache $wanStatCache
Cache used for persistent file/container stat entries.
maxFileSizeInternal()
Get the maximum allowable file size given backend medium restrictions and basic performance constrain...
doPrepareInternal( $fullCont, $dirRel, array $params)
getDirectoryListInternal( $fullCont, $dirRel, array $params)
Do not call this function from places outside FileBackend.
doClearCache(?array $paths=null)
Clears any additional stat caches for storage paths.
WANObjectCache $wanCache
Persistent cache accessible to all relevant datacenters.
int $maxFileSize
Size in bytes, defaults to 32 GiB.
doPrepare(array $params)
FileBackend::prepare() StatusValue Good status without value for success, fatal otherwise.
getFileStat(array $params)
Get quick information about a file at a storage path in the backend.If the file does not exist,...
fullContainerName( $container)
Get the full container name, including the domain ID prefix.
fileExists(array $params)
Check if a file exists at a storage path in the backend.This returns false if only a directory exists...
callable null $mimeCallback
Method to get the MIME type of files.
deleteContainerCache( $container)
Delete the cached info for a container.
array< string, array > $shardViaHashLevels
Map of container names to sharding config.
streamFile(array $params)
Stream the content of the file at a storage path in the backend.If the file does not exists,...
clearCache(?array $paths=null)
Invalidate any in-process file stat and property cache.
getFileTimestamp(array $params)
Get the last-modified timestamp of the file at a storage path.FileBackend::TIMESTAMP_FAILstring|false...
deleteFileCache( $path)
Delete the cached stat info for a file path.
resolveContainerName( $container)
Resolve a container name, checking if it's allowed by the backend.
doClean(array $params)
FileBackend::clean() StatusValue
copyInternal(array $params)
Copy a file from one storage path to another in the backend.
resolveStoragePath( $storagePath)
Splits a storage path into an internal container name, an internal relative file name,...
getLocalReferenceMulti(array $params)
Like getLocalReference() except it takes an array of storage paths and yields an order-preserved map ...
getDirectoryList(array $params)
Get an iterator to list all directories under a storage directory.If the directory is of the form "mw...
preloadFileStat(array $params)
Preload file stat information (concurrently if possible) into in-process cache.This should be used wh...
sanitizeOpHeaders(array $op)
Normalize and filter HTTP headers from a file operation.
getFileSize(array $params)
Get the size (bytes) of a file at a storage path in the backend.FileBackend::SIZE_FAILint|false File ...
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path in the backend.
nullInternal(array $params)
No-op file operation that does nothing.
doPublish(array $params)
FileBackend::publish() StatusValue
getFileListInternal( $fullCont, $dirRel, array $params)
Do not call this function from places outside FileBackend.
getOperationsInternal(array $ops)
Return a list of FileOp objects from a list of operations.
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
BagOStuff $srvCache
Persistent local server/host cache (e.g.
getFileSha1Base36(array $params)
Get a SHA-1 hash of the content of the file at a storage path in the backend.FileBackend::SHA1_FAILst...
doPublishInternal( $fullCont, $dirRel, array $params)
addShellboxInputFile(BoxedCommand $command, string $boxedName, array $params)
Add a file to a Shellbox command as an input file.StatusValue 1.43
Base class for all file backend classes (including multi-write backends).
string $name
Unique backend name.
static normalizeContainerPath( $path)
Validate and normalize a relative storage path.
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
getLocalReference(array $params)
Returns a file system file, identical in content to the file at a storage path.
getTopDirectoryList(array $params)
Same as FileBackend::getDirectoryList() except only lists directories that are immediately under the ...
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
newStatus( $message=null,... $params)
Yields the result of the status wrapper callback on either:
int $concurrency
How many operations can be done in parallel.
static attempt(array $performOps, array $opts)
Attempt to perform a series of file operations.
FileBackendStore helper class for performing asynchronous file operations.
Copy a file from one storage path to another in the backend.
Create a file in the backend with the given content.
Delete a file at the given storage path from the backend.
Change metadata for a file at the given storage path in the backend.
FileBackend helper class for representing operations.
Definition FileOp.php:32
Move a file from one storage path to another in the backend.
Placeholder operation that has no params and does nothing.
Store a file into the backend from a file on the file system.
Functions related to the output of file content.
static send404Message( $fname, $flags=0)
Send out a standard 404 message for a file.
Resource locking handling.
Store key-value entries in a size-limited in-memory LRU cache.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
No-op implementation that stores nothing.
Multi-datacenter aware caching interface.