32use Psr\Log\LoggerAwareInterface;
33use Psr\Log\LoggerInterface;
34use Psr\Log\NullLogger;
35use Wikimedia\ScopedCallback;
138 protected const STAT_ABSENT =
false;
141 public const STAT_ERROR =
null;
143 public const LIST_ERROR =
null;
145 public const TEMPURL_ERROR =
null;
147 public const EXISTENCE_ERROR =
null;
150 public const TIMESTAMP_FAIL =
false;
152 public const CONTENT_FAIL =
false;
154 public const XATTRS_FAIL =
false;
156 public const SIZE_FAIL =
false;
158 public const SHA1_FAIL =
false;
194 if ( !array_key_exists(
'name', $config ) ) {
195 throw new InvalidArgumentException(
'Backend name not specified.' );
197 $this->name = $config[
'name'];
198 $this->domainId = $config[
'domainId']
201 if ( !is_string( $this->name ) || !preg_match(
'!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
202 throw new InvalidArgumentException(
"Backend name '{$this->name}' is invalid." );
204 if ( !is_string( $this->domainId ) ) {
205 throw new InvalidArgumentException(
206 "Backend domain ID not provided for '{$this->name}'." );
208 $this->lockManager = $config[
'lockManager'] ??
new NullLockManager( [] );
209 $this->readOnly = isset( $config[
'readOnly'] )
210 ? (string)$config[
'readOnly']
212 $this->parallelize = isset( $config[
'parallelize'] )
213 ? (string)$config[
'parallelize']
215 $this->concurrency = isset( $config[
'concurrency'] )
216 ? (int)$config[
'concurrency']
218 $this->obResetFunc = $config[
'obResetFunc'] ?? [ $this,
'resetOutputBuffer' ];
219 $this->streamMimeFunc = $config[
'streamMimeFunc'] ??
null;
221 $this->profiler = $config[
'profiler'] ??
null;
222 if ( !is_callable( $this->profiler ) ) {
223 $this->profiler =
null;
225 $this->logger = $config[
'logger'] ??
new NullLogger();
226 $this->statusWrapper = $config[
'statusWrapper'] ??
null;
228 if ( isset( $config[
'tmpDirectory'] ) ) {
278 return ( $this->readOnly !=
'' );
287 return ( $this->readOnly !=
'' ) ? $this->readOnly :
false;
309 return ( $this->
getFeatures() & $bitfield ) === $bitfield;
462 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
463 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
470 if ( empty( $opts[
'force'] ) ) {
471 unset( $opts[
'nonLocking'] );
475 $scope = ScopedCallback::newScopedIgnoreUserAbort();
499 final public function doOperation( array $op, array $opts = [] ) {
513 final public function create( array $params, array $opts = [] ) {
514 return $this->
doOperation( [
'op' =>
'create' ] + $params, $opts );
527 final public function store( array $params, array $opts = [] ) {
528 return $this->
doOperation( [
'op' =>
'store' ] + $params, $opts );
541 final public function copy( array $params, array $opts = [] ) {
542 return $this->
doOperation( [
'op' =>
'copy' ] + $params, $opts );
555 final public function move( array $params, array $opts = [] ) {
556 return $this->
doOperation( [
'op' =>
'move' ] + $params, $opts );
569 final public function delete( array $params, array $opts = [] ) {
570 return $this->
doOperation( [
'op' =>
'delete' ] + $params, $opts );
584 final public function describe( array $params, array $opts = [] ) {
585 return $this->
doOperation( [
'op' =>
'describe' ] + $params, $opts );
703 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
704 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
711 foreach ( $ops as &$op ) {
712 $op[
'overwrite'] =
true;
716 $scope = ScopedCallback::newScopedIgnoreUserAbort();
756 final public function quickCreate( array $params, array $opts = [] ) {
771 final public function quickStore( array $params, array $opts = [] ) {
786 final public function quickCopy( array $params, array $opts = [] ) {
801 final public function quickMove( array $params, array $opts = [] ) {
816 final public function quickDelete( array $params, array $opts = [] ) {
867 final public function prepare( array $params ) {
868 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
869 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
872 $scope = ScopedCallback::newScopedIgnoreUserAbort();
899 final public function secure( array $params ) {
900 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
901 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
904 $scope = ScopedCallback::newScopedIgnoreUserAbort();
913 abstract protected function doSecure( array $params );
933 final public function publish( array $params ) {
934 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
935 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
938 $scope = ScopedCallback::newScopedIgnoreUserAbort();
960 final public function clean( array $params ) {
961 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
962 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
965 $scope = ScopedCallback::newScopedIgnoreUserAbort();
966 return $this->
doClean( $params );
974 abstract protected function doClean( array $params );
1020 return $contents[$params[
'src']];
1155 return $fsFiles[$params[
'src']];
1190 $tmpFiles = $this->
getLocalCopyMulti( [
'srcs' => [ $params[
'src'] ] ] + $params );
1192 return $tmpFiles[$params[
'src']];
1342 return $this->
getFileList( [
'topOnly' =>
true ] + $params );
1393 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1395 return $this->
wrapStatus( $this->lockManager->lock( $paths,
$type, $timeout ) );
1406 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1408 return $this->
wrapStatus( $this->lockManager->unlock( $paths,
$type ) );
1430 if (
$type ===
'mixed' ) {
1431 foreach ( $paths as &$typePaths ) {
1432 $typePaths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $typePaths );
1435 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1467 return "mwstore://{$this->name}";
1491 foreach ( $ops as &$op ) {
1492 $src = $op[
'src'] ??
null;
1493 if ( $src instanceof
FSFile ) {
1494 $op[
'srcRef'] = $src;
1495 $op[
'src'] = $src->getPath();
1511 return ( strpos(
$path ??
'',
'mwstore://' ) === 0 );
1523 if ( self::isStoragePath( $storagePath ) ) {
1525 $parts = explode(
'/', substr( $storagePath, 10 ), 3 );
1526 if ( count( $parts ) >= 2 && $parts[0] !=
'' && $parts[1] !=
'' ) {
1527 if ( count( $parts ) == 3 ) {
1530 return [ $parts[0], $parts[1],
'' ];
1535 return [
null,
null, null ];
1547 if ( $relPath !==
null ) {
1549 if ( $relPath !==
null ) {
1550 return ( $relPath !=
'' )
1551 ?
"mwstore://{$backend}/{$container}/{$relPath}"
1552 :
"mwstore://{$backend}/{$container}";
1571 $storagePath = dirname( $storagePath );
1574 return ( $rel ===
null ) ? null : $storagePath;
1587 $i = strrpos(
$path,
'.' );
1588 $ext = $i ? substr(
$path, $i + 1 ) :
'';
1590 if ( $case ===
'lowercase' ) {
1592 } elseif ( $case ===
'uppercase' ) {
1607 return ( self::normalizeContainerPath(
$path ) !==
null );
1623 if ( !in_array(
$type, [
'inline',
'attachment' ] ) ) {
1624 throw new InvalidArgumentException(
"Invalid Content-Disposition type '$type'." );
1628 if ( strlen( $filename ) ) {
1629 $parts[] =
"filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1632 return implode(
';', $parts );
1649 $path = preg_replace(
'![/]{2,}!',
'/',
$path );
1653 if ( strpos(
$path,
'.' ) !==
false ) {
1657 strpos(
$path,
'./' ) === 0 ||
1658 strpos(
$path,
'../' ) === 0 ||
1659 strpos(
$path,
'/./' ) !==
false ||
1660 strpos(
$path,
'/../' ) !==
false
1678 if ( count(
$args ) ) {
1679 $sv = StatusValue::newFatal( ...
$args );
1681 $sv = StatusValue::newGood();
1692 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1709 while ( ob_get_status() ) {
1710 if ( !ob_end_clean() ) {
Class representing a non-directory file on the file system.
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.
create(array $params, array $opts=[])
Performs a single create operation.
quickDescribe(array $params, array $opts=[])
Performs a single quick describe 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.
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.
quickCreate(array $params, array $opts=[])
Performs a single quick create operation.
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.
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.
publish(array $params)
Remove measures to block web access to a storage directory and the container it belongs to.
quickDelete(array $params, array $opts=[])
Performs a single quick delete operation.
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.
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.
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()
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)
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.
resolveFSFileObjects(array $ops)
Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
quickMove(array $params, array $opts=[])
Performs a single quick move operation.
string $parallelize
When to do operations in parallel.
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.
__construct(array $config)
Create a new backend instance from configuration.
quickCopy(array $params, array $opts=[])
Performs a single quick copy operation.
static isPathTraversalFree( $path)
Check if a relative path has no directory traversals.
quickStore(array $params, array $opts=[])
Performs a single quick store operation.
doQuickOperation(array $op, array $opts=[])
Same as doQuickOperations() except it takes a single operation.
setLogger(LoggerInterface $logger)
doQuickOperationsInternal(array $ops, array $opts)
getLocalReferenceMulti(array $params)
Like getLocalReference() except it takes an array of storage paths and yields an order-preserved map ...
copy(array $params, array $opts=[])
Performs a single copy operation.
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.
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 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