MediaWiki REL1_34
FileBackend.php
Go to the documentation of this file.
1<?php
31use Psr\Log\LoggerAwareInterface;
32use Psr\Log\LoggerInterface;
33use Wikimedia\ScopedCallback;
34use Psr\Log\NullLogger;
35
94abstract class FileBackend implements LoggerAwareInterface {
96 protected $name;
97
99 protected $domainId;
100
102 protected $readOnly;
103
105 protected $parallelize;
106
108 protected $concurrency;
109
112
114 protected $lockManager;
116 protected $fileJournal;
118 protected $logger;
120 protected $profiler;
121
123 protected $obResetFunc;
127 protected $statusWrapper;
128
130 const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
131 const ATTR_METADATA = 2; // files can be stored with metadata key/values
132 const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
133
135 const STAT_ABSENT = false;
136
138 const STAT_ERROR = null;
140 const LIST_ERROR = null;
142 const TEMPURL_ERROR = null;
144 const EXISTENCE_ERROR = null;
145
147 const TIMESTAMP_FAIL = false;
149 const CONTENT_FAIL = false;
151 const XATTRS_FAIL = false;
153 const SIZE_FAIL = false;
155 const SHA1_FAIL = false;
156
190 public function __construct( array $config ) {
191 $this->name = $config['name'];
192 $this->domainId = $config['domainId'] // e.g. "my_wiki-en_"
193 ?? $config['wikiId']; // b/c alias
194 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
195 throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." );
196 } elseif ( !is_string( $this->domainId ) ) {
197 throw new InvalidArgumentException(
198 "Backend domain ID not provided for '{$this->name}'." );
199 }
200 $this->lockManager = $config['lockManager'] ?? new NullLockManager( [] );
201 $this->fileJournal = $config['fileJournal']
202 ?? FileJournal::factory( [ 'class' => NullFileJournal::class ], $this->name );
203 $this->readOnly = isset( $config['readOnly'] )
204 ? (string)$config['readOnly']
205 : '';
206 $this->parallelize = isset( $config['parallelize'] )
207 ? (string)$config['parallelize']
208 : 'off';
209 $this->concurrency = isset( $config['concurrency'] )
210 ? (int)$config['concurrency']
211 : 50;
212 $this->obResetFunc = $config['obResetFunc'] ?? [ $this, 'resetOutputBuffer' ];
213 $this->streamMimeFunc = $config['streamMimeFunc'] ?? null;
214 $this->statusWrapper = $config['statusWrapper'] ?? null;
215
216 $this->profiler = $config['profiler'] ?? null;
217 if ( !is_callable( $this->profiler ) ) {
218 $this->profiler = null;
219 }
220 $this->logger = $config['logger'] ?? new NullLogger();
221 $this->statusWrapper = $config['statusWrapper'] ?? null;
222 // tmpDirectory gets precedence for backward compatibility
223 if ( isset( $config['tmpDirectory'] ) ) {
224 $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] );
225 } else {
226 $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory();
227 }
228 }
229
230 public function setLogger( LoggerInterface $logger ) {
231 $this->logger = $logger;
232 }
233
242 final public function getName() {
243 return $this->name;
244 }
245
252 final public function getDomainId() {
253 return $this->domainId;
254 }
255
263 final public function getWikiId() {
264 return $this->getDomainId();
265 }
266
272 final public function isReadOnly() {
273 return ( $this->readOnly != '' );
274 }
275
281 final public function getReadOnlyReason() {
282 return ( $this->readOnly != '' ) ? $this->readOnly : false;
283 }
284
291 public function getFeatures() {
293 }
294
302 final public function hasFeatures( $bitfield ) {
303 return ( $this->getFeatures() & $bitfield ) === $bitfield;
304 }
305
458 final public function doOperations( array $ops, array $opts = [] ) {
459 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
460 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
461 }
462 if ( $ops === [] ) {
463 return $this->newStatus(); // nothing to do
464 }
465
466 $ops = $this->resolveFSFileObjects( $ops );
467 if ( empty( $opts['force'] ) ) { // sanity
468 unset( $opts['nonLocking'] );
469 }
470
472 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
473
474 return $this->doOperationsInternal( $ops, $opts );
475 }
476
483 abstract protected function doOperationsInternal( array $ops, array $opts );
484
496 final public function doOperation( array $op, array $opts = [] ) {
497 return $this->doOperations( [ $op ], $opts );
498 }
499
510 final public function create( array $params, array $opts = [] ) {
511 return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
512 }
513
524 final public function store( array $params, array $opts = [] ) {
525 return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
526 }
527
538 final public function copy( array $params, array $opts = [] ) {
539 return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
540 }
541
552 final public function move( array $params, array $opts = [] ) {
553 return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
554 }
555
566 final public function delete( array $params, array $opts = [] ) {
567 return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
568 }
569
581 final public function describe( array $params, array $opts = [] ) {
582 return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
583 }
584
699 final public function doQuickOperations( array $ops, array $opts = [] ) {
700 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
701 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
702 }
703 if ( $ops === [] ) {
704 return $this->newStatus(); // nothing to do
705 }
706
707 $ops = $this->resolveFSFileObjects( $ops );
708 foreach ( $ops as &$op ) {
709 $op['overwrite'] = true; // avoids RTTs in key/value stores
710 }
711
713 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
714
715 return $this->doQuickOperationsInternal( $ops );
716 }
717
724 abstract protected function doQuickOperationsInternal( array $ops );
725
736 final public function doQuickOperation( array $op ) {
737 return $this->doQuickOperations( [ $op ] );
738 }
739
750 final public function quickCreate( array $params ) {
751 return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
752 }
753
764 final public function quickStore( array $params ) {
765 return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
766 }
767
778 final public function quickCopy( array $params ) {
779 return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
780 }
781
792 final public function quickMove( array $params ) {
793 return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
794 }
795
806 final public function quickDelete( array $params ) {
807 return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
808 }
809
820 final public function quickDescribe( array $params ) {
821 return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
822 }
823
836 abstract public function concatenate( array $params );
837
856 final public function prepare( array $params ) {
857 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
858 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
859 }
861 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
862 return $this->doPrepare( $params );
863 }
864
870 abstract protected function doPrepare( array $params );
871
888 final public function secure( array $params ) {
889 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
890 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
891 }
893 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
894 return $this->doSecure( $params );
895 }
896
902 abstract protected function doSecure( array $params );
903
922 final public function publish( array $params ) {
923 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
924 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
925 }
927 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
928 return $this->doPublish( $params );
929 }
930
936 abstract protected function doPublish( array $params );
937
949 final public function clean( array $params ) {
950 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
951 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
952 }
954 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
955 return $this->doClean( $params );
956 }
957
963 abstract protected function doClean( array $params );
964
981 abstract public function fileExists( array $params );
982
993 abstract public function getFileTimestamp( array $params );
994
1006 final public function getFileContents( array $params ) {
1007 $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1008
1009 return $contents[$params['src']];
1010 }
1011
1025 abstract public function getFileContentsMulti( array $params );
1026
1047 abstract public function getFileXAttributes( array $params );
1048
1059 abstract public function getFileSize( array $params );
1060
1077 abstract public function getFileStat( array $params );
1078
1089 abstract public function getFileSha1Base36( array $params );
1090
1100 abstract public function getFileProps( array $params );
1101
1121 abstract public function streamFile( array $params );
1122
1141 final public function getLocalReference( array $params ) {
1142 $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1143
1144 return $fsFiles[$params['src']];
1145 }
1146
1164 abstract public function getLocalReferenceMulti( array $params );
1165
1178 final public function getLocalCopy( array $params ) {
1179 $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1180
1181 return $tmpFiles[$params['src']];
1182 }
1183
1199 abstract public function getLocalCopyMulti( array $params );
1200
1219 abstract public function getFileHttpUrl( array $params );
1220
1246 abstract public function directoryExists( array $params );
1247
1270 abstract public function getDirectoryList( array $params );
1271
1288 final public function getTopDirectoryList( array $params ) {
1289 return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
1290 }
1291
1312 abstract public function getFileList( array $params );
1313
1330 final public function getTopFileList( array $params ) {
1331 return $this->getFileList( [ 'topOnly' => true ] + $params );
1332 }
1333
1342 abstract public function preloadCache( array $paths );
1343
1352 abstract public function clearCache( array $paths = null );
1353
1368 abstract public function preloadFileStat( array $params );
1369
1381 final public function lockFiles( array $paths, $type, $timeout = 0 ) {
1382 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1383
1384 return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) );
1385 }
1386
1394 final public function unlockFiles( array $paths, $type ) {
1395 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1396
1397 return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) );
1398 }
1399
1416 final public function getScopedFileLocks(
1417 array $paths, $type, StatusValue $status, $timeout = 0
1418 ) {
1419 if ( $type === 'mixed' ) {
1420 foreach ( $paths as &$typePaths ) {
1421 $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
1422 }
1423 } else {
1424 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1425 }
1426
1427 return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
1428 }
1429
1446 abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
1447
1455 final public function getRootStoragePath() {
1456 return "mwstore://{$this->name}";
1457 }
1458
1466 final public function getContainerStoragePath( $container ) {
1467 return $this->getRootStoragePath() . "/{$container}";
1468 }
1469
1475 final public function getJournal() {
1476 return $this->fileJournal;
1477 }
1478
1488 protected function resolveFSFileObjects( array $ops ) {
1489 foreach ( $ops as &$op ) {
1490 $src = $op['src'] ?? null;
1491 if ( $src instanceof FSFile ) {
1492 $op['srcRef'] = $src;
1493 $op['src'] = $src->getPath();
1494 }
1495 }
1496 unset( $op );
1497
1498 return $ops;
1499 }
1500
1508 final public static function isStoragePath( $path ) {
1509 return ( strpos( $path, 'mwstore://' ) === 0 );
1510 }
1511
1520 final public static function splitStoragePath( $storagePath ) {
1521 if ( self::isStoragePath( $storagePath ) ) {
1522 // Remove the "mwstore://" prefix and split the path
1523 $parts = explode( '/', substr( $storagePath, 10 ), 3 );
1524 if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
1525 if ( count( $parts ) == 3 ) {
1526 return $parts; // e.g. "backend/container/path"
1527 } else {
1528 return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
1529 }
1530 }
1531 }
1532
1533 return [ null, null, null ];
1534 }
1535
1543 final public static function normalizeStoragePath( $storagePath ) {
1544 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
1545 if ( $relPath !== null ) { // must be for this backend
1546 $relPath = self::normalizeContainerPath( $relPath );
1547 if ( $relPath !== null ) {
1548 return ( $relPath != '' )
1549 ? "mwstore://{$backend}/{$container}/{$relPath}"
1550 : "mwstore://{$backend}/{$container}";
1551 }
1552 }
1553
1554 return null;
1555 }
1556
1565 final public static function parentStoragePath( $storagePath ) {
1566 $storagePath = dirname( $storagePath );
1567 list( , , $rel ) = self::splitStoragePath( $storagePath );
1568
1569 return ( $rel === null ) ? null : $storagePath;
1570 }
1571
1579 final public static function extensionFromPath( $path, $case = 'lowercase' ) {
1580 $i = strrpos( $path, '.' );
1581 $ext = $i ? substr( $path, $i + 1 ) : '';
1582
1583 if ( $case === 'lowercase' ) {
1584 $ext = strtolower( $ext );
1585 } elseif ( $case === 'uppercase' ) {
1586 $ext = strtoupper( $ext );
1587 }
1588
1589 return $ext;
1590 }
1591
1599 final public static function isPathTraversalFree( $path ) {
1600 return ( self::normalizeContainerPath( $path ) !== null );
1601 }
1602
1612 final public static function makeContentDisposition( $type, $filename = '' ) {
1613 $parts = [];
1614
1615 $type = strtolower( $type );
1616 if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
1617 throw new InvalidArgumentException( "Invalid Content-Disposition type '$type'." );
1618 }
1619 $parts[] = $type;
1620
1621 if ( strlen( $filename ) ) {
1622 $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1623 }
1624
1625 return implode( ';', $parts );
1626 }
1627
1638 final protected static function normalizeContainerPath( $path ) {
1639 // Normalize directory separators
1640 $path = strtr( $path, '\\', '/' );
1641 // Collapse any consecutive directory separators
1642 $path = preg_replace( '![/]{2,}!', '/', $path );
1643 // Remove any leading directory separator
1644 $path = ltrim( $path, '/' );
1645 // Use the same traversal protection as Title::secureAndSplit()
1646 if ( strpos( $path, '.' ) !== false ) {
1647 if (
1648 $path === '.' ||
1649 $path === '..' ||
1650 strpos( $path, './' ) === 0 ||
1651 strpos( $path, '../' ) === 0 ||
1652 strpos( $path, '/./' ) !== false ||
1653 strpos( $path, '/../' ) !== false
1654 ) {
1655 return null;
1656 }
1657 }
1658
1659 return $path;
1660 }
1661
1670 final protected function newStatus( ...$args ) {
1671 if ( count( $args ) ) {
1672 $sv = StatusValue::newFatal( ...$args );
1673 } else {
1674 $sv = StatusValue::newGood();
1675 }
1676
1677 return $this->wrapStatus( $sv );
1678 }
1679
1684 final protected function wrapStatus( StatusValue $sv ) {
1685 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1686 }
1687
1692 protected function scopedProfileSection( $section ) {
1693 return $this->profiler ? ( $this->profiler )( $section ) : null;
1694 }
1695
1696 protected function resetOutputBuffer() {
1697 while ( ob_get_status() ) {
1698 if ( !ob_end_clean() ) {
1699 // Could not remove output buffer handler; abort now
1700 // to avoid getting in some kind of infinite loop.
1701 break;
1702 }
1703 }
1704 }
1705}
if( $line===false) $args
Definition cdb.php:64
Class representing a non-directory file on the file system.
Definition FSFile.php:32
Base class for all file backend classes (including multi-write backends).
getScopedLocksForOps(array $ops, StatusValue $status)
Get an array of scoped locks needed for a batch of file operations.
concatenate(array $params)
Concatenate a list of storage files into a single file system file.
static parentStoragePath( $storagePath)
Get the parent storage directory of a storage path.
doOperation(array $op, array $opts=[])
Same as doOperations() except it takes a single operation.
quickDelete(array $params)
Performs a single quick delete operation.
create(array $params, array $opts=[])
Performs a single create operation.
wrapStatus(StatusValue $sv)
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
hasFeatures( $bitfield)
Check if the backend medium supports a field of extra features.
quickMove(array $params)
Performs a single quick move operation.
getFileXAttributes(array $params)
Get metadata about a file at a storage path in the backend.
getLocalCopy(array $params)
Get a local copy on disk of the file at a storage path in the backend.
clean(array $params)
Delete a storage directory if it is empty.
string $name
Unique backend name.
move(array $params, array $opts=[])
Performs a single move operation.
getDirectoryList(array $params)
Get an iterator to list all directories under a storage directory.
getFileList(array $params)
Get an iterator to list all stored files under a storage directory.
callable null $profiler
lockFiles(array $paths, $type, $timeout=0)
Lock the files at the given storage paths in the backend.
preloadFileStat(array $params)
Preload file stat information (concurrently if possible) into in-process cache.
describe(array $params, array $opts=[])
Performs a single describe operation.
getDomainId()
Get the domain identifier used for this backend (possibly empty).
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
streamFile(array $params)
Stream the content of the file at a storage path in the backend.
getFileSha1Base36(array $params)
Get a SHA-1 hash of the content of the file at a storage path in the backend.
getReadOnlyReason()
Get an explanatory message if this backend is read-only.
doClean(array $params)
publish(array $params)
Remove measures to block web access to a storage directory and the container it belongs to.
const ATTR_UNICODE_PATHS
callable $obResetFunc
clearCache(array $paths=null)
Invalidate any in-process file stat and property cache.
getFileStat(array $params)
Get quick information about a file at a storage path in the backend.
fileExists(array $params)
Check if a file exists at a storage path in the backend.
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
getFileTimestamp(array $params)
Get the last-modified timestamp of the file at a storage path.
LoggerInterface $logger
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
static makeContentDisposition( $type, $filename='')
Build a Content-Disposition header value per RFC 6266.
static normalizeContainerPath( $path)
Validate and normalize a relative storage path.
doQuickOperationsInternal(array $ops)
string $readOnly
Read-only explanation message.
preloadCache(array $paths)
Preload persistent file stat cache and property cache into in-process cache.
string $domainId
Unique domain name.
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
store(array $params, array $opts=[])
Performs a single store operation.
isReadOnly()
Check if this backend is read-only.
getRootStoragePath()
Get the root storage path of this backend.
getTopDirectoryList(array $params)
Same as FileBackend::getDirectoryList() except only lists directories that are immediately under the ...
getFileProps(array $params)
Get the properties of the content of the file at a storage path in the backend.
getFileSize(array $params)
Get the size (bytes) of a file at a storage path in the backend.
getWikiId()
Alias to getDomainId()
quickStore(array $params)
Performs a single quick store operation.
prepare(array $params)
Prepare a storage directory for usage.
getFileContentsMulti(array $params)
Like getFileContents() except it takes an array of storage paths and returns an order preserved map o...
doQuickOperations(array $ops, array $opts=[])
Perform a set of independent file operations on some files.
unlockFiles(array $paths, $type)
Unlock the files at the given storage paths in the backend.
getContainerStoragePath( $container)
Get the storage path for the given container for this backend.
scopedProfileSection( $section)
const ATTR_METADATA
doOperations(array $ops, array $opts=[])
This is the main entry point into the backend for write operations.
newStatus(... $args)
Yields the result of the status wrapper callback on either:
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
directoryExists(array $params)
Check if a directory exists at a given storage path.
callable $streamMimeFunc
resolveFSFileObjects(array $ops)
Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
string $parallelize
When to do operations in parallel.
doPublish(array $params)
LockManager $lockManager
getLocalCopyMulti(array $params)
Like getLocalCopy() except it takes an array of storage paths and yields an order preserved-map of st...
int $concurrency
How many operations can be done in parallel.
getTopFileList(array $params)
Same as FileBackend::getFileList() except only lists files that are immediately under the given direc...
const ATTR_HEADERS
Bitfield flags for supported features.
doPrepare(array $params)
__construct(array $config)
Create a new backend instance from configuration.
static isPathTraversalFree( $path)
Check if a relative path has no directory traversals.
callable $statusWrapper
doQuickOperation(array $op)
Same as doQuickOperations() except it takes a single operation.
doSecure(array $params)
setLogger(LoggerInterface $logger)
getLocalReferenceMulti(array $params)
Like getLocalReference() except it takes an array of storage paths and yields an order-preserved map ...
quickDescribe(array $params)
Performs a single quick describe operation.
copy(array $params, array $opts=[])
Performs a single copy operation.
FileJournal $fileJournal
getFileContents(array $params)
Get the contents of a file at a storage path in the backend.
TempFSFileFactory $tmpFileFactory
getLocalReference(array $params)
Returns a file system file, identical in content to the file at a storage path.
getName()
Get the unique backend name.
quickCreate(array $params)
Performs a single quick create operation.
quickCopy(array $params)
Performs a single quick copy operation.
getJournal()
Get the file journal object for this backend.
getFileHttpUrl(array $params)
Return an HTTP URL to a given file that requires no authentication to use.
doOperationsInternal(array $ops, array $opts)
secure(array $params)
Take measures to block web access to a storage directory and the container it belongs to.
Class for handling file operation journaling.
static factory(array $config, $backend)
Create an appropriate FileJournal object from config.
Class for handling resource locking.
Simple version of LockManager that only does lock reference counting.
static factory(LockManager $manager, array $paths, $type, StatusValue $status, $timeout=0)
Get a ScopedLock object representing a lock on resource paths.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
if(!is_readable( $file)) $ext
Definition router.php:48