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 {
63
67 protected $memCache;
68
70 protected $shardViaHashLevels = [];
71
73 protected $mimeCallback;
74
76 protected $maxFileSize = 32 * 1024 * 1024 * 1024;
77
78 protected const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
79 protected const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
80 protected const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
81
83 protected const RES_ABSENT = false;
85 protected const RES_ERROR = null;
86
88 protected const ABSENT_NORMAL = 'FNE-N';
90 protected const ABSENT_LATEST = 'FNE-L';
91
105 public function __construct( array $config ) {
106 parent::__construct( $config );
107 $this->mimeCallback = $config['mimeCallback'] ?? null;
108 $this->srvCache = $config['srvCache'] ?? new EmptyBagOStuff();
109 $this->wanCache = $config['wanCache'] ?? WANObjectCache::newEmpty();
110 $this->wanStatCache = WANObjectCache::newEmpty(); // disabled by default
111 $this->memCache =& $this->wanStatCache; // compatibility alias
112 $this->procFileStatCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE );
113 $this->cheapCache =& $this->procFileStatCache; // compatability alias
114 $this->procFileDataCache = new MapCacheLRU( self::CACHE_EXPENSIVE_SIZE );
115 $this->expensiveCache =& $this->procFileDataCache; // compatability alias
116 }
117
125 final public function maxFileSizeInternal() {
126 return min( $this->maxFileSize, PHP_INT_MAX );
127 }
128
140 abstract public function isPathUsableInternal( $storagePath );
141
160 final public function createInternal( array $params ) {
161 if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
162 $status = $this->newStatus( 'backend-fail-maxsize',
163 $params['dst'], $this->maxFileSizeInternal() );
164 } else {
165 $status = $this->doCreateInternal( $params );
166 $this->clearCache( [ $params['dst'] ] );
167 if ( $params['dstExists'] ?? true ) {
168 $this->deleteFileCache( $params['dst'] ); // persistent cache
169 }
170 }
171
172 return $status;
173 }
174
180 abstract protected function doCreateInternal( array $params );
181
200 final public function storeInternal( array $params ) {
201 if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
202 $status = $this->newStatus( 'backend-fail-maxsize',
203 $params['dst'], $this->maxFileSizeInternal() );
204 } else {
205 $status = $this->doStoreInternal( $params );
206 $this->clearCache( [ $params['dst'] ] );
207 if ( $params['dstExists'] ?? true ) {
208 $this->deleteFileCache( $params['dst'] ); // persistent cache
209 }
210 }
211
212 return $status;
213 }
214
220 abstract protected function doStoreInternal( array $params );
221
241 final public function copyInternal( array $params ) {
242 $status = $this->doCopyInternal( $params );
243 $this->clearCache( [ $params['dst'] ] );
244 if ( $params['dstExists'] ?? true ) {
245 $this->deleteFileCache( $params['dst'] ); // persistent cache
246 }
247
248 return $status;
249 }
250
256 abstract protected function doCopyInternal( array $params );
257
272 final public function deleteInternal( array $params ) {
273 $status = $this->doDeleteInternal( $params );
274 $this->clearCache( [ $params['src'] ] );
275 $this->deleteFileCache( $params['src'] ); // persistent cache
276 return $status;
277 }
278
284 abstract protected function doDeleteInternal( array $params );
285
305 final public function moveInternal( array $params ) {
306 $status = $this->doMoveInternal( $params );
307 $this->clearCache( [ $params['src'], $params['dst'] ] );
308 $this->deleteFileCache( $params['src'] ); // persistent cache
309 if ( $params['dstExists'] ?? true ) {
310 $this->deleteFileCache( $params['dst'] ); // persistent cache
311 }
312
313 return $status;
314 }
315
321 abstract protected function doMoveInternal( array $params );
322
337 final public function describeInternal( array $params ) {
338 if ( count( $params['headers'] ) ) {
339 $status = $this->doDescribeInternal( $params );
340 $this->clearCache( [ $params['src'] ] );
341 $this->deleteFileCache( $params['src'] ); // persistent cache
342 } else {
343 $status = $this->newStatus(); // nothing to do
344 }
345
346 return $status;
347 }
348
355 protected function doDescribeInternal( array $params ) {
356 return $this->newStatus();
357 }
358
366 final public function nullInternal( array $params ) {
367 return $this->newStatus();
368 }
369
371 final public function concatenate( array $params ) {
372 $status = $this->newStatus();
373 // Try to lock the source files for the scope of this function
375 $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
376 if ( $status->isOK() ) {
377 // Actually do the file concatenation...
378 $hrStart = hrtime( true );
379 $status->merge( $this->doConcatenate( $params ) );
380 $sec = ( hrtime( true ) - $hrStart ) / 1e9;
381 if ( !$status->isOK() ) {
382 $this->logger->error( static::class . "-{$this->name}" .
383 " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
384 }
385 }
386
387 return $status;
388 }
389
396 protected function doConcatenate( array $params ) {
397 $status = $this->newStatus();
398 $tmpPath = $params['dst'];
399 unset( $params['latest'] );
400
401 // Check that the specified temp file is valid...
402 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
403 $ok = ( @is_file( $tmpPath ) && @filesize( $tmpPath ) == 0 );
404 if ( !$ok ) { // not present or not empty
405 $status->fatal( 'backend-fail-opentemp', $tmpPath );
406
407 return $status;
408 }
409
410 // Get local FS versions of the chunks needed for the concatenation...
411 $fsFiles = $this->getLocalReferenceMulti( $params );
412 foreach ( $fsFiles as $path => &$fsFile ) {
413 if ( !$fsFile ) { // chunk failed to download?
414 $fsFile = $this->getLocalReference( [ 'src' => $path ] );
415 if ( !$fsFile ) { // retry failed?
416 $status->fatal(
417 $fsFile === self::RES_ERROR ? 'backend-fail-read' : 'backend-fail-notexists',
418 $path
419 );
420
421 return $status;
422 }
423 }
424 }
425 unset( $fsFile ); // unset reference so we can reuse $fsFile
426
427 // Get a handle for the destination temp file
428 $tmpHandle = fopen( $tmpPath, 'ab' );
429 if ( $tmpHandle === false ) {
430 $status->fatal( 'backend-fail-opentemp', $tmpPath );
431
432 return $status;
433 }
434
435 // Build up the temp file using the source chunks (in order)...
436 foreach ( $fsFiles as $virtualSource => $fsFile ) {
437 // Get a handle to the local FS version
438 $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
439 if ( $sourceHandle === false ) {
440 fclose( $tmpHandle );
441 $status->fatal( 'backend-fail-read', $virtualSource );
442
443 return $status;
444 }
445 // Append chunk to file (pass chunk size to avoid magic quotes)
446 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
447 fclose( $sourceHandle );
448 fclose( $tmpHandle );
449 $status->fatal( 'backend-fail-writetemp', $tmpPath );
450
451 return $status;
452 }
453 fclose( $sourceHandle );
454 }
455 if ( !fclose( $tmpHandle ) ) {
456 $status->fatal( 'backend-fail-closetemp', $tmpPath );
457
458 return $status;
459 }
460
461 clearstatcache(); // temp file changed
462
463 return $status;
464 }
465
469 final protected function doPrepare( array $params ) {
470 $status = $this->newStatus();
471
472 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
473 if ( $dir === null ) {
474 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
475
476 return $status; // invalid storage path
477 }
478
479 if ( $shard !== null ) { // confined to a single container/shard
480 $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
481 } else { // directory is on several shards
482 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
483 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
484 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
485 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
486 }
487 }
488
489 return $status;
490 }
491
500 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
501 return $this->newStatus();
502 }
503
505 final protected function doSecure( array $params ) {
506 $status = $this->newStatus();
507
508 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
509 if ( $dir === null ) {
510 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
511
512 return $status; // invalid storage path
513 }
514
515 if ( $shard !== null ) { // confined to a single container/shard
516 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
517 } else { // directory is on several shards
518 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
519 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
520 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
521 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
522 }
523 }
524
525 return $status;
526 }
527
536 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
537 return $this->newStatus();
538 }
539
541 final protected function doPublish( array $params ) {
542 $status = $this->newStatus();
543
544 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
545 if ( $dir === null ) {
546 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
547
548 return $status; // invalid storage path
549 }
550
551 if ( $shard !== null ) { // confined to a single container/shard
552 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
553 } else { // directory is on several shards
554 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
555 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
556 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
557 $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
558 }
559 }
560
561 return $status;
562 }
563
572 protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
573 return $this->newStatus();
574 }
575
577 final protected function doClean( array $params ) {
578 $status = $this->newStatus();
579
580 // Recursive: first delete all empty subdirs recursively
581 if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
582 $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
583 if ( $subDirsRel !== null ) { // no errors
584 foreach ( $subDirsRel as $subDirRel ) {
585 $subDir = $params['dir'] . "/{$subDirRel}"; // full path
586 $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
587 }
588 unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
589 }
590 }
591
592 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
593 if ( $dir === null ) {
594 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
595
596 return $status; // invalid storage path
597 }
598
599 // Attempt to lock this directory...
600 $filesLockEx = [ $params['dir'] ];
602 $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
603 if ( !$status->isOK() ) {
604 return $status; // abort
605 }
606
607 if ( $shard !== null ) { // confined to a single container/shard
608 $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
609 $this->deleteContainerCache( $fullCont ); // purge cache
610 } else { // directory is on several shards
611 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
612 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
613 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
614 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
615 $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
616 }
617 }
618
619 return $status;
620 }
621
630 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
631 return $this->newStatus();
632 }
633
635 final public function fileExists( array $params ) {
636 $stat = $this->getFileStat( $params );
637 if ( is_array( $stat ) ) {
638 return true;
639 }
640
641 return $stat === self::RES_ABSENT ? false : self::EXISTENCE_ERROR;
642 }
643
645 final public function getFileTimestamp( array $params ) {
646 $stat = $this->getFileStat( $params );
647 if ( is_array( $stat ) ) {
648 return $stat['mtime'];
649 }
650
651 return self::TIMESTAMP_FAIL; // all failure cases
652 }
653
655 final public function getFileSize( array $params ) {
656 $stat = $this->getFileStat( $params );
657 if ( is_array( $stat ) ) {
658 return $stat['size'];
659 }
660
661 return self::SIZE_FAIL; // all failure cases
662 }
663
665 final public function getFileStat( array $params ) {
666 $path = self::normalizeStoragePath( $params['src'] );
667 if ( $path === null ) {
668 return self::STAT_ERROR; // invalid storage path
669 }
670
671 // Whether to bypass cache except for process cache entries loaded directly from
672 // high consistency backend queries (caller handles any cache flushing and locking)
673 $latest = !empty( $params['latest'] );
674 // Whether to ignore cache entries missing the SHA-1 field for existing files
675 $requireSHA1 = !empty( $params['requireSHA1'] );
676
677 $stat = $this->procFileStatCache->getField( $path, 'stat', self::CACHE_TTL );
678 // Load the persistent stat cache into process cache if needed
679 if ( !$latest ) {
680 if (
681 // File stat is not in process cache
682 $stat === null ||
683 // Key/value store backends might opportunistically set file stat process
684 // cache entries from object listings that do not include the SHA-1. In that
685 // case, loading the persistent stat cache will likely yield the SHA-1.
686 ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) )
687 ) {
688 $this->primeFileCache( [ $path ] );
689 // Get any newly process-cached entry
690 $stat = $this->procFileStatCache->getField( $path, 'stat', self::CACHE_TTL );
691 }
692 }
693
694 if ( is_array( $stat ) ) {
695 if (
696 ( !$latest || !empty( $stat['latest'] ) ) &&
697 ( !$requireSHA1 || isset( $stat['sha1'] ) )
698 ) {
699 return $stat;
700 }
701 } elseif ( $stat === self::ABSENT_LATEST ) {
702 return self::STAT_ABSENT;
703 } elseif ( $stat === self::ABSENT_NORMAL ) {
704 if ( !$latest ) {
705 return self::STAT_ABSENT;
706 }
707 }
708
709 // Load the file stat from the backend and update caches
710 $stat = $this->doGetFileStat( $params );
711 $this->ingestFreshFileStats( [ $path => $stat ], $latest );
712
713 if ( is_array( $stat ) ) {
714 return $stat;
715 }
716
717 return $stat === self::RES_ERROR ? self::STAT_ERROR : self::STAT_ABSENT;
718 }
719
727 final protected function ingestFreshFileStats( array $stats, $latest ) {
728 $success = true;
729
730 foreach ( $stats as $path => $stat ) {
731 if ( is_array( $stat ) ) {
732 // Strongly consistent backends might automatically set this flag
733 $stat['latest'] ??= $latest;
734
735 $this->procFileStatCache->setField( $path, 'stat', $stat );
736 if ( isset( $stat['sha1'] ) ) {
737 // Some backends store the SHA-1 hash as metadata
738 $this->procFileStatCache->setField(
739 $path,
740 'sha1',
741 [ 'hash' => $stat['sha1'], 'latest' => $latest ]
742 );
743 }
744 if ( isset( $stat['xattr'] ) ) {
745 // Some backends store custom headers/metadata
746 $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
747 $this->procFileStatCache->setField(
748 $path,
749 'xattr',
750 [ 'map' => $stat['xattr'], 'latest' => $latest ]
751 );
752 }
753 // Update persistent cache (@TODO: set all entries in one batch)
754 $this->setFileCache( $path, $stat );
755 } elseif ( $stat === self::RES_ABSENT ) {
756 $this->procFileStatCache->setField(
757 $path,
758 'stat',
759 $latest ? self::ABSENT_LATEST : self::ABSENT_NORMAL
760 );
761 $this->procFileStatCache->setField(
762 $path,
763 'xattr',
764 [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
765 );
766 $this->procFileStatCache->setField(
767 $path,
768 'sha1',
769 [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
770 );
771 $this->logger->debug(
772 __METHOD__ . ': File {path} does not exist',
773 [ 'path' => $path ]
774 );
775 } else {
776 $success = false;
777 $this->logger->error(
778 __METHOD__ . ': Could not stat file {path}',
779 [ 'path' => $path ]
780 );
781 }
782 }
783
784 return $success;
785 }
786
792 abstract protected function doGetFileStat( array $params );
793
795 public function getFileContentsMulti( array $params ) {
796 $params = $this->setConcurrencyFlags( $params );
797 $contents = $this->doGetFileContentsMulti( $params );
798 foreach ( $contents as $path => $content ) {
799 if ( !is_string( $content ) ) {
800 $contents[$path] = self::CONTENT_FAIL; // used for all failure cases
801 }
802 }
803
804 return $contents;
805 }
806
813 protected function doGetFileContentsMulti( array $params ) {
814 $contents = [];
815 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
816 if ( $fsFile instanceof FSFile ) {
817 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
818 $content = @file_get_contents( $fsFile->getPath() );
819 $contents[$path] = is_string( $content ) ? $content : self::RES_ERROR;
820 } else {
821 // self::RES_ERROR or self::RES_ABSENT
822 $contents[$path] = $fsFile;
823 }
824 }
825
826 return $contents;
827 }
828
830 final public function getFileXAttributes( array $params ) {
831 $path = self::normalizeStoragePath( $params['src'] );
832 if ( $path === null ) {
833 return self::XATTRS_FAIL; // invalid storage path
834 }
835 $latest = !empty( $params['latest'] ); // use latest data?
836 if ( $this->procFileStatCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
837 $stat = $this->procFileStatCache->getField( $path, 'xattr' );
838 // If we want the latest data, check that this cached
839 // value was in fact fetched with the latest available data.
840 if ( !$latest || $stat['latest'] ) {
841 return $stat['map'];
842 }
843 }
844 $fields = $this->doGetFileXAttributes( $params );
845 if ( is_array( $fields ) ) {
846 $fields = self::normalizeXAttributes( $fields );
847 $this->procFileStatCache->setField(
848 $path,
849 'xattr',
850 [ 'map' => $fields, 'latest' => $latest ]
851 );
852 } elseif ( $fields === self::RES_ABSENT ) {
853 $this->procFileStatCache->setField(
854 $path,
855 'xattr',
856 [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
857 );
858 } else {
859 $fields = self::XATTRS_FAIL; // used for all failure cases
860 }
861
862 return $fields;
863 }
864
871 protected function doGetFileXAttributes( array $params ) {
872 return [ 'headers' => [], 'metadata' => [] ]; // not supported
873 }
874
876 final public function getFileSha1Base36( array $params ) {
877 $path = self::normalizeStoragePath( $params['src'] );
878 if ( $path === null ) {
879 return self::SHA1_FAIL; // invalid storage path
880 }
881 $latest = !empty( $params['latest'] ); // use latest data?
882 if ( $this->procFileStatCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
883 $stat = $this->procFileStatCache->getField( $path, 'sha1' );
884 // If we want the latest data, check that this cached
885 // value was in fact fetched with the latest available data.
886 if ( !$latest || $stat['latest'] ) {
887 return $stat['hash'];
888 }
889 }
890 $sha1 = $this->doGetFileSha1Base36( $params );
891 if ( is_string( $sha1 ) ) {
892 $this->procFileStatCache->setField(
893 $path,
894 'sha1',
895 [ 'hash' => $sha1, 'latest' => $latest ]
896 );
897 } elseif ( $sha1 === self::RES_ABSENT ) {
898 $this->procFileStatCache->setField(
899 $path,
900 'sha1',
901 [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
902 );
903 } else {
904 $sha1 = self::SHA1_FAIL; // used for all failure cases
905 }
906
907 return $sha1;
908 }
909
916 protected function doGetFileSha1Base36( array $params ) {
917 $fsFile = $this->getLocalReference( $params );
918 if ( $fsFile instanceof FSFile ) {
919 $sha1 = $fsFile->getSha1Base36();
920
921 return is_string( $sha1 ) ? $sha1 : self::RES_ERROR;
922 }
923
924 return $fsFile === self::RES_ERROR ? self::RES_ERROR : self::RES_ABSENT;
925 }
926
928 final public function getFileProps( array $params ) {
929 $fsFile = $this->getLocalReference( $params );
930
931 return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
932 }
933
935 final public function getLocalReferenceMulti( array $params ) {
936 $params = $this->setConcurrencyFlags( $params );
937
938 $fsFiles = []; // (path => FSFile)
939 $latest = !empty( $params['latest'] ); // use latest data?
940 // Reuse any files already in process cache...
941 foreach ( $params['srcs'] as $src ) {
943 if ( $path === null ) {
944 $fsFiles[$src] = self::RES_ERROR; // invalid storage path
945 } elseif ( $this->procFileDataCache->hasField( $path, 'localRef' ) ) {
946 $val = $this->procFileDataCache->getField( $path, 'localRef' );
947 // If we want the latest data, check that this cached
948 // value was in fact fetched with the latest available data.
949 if ( !$latest || $val['latest'] ) {
950 $fsFiles[$src] = $val['object'];
951 }
952 }
953 }
954 // Fetch local references of any remaining files...
955 $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
956 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
957 $fsFiles[$path] = $fsFile;
958 if ( $fsFile instanceof FSFile ) {
959 $this->procFileDataCache->setField(
960 $path,
961 'localRef',
962 [ 'object' => $fsFile, 'latest' => $latest ]
963 );
964 }
965 }
966
967 return $fsFiles;
968 }
969
976 protected function doGetLocalReferenceMulti( array $params ) {
977 return $this->doGetLocalCopyMulti( $params );
978 }
979
981 final public function getLocalCopyMulti( array $params ) {
982 $params = $this->setConcurrencyFlags( $params );
983
984 return $this->doGetLocalCopyMulti( $params );
985 }
986
992 abstract protected function doGetLocalCopyMulti( array $params );
993
1000 public function getFileHttpUrl( array $params ) {
1001 return self::TEMPURL_ERROR; // not supported
1002 }
1003
1005 public function addShellboxInputFile( BoxedCommand $command, string $boxedName,
1006 array $params
1007 ) {
1008 $ref = $this->getLocalReference( [ 'src' => $params['src'] ] );
1009 if ( $ref === false ) {
1010 return $this->newStatus( 'backend-fail-notexists', $params['src'] );
1011 } elseif ( $ref === null ) {
1012 return $this->newStatus( 'backend-fail-read', $params['src'] );
1013 } else {
1014 $file = $command->newInputFileFromFile( $ref->getPath() )
1015 ->userData( __CLASS__, $ref );
1016 $command->inputFile( $boxedName, $file );
1017 return $this->newStatus();
1018 }
1019 }
1020
1022 final public function streamFile( array $params ) {
1023 $status = $this->newStatus();
1024
1025 // Always set some fields for subclass convenience
1026 $params['options'] ??= [];
1027 $params['headers'] ??= [];
1028
1029 // Don't stream it out as text/html if there was a PHP error
1030 if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
1031 print "Headers already sent, terminating.\n";
1032 $status->fatal( 'backend-fail-stream', $params['src'] );
1033 return $status;
1034 }
1035
1036 $status->merge( $this->doStreamFile( $params ) );
1037
1038 return $status;
1039 }
1040
1047 protected function doStreamFile( array $params ) {
1048 $status = $this->newStatus();
1049
1050 $flags = 0;
1051 $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0;
1052 $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0;
1053
1054 $fsFile = $this->getLocalReference( $params );
1055 if ( $fsFile ) {
1056 $streamer = new HTTPFileStreamer(
1057 $fsFile->getPath(),
1058 $this->getStreamerOptions()
1059 );
1060 $res = $streamer->stream( $params['headers'], true, $params['options'], $flags );
1061 } else {
1062 $res = false;
1063 HTTPFileStreamer::send404Message( $params['src'], $flags );
1064 }
1065
1066 if ( !$res ) {
1067 $status->fatal( 'backend-fail-stream', $params['src'] );
1068 }
1069
1070 return $status;
1071 }
1072
1074 final public function directoryExists( array $params ) {
1075 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1076 if ( $dir === null ) {
1077 return self::EXISTENCE_ERROR; // invalid storage path
1078 }
1079 if ( $shard !== null ) { // confined to a single container/shard
1080 return $this->doDirectoryExists( $fullCont, $dir, $params );
1081 } else { // directory is on several shards
1082 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1083 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1084 $res = false; // response
1085 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
1086 $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
1087 if ( $exists === true ) {
1088 $res = true;
1089 break; // found one!
1090 } elseif ( $exists === self::RES_ERROR ) {
1091 $res = self::EXISTENCE_ERROR;
1092 }
1093 }
1094
1095 return $res;
1096 }
1097 }
1098
1107 abstract protected function doDirectoryExists( $fullCont, $dirRel, array $params );
1108
1110 final public function getDirectoryList( array $params ) {
1111 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1112 if ( $dir === null ) {
1113 return self::EXISTENCE_ERROR; // invalid storage path
1114 }
1115 if ( $shard !== null ) {
1116 // File listing is confined to a single container/shard
1117 return $this->getDirectoryListInternal( $fullCont, $dir, $params );
1118 } else {
1119 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1120 // File listing spans multiple containers/shards
1121 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1122
1123 return new FileBackendStoreShardDirIterator( $this,
1124 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
1125 }
1126 }
1127
1138 abstract public function getDirectoryListInternal( $fullCont, $dirRel, array $params );
1139
1141 final public function getFileList( array $params ) {
1142 [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] );
1143 if ( $dir === null ) {
1144 return self::LIST_ERROR; // invalid storage path
1145 }
1146 if ( $shard !== null ) {
1147 // File listing is confined to a single container/shard
1148 return $this->getFileListInternal( $fullCont, $dir, $params );
1149 } else {
1150 $this->logger->debug( __METHOD__ . ": iterating over all container shards." );
1151 // File listing spans multiple containers/shards
1152 [ , $shortCont, ] = self::splitStoragePath( $params['dir'] );
1153
1154 return new FileBackendStoreShardFileIterator( $this,
1155 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
1156 }
1157 }
1158
1169 abstract public function getFileListInternal( $fullCont, $dirRel, array $params );
1170
1182 final public function getOperationsInternal( array $ops ) {
1183 $supportedOps = [
1184 'store' => StoreFileOp::class,
1185 'copy' => CopyFileOp::class,
1186 'move' => MoveFileOp::class,
1187 'delete' => DeleteFileOp::class,
1188 'create' => CreateFileOp::class,
1189 'describe' => DescribeFileOp::class,
1190 'null' => NullFileOp::class
1191 ];
1192
1193 $performOps = []; // array of FileOp objects
1194 // Build up ordered array of FileOps...
1195 foreach ( $ops as $operation ) {
1196 $opName = $operation['op'];
1197 if ( isset( $supportedOps[$opName] ) ) {
1198 $class = $supportedOps[$opName];
1199 // Get params for this operation
1200 $params = $operation;
1201 // Append the FileOp class
1202 $performOps[] = new $class( $this, $params, $this->logger );
1203 } else {
1204 throw new FileBackendError( "Operation '$opName' is not supported." );
1205 }
1206 }
1207
1208 return $performOps;
1209 }
1210
1221 final public function getPathsToLockForOpsInternal( array $performOps ) {
1222 // Build up a list of files to lock...
1223 $paths = [ 'sh' => [], 'ex' => [] ];
1224 foreach ( $performOps as $fileOp ) {
1225 $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1226 $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1227 }
1228 // Optimization: if doing an EX lock anyway, don't also set an SH one
1229 $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1230 // Get a shared lock on the parent directory of each path changed
1231 $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1232
1233 return [
1234 LockManager::LOCK_UW => $paths['sh'],
1235 LockManager::LOCK_EX => $paths['ex']
1236 ];
1237 }
1238
1240 public function getScopedLocksForOps( array $ops, StatusValue $status ) {
1241 $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1242
1243 return $this->getScopedFileLocks( $paths, 'mixed', $status );
1244 }
1245
1247 final protected function doOperationsInternal( array $ops, array $opts ) {
1248 $status = $this->newStatus();
1249
1250 // Fix up custom header name/value pairs
1251 $ops = array_map( $this->sanitizeOpHeaders( ... ), $ops );
1252 // Build up a list of FileOps and involved paths
1253 $fileOps = $this->getOperationsInternal( $ops );
1254 $pathsUsed = [];
1255 foreach ( $fileOps as $fileOp ) {
1256 $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() );
1257 }
1258
1259 // Acquire any locks as needed for the scope of this function
1260 if ( empty( $opts['nonLocking'] ) ) {
1261 $pathsByLockType = $this->getPathsToLockForOpsInternal( $fileOps );
1263 $scopeLock = $this->getScopedFileLocks( $pathsByLockType, 'mixed', $status );
1264 if ( !$status->isOK() ) {
1265 return $status; // abort
1266 }
1267 }
1268
1269 // Clear any file cache entries (after locks acquired)
1270 if ( empty( $opts['preserveCache'] ) ) {
1271 $this->clearCache( $pathsUsed );
1272 }
1273
1274 // Enlarge the cache to fit the stat entries of these files
1275 $this->procFileStatCache->setMaxSize(
1276 max( 2 * count( $pathsUsed ), self::CACHE_CHEAP_SIZE )
1277 );
1278
1279 // Load from the persistent container caches
1280 $this->primeContainerCache( $pathsUsed );
1281 // Get the latest stat info for all the files (having locked them)
1282 $ok = $this->preloadFileStat( [ 'srcs' => $pathsUsed, 'latest' => true ] );
1283
1284 if ( $ok ) {
1285 // Actually attempt the operation batch...
1286 $opts = $this->setConcurrencyFlags( $opts );
1287 $subStatus = FileOpBatch::attempt( $fileOps, $opts );
1288 } else {
1289 // If we could not even stat some files, then bail out
1290 $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
1291 foreach ( $ops as $i => $op ) { // mark each op as failed
1292 $subStatus->success[$i] = false;
1293 ++$subStatus->failCount;
1294 }
1295 $this->logger->error( static::class . "-{$this->name} stat failure",
1296 [ 'aborted_operations' => $ops ]
1297 );
1298 }
1299
1300 // Merge errors into StatusValue fields
1301 $status->merge( $subStatus );
1302 $status->success = $subStatus->success; // not done in merge()
1303
1304 // Shrink the stat cache back to normal size
1305 $this->procFileStatCache->setMaxSize( self::CACHE_CHEAP_SIZE );
1306
1307 return $status;
1308 }
1309
1311 final protected function doQuickOperationsInternal( array $ops, array $opts ) {
1312 $status = $this->newStatus();
1313
1314 // Fix up custom header name/value pairs
1315 $ops = array_map( $this->sanitizeOpHeaders( ... ), $ops );
1316 // Build up a list of FileOps and involved paths
1317 $fileOps = $this->getOperationsInternal( $ops );
1318 $pathsUsed = [];
1319 foreach ( $fileOps as $fileOp ) {
1320 $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() );
1321 }
1322
1323 // Clear any file cache entries for involved paths
1324 $this->clearCache( $pathsUsed );
1325
1326 // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
1327 $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
1328 $maxConcurrency = $this->concurrency; // throttle
1330 $statuses = []; // array of (index => StatusValue)
1332 $batch = [];
1333 foreach ( $fileOps as $index => $fileOp ) {
1334 $subStatus = $async
1335 ? $fileOp->attemptAsyncQuick()
1336 : $fileOp->attemptQuick();
1337 if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1338 if ( count( $batch ) >= $maxConcurrency ) {
1339 // Execute this batch. Don't queue any more ops since they contain
1340 // open filehandles which are a limited resource (T230245).
1341 $statuses += $this->executeOpHandlesInternal( $batch );
1342 $batch = [];
1343 }
1344 $batch[$index] = $subStatus->value; // keep index
1345 } else { // error or completed
1346 $statuses[$index] = $subStatus; // keep index
1347 }
1348 }
1349 if ( count( $batch ) ) {
1350 $statuses += $this->executeOpHandlesInternal( $batch );
1351 }
1352 // Marshall and merge all the responses...
1353 foreach ( $statuses as $index => $subStatus ) {
1354 $status->merge( $subStatus );
1355 if ( $subStatus->isOK() ) {
1356 $status->success[$index] = true;
1357 ++$status->successCount;
1358 } else {
1359 $status->success[$index] = false;
1360 ++$status->failCount;
1361 }
1362 }
1363
1364 $this->clearCache( $pathsUsed );
1365
1366 return $status;
1367 }
1368
1378 final public function executeOpHandlesInternal( array $fileOpHandles ) {
1379 foreach ( $fileOpHandles as $fileOpHandle ) {
1380 if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1381 throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." );
1382 } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1383 throw new InvalidArgumentException( "Expected handle for this file backend." );
1384 }
1385 }
1386
1387 $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1388 foreach ( $fileOpHandles as $fileOpHandle ) {
1389 $fileOpHandle->closeResources();
1390 }
1391
1392 return $statuses;
1393 }
1394
1404 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1405 if ( count( $fileOpHandles ) ) {
1406 throw new FileBackendError( "Backend does not support asynchronous operations." );
1407 }
1408
1409 return [];
1410 }
1411
1423 protected function sanitizeOpHeaders( array $op ) {
1424 static $longs = [ 'content-disposition' ];
1425
1426 if ( isset( $op['headers'] ) ) { // op sets HTTP headers
1427 $newHeaders = [];
1428 foreach ( $op['headers'] as $name => $value ) {
1429 $name = strtolower( $name );
1430 $maxHVLen = in_array( $name, $longs ) ? INF : 255;
1431 if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
1432 $this->logger->error( "Header '{header}' is too long.", [
1433 'filebackend' => $this->name,
1434 'header' => "$name: $value",
1435 ] );
1436 } else {
1437 $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
1438 }
1439 }
1440 $op['headers'] = $newHeaders;
1441 }
1442
1443 return $op;
1444 }
1445
1446 final public function preloadCache( array $paths ) {
1447 $fullConts = []; // full container names
1448 foreach ( $paths as $path ) {
1449 [ $fullCont, , ] = $this->resolveStoragePath( $path );
1450 $fullConts[] = $fullCont;
1451 }
1452 // Load from the persistent file and container caches
1453 $this->primeContainerCache( $fullConts );
1454 $this->primeFileCache( $paths );
1455 }
1456
1457 final public function clearCache( ?array $paths = null ) {
1458 if ( is_array( $paths ) ) {
1459 $paths = array_map( FileBackend::normalizeStoragePath( ... ), $paths );
1460 $paths = array_filter( $paths, 'strlen' ); // remove nulls
1461 }
1462 if ( $paths === null ) {
1463 $this->procFileStatCache->clear();
1464 $this->procFileDataCache->clear();
1465 } else {
1466 foreach ( $paths as $path ) {
1467 $this->procFileStatCache->clear( $path );
1468 $this->procFileDataCache->clear( $path );
1469 }
1470 }
1471 $this->doClearCache( $paths );
1472 }
1473
1482 protected function doClearCache( ?array $paths = null ) {
1483 }
1484
1486 final public function preloadFileStat( array $params ) {
1487 $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
1488 $stats = $this->doGetFileStatMulti( $params );
1489 if ( $stats === null ) {
1490 return true; // not supported
1491 }
1492
1493 // Whether this queried the backend in high consistency mode
1494 $latest = !empty( $params['latest'] );
1495
1496 return $this->ingestFreshFileStats( $stats, $latest );
1497 }
1498
1512 protected function doGetFileStatMulti( array $params ) {
1513 return null; // not supported
1514 }
1515
1524 abstract protected function directoriesAreVirtual();
1525
1536 final protected static function isValidShortContainerName( $container ) {
1537 // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
1538 // might be used by subclasses. Reserve the dot character.
1539 // The only way dots end up in containers (e.g. resolveStoragePath)
1540 // is due to the wikiId container prefix or the above suffixes.
1541 return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
1542 }
1543
1553 final protected static function isValidContainerName( $container ) {
1554 // This accounts for NTFS, Swift, and Ceph restrictions
1555 // and disallows directory separators or traversal characters.
1556 // Note that matching strings URL encode to the same string;
1557 // in Swift/Ceph, the length restriction is *after* URL encoding.
1558 return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1559 }
1560
1574 final protected function resolveStoragePath( $storagePath ) {
1575 [ $backend, $shortCont, $relPath ] = self::splitStoragePath( $storagePath );
1576 if ( $backend === $this->name && $relPath !== null ) { // must be for this backend
1577 $relPath = self::normalizeContainerPath( $relPath );
1578 if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
1579 // Get shard for the normalized path if this container is sharded
1580 $cShard = $this->getContainerShard( $shortCont, $relPath );
1581 // Validate and sanitize the relative path (backend-specific)
1582 $relPath = $this->resolveContainerPath( $shortCont, $relPath );
1583 if ( $relPath !== null ) {
1584 // Prepend any domain ID prefix to the container name
1585 $container = $this->fullContainerName( $shortCont );
1586 if ( self::isValidContainerName( $container ) ) {
1587 // Validate and sanitize the container name (backend-specific)
1588 $container = $this->resolveContainerName( "{$container}{$cShard}" );
1589 if ( $container !== null ) {
1590 return [ $container, $relPath, $cShard ];
1591 }
1592 }
1593 }
1594 }
1595 }
1596
1597 return [ null, null, null ];
1598 }
1599
1615 final protected function resolveStoragePathReal( $storagePath ) {
1616 [ $container, $relPath, $cShard ] = $this->resolveStoragePath( $storagePath );
1617 if ( $cShard !== null && !str_ends_with( $relPath, '/' ) ) {
1618 return [ $container, $relPath ];
1619 }
1620
1621 return [ null, null ];
1622 }
1623
1632 final protected function getContainerShard( $container, $relPath ) {
1633 [ $levels, $base, $repeat ] = $this->getContainerHashLevels( $container );
1634 if ( $levels == 1 || $levels == 2 ) {
1635 // Hash characters are either base 16 or 36
1636 $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1637 // Get a regex that represents the shard portion of paths.
1638 // The concatenation of the captures gives us the shard.
1639 if ( $levels === 1 ) { // 16 or 36 shards per container
1640 $hashDirRegex = '(' . $char . ')';
1641 } else { // 256 or 1296 shards per container
1642 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1643 $hashDirRegex = $char . '/(' . $char . '{2})';
1644 } else { // short hash dir format (e.g. "a/b/c")
1645 $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1646 }
1647 }
1648 // Allow certain directories to be above the hash dirs so as
1649 // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1650 // They must be 2+ chars to avoid any hash directory ambiguity.
1651 $m = [];
1652 if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1653 return '.' . implode( '', array_slice( $m, 1 ) );
1654 }
1655
1656 return null; // failed to match
1657 }
1658
1659 return ''; // no sharding
1660 }
1661
1670 final public function isSingleShardPathInternal( $storagePath ) {
1671 [ , , $shard ] = $this->resolveStoragePath( $storagePath );
1672
1673 return ( $shard !== null );
1674 }
1675
1684 final protected function getContainerHashLevels( $container ) {
1685 if ( isset( $this->shardViaHashLevels[$container] ) ) {
1686 $config = $this->shardViaHashLevels[$container];
1687 $hashLevels = (int)$config['levels'];
1688 if ( $hashLevels == 1 || $hashLevels == 2 ) {
1689 $hashBase = (int)$config['base'];
1690 if ( $hashBase == 16 || $hashBase == 36 ) {
1691 return [ $hashLevels, $hashBase, $config['repeat'] ];
1692 }
1693 }
1694 }
1695
1696 return [ 0, 0, false ]; // no sharding
1697 }
1698
1705 final protected function getContainerSuffixes( $container ) {
1706 $shards = [];
1707 [ $digits, $base ] = $this->getContainerHashLevels( $container );
1708 if ( $digits > 0 ) {
1709 $numShards = $base ** $digits;
1710 for ( $index = 0; $index < $numShards; $index++ ) {
1711 $shards[] = '.' . \Wikimedia\base_convert( (string)$index, 10, $base, $digits );
1712 }
1713 }
1714
1715 return $shards;
1716 }
1717
1724 final protected function fullContainerName( $container ) {
1725 if ( $this->domainId != '' ) {
1726 return "{$this->domainId}-$container";
1727 } else {
1728 return $container;
1729 }
1730 }
1731
1741 protected function resolveContainerName( $container ) {
1742 return $container;
1743 }
1744
1756 protected function resolveContainerPath( $container, $relStoragePath ) {
1757 return $relStoragePath;
1758 }
1759
1766 private function containerCacheKey( $container ) {
1767 return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
1768 }
1769
1776 final protected function setContainerCache( $container, array $val ) {
1777 if ( !$this->wanStatCache->set(
1778 $this->containerCacheKey( $container ),
1779 $val,
1780 14 * 86400
1781 ) ) {
1782 $this->logger->warning( "Unable to set stat cache for container {container}.",
1783 [ 'filebackend' => $this->name, 'container' => $container ]
1784 );
1785 }
1786 }
1787
1794 final protected function deleteContainerCache( $container ) {
1795 if ( !$this->wanStatCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1796 $this->logger->warning( "Unable to delete stat cache for container {container}.",
1797 [ 'filebackend' => $this->name, 'container' => $container ]
1798 );
1799 }
1800 }
1801
1807 final protected function primeContainerCache( array $items ) {
1808 $paths = []; // list of storage paths
1809 $contNames = []; // (cache key => resolved container name)
1810 // Get all the paths/containers from the items...
1811 foreach ( $items as $item ) {
1812 if ( self::isStoragePath( $item ) ) {
1813 $paths[] = $item;
1814 } elseif ( is_string( $item ) ) { // full container name
1815 $contNames[$this->containerCacheKey( $item )] = $item;
1816 }
1817 }
1818 // Get all the corresponding cache keys for paths...
1819 foreach ( $paths as $path ) {
1820 [ $fullCont, , ] = $this->resolveStoragePath( $path );
1821 if ( $fullCont !== null ) { // valid path for this backend
1822 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1823 }
1824 }
1825
1826 $contInfo = []; // (resolved container name => cache value)
1827 // Get all cache entries for these container cache keys...
1828 $values = $this->wanStatCache->getMulti( array_keys( $contNames ) );
1829 foreach ( $values as $cacheKey => $val ) {
1830 $contInfo[$contNames[$cacheKey]] = $val;
1831 }
1832
1833 // Populate the container process cache for the backend...
1834 $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1835 }
1836
1845 protected function doPrimeContainerCache( array $containerInfo ) {
1846 }
1847
1854 private function fileCacheKey( $path ) {
1855 return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path );
1856 }
1857
1866 final protected function setFileCache( $path, array $val ) {
1868 if ( $path === null ) {
1869 return; // invalid storage path
1870 }
1871 $mtime = (int)ConvertibleTimestamp::convert( TS::UNIX, $val['mtime'] );
1872 $ttl = $this->wanStatCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 );
1873 // Set the cache unless it is currently salted.
1874 if ( !$this->wanStatCache->set( $this->fileCacheKey( $path ), $val, $ttl ) ) {
1875 $this->logger->warning( "Unable to set stat cache for file {path}.",
1876 [ 'filebackend' => $this->name, 'path' => $path ]
1877 );
1878 }
1879 }
1880
1889 final protected function deleteFileCache( $path ) {
1891 if ( $path === null ) {
1892 return; // invalid storage path
1893 }
1894 if ( !$this->wanStatCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
1895 $this->logger->warning( "Unable to delete stat cache for file {path}.",
1896 [ 'filebackend' => $this->name, 'path' => $path ]
1897 );
1898 }
1899 }
1900
1908 final protected function primeFileCache( array $items ) {
1909 $paths = []; // list of storage paths
1910 $pathNames = []; // (cache key => storage path)
1911 // Get all the paths/containers from the items...
1912 foreach ( $items as $item ) {
1913 if ( self::isStoragePath( $item ) ) {
1915 if ( $path !== null ) {
1916 $paths[] = $path;
1917 }
1918 }
1919 }
1920 // Get all the corresponding cache keys for paths...
1921 foreach ( $paths as $path ) {
1922 [ , $rel, ] = $this->resolveStoragePath( $path );
1923 if ( $rel !== null ) { // valid path for this backend
1924 $pathNames[$this->fileCacheKey( $path )] = $path;
1925 }
1926 }
1927 // Get all cache entries for these file cache keys.
1928 // Note that negatives are not cached by getFileStat()/preloadFileStat().
1929 $values = $this->wanStatCache->getMulti( array_keys( $pathNames ) );
1930 // Load all of the results into process cache...
1931 foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) {
1932 $path = $pathNames[$cacheKey];
1933 // This flag only applies to stat info loaded directly
1934 // from a high consistency backend query to the process cache
1935 unset( $stat['latest'] );
1936
1937 $this->procFileStatCache->setField( $path, 'stat', $stat );
1938 if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) {
1939 // Some backends store SHA-1 as metadata
1940 $this->procFileStatCache->setField(
1941 $path,
1942 'sha1',
1943 [ 'hash' => $stat['sha1'], 'latest' => false ]
1944 );
1945 }
1946 if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) {
1947 // Some backends store custom headers/metadata
1948 $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
1949 $this->procFileStatCache->setField(
1950 $path,
1951 'xattr',
1952 [ 'map' => $stat['xattr'], 'latest' => false ]
1953 );
1954 }
1955 }
1956 }
1957
1965 final protected static function normalizeXAttributes( array $xattr ) {
1966 $newXAttr = [ 'headers' => [], 'metadata' => [] ];
1967
1968 foreach ( $xattr['headers'] as $name => $value ) {
1969 $newXAttr['headers'][strtolower( $name )] = $value;
1970 }
1971
1972 foreach ( $xattr['metadata'] as $name => $value ) {
1973 $newXAttr['metadata'][strtolower( $name )] = $value;
1974 }
1975
1976 return $newXAttr;
1977 }
1978
1985 final protected function setConcurrencyFlags( array $opts ) {
1986 $opts['concurrency'] = 1; // off
1987 if ( $this->parallelize === 'implicit' ) {
1988 if ( $opts['parallelize'] ?? true ) {
1989 $opts['concurrency'] = $this->concurrency;
1990 }
1991 } elseif ( $this->parallelize === 'explicit' ) {
1992 if ( !empty( $opts['parallelize'] ) ) {
1993 $opts['concurrency'] = $this->concurrency;
1994 }
1995 }
1996
1997 return $opts;
1998 }
1999
2009 protected function getContentType( $storagePath, $content, $fsPath ) {
2010 if ( $this->mimeCallback ) {
2011 return ( $this->mimeCallback )( $storagePath, $content, $fsPath );
2012 }
2013
2014 $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false;
2015 return $mime ?: 'unknown/unknown';
2016 }
2017}
2018
2020class_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.
static normalizeXAttributes(array $xattr)
Normalize file headers/metadata to the FileBackend::getFileXAttributes() format.
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.
MapCacheLRU $procFileStatCache
LRU map of file paths to small (RAM/disk) cache items.
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.
MapCacheLRU $procFileDataCache
LRU map of file paths to large (RAM/disk) cache items.
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()
Whether this a key/value store where directories are merely virtual.
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.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
No-op implementation that stores nothing.
Store key-value entries in a size-limited in-memory LRU cache.
Multi-datacenter aware caching interface.