31use Psr\Log\LoggerAwareInterface;
32use Psr\Log\LoggerInterface;
33use Psr\Log\NullLogger;
34use Wikimedia\ScopedCallback;
139 protected const STAT_ABSENT =
false;
142 public const STAT_ERROR =
null;
144 public const LIST_ERROR =
null;
146 public const TEMPURL_ERROR =
null;
148 public const EXISTENCE_ERROR =
null;
151 public const TIMESTAMP_FAIL =
false;
153 public const CONTENT_FAIL =
false;
155 public const XATTRS_FAIL =
false;
157 public const SIZE_FAIL =
false;
159 public const SHA1_FAIL =
false;
197 if ( !array_key_exists(
'name', $config ) ) {
198 throw new InvalidArgumentException(
'Backend name not specified.' );
200 $this->name = $config[
'name'];
201 $this->domainId = $config[
'domainId']
204 if ( !preg_match(
'!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
205 throw new InvalidArgumentException(
"Backend name '{$this->name}' is invalid." );
207 if ( !is_string( $this->domainId ) ) {
208 throw new InvalidArgumentException(
209 "Backend domain ID not provided for '{$this->name}'." );
211 $this->lockManager = $config[
'lockManager'] ??
new NullLockManager( [] );
213 $this->readOnly = isset( $config[
'readOnly'] )
214 ? (string)$config[
'readOnly']
216 $this->parallelize = isset( $config[
'parallelize'] )
217 ? (string)$config[
'parallelize']
219 $this->concurrency = isset( $config[
'concurrency'] )
220 ? (int)$config[
'concurrency']
222 $this->obResetFunc = $config[
'obResetFunc'] ?? [ $this,
'resetOutputBuffer' ];
223 $this->streamMimeFunc = $config[
'streamMimeFunc'] ??
null;
225 $this->profiler = $config[
'profiler'] ??
null;
226 if ( !is_callable( $this->profiler ) ) {
227 $this->profiler =
null;
229 $this->logger = $config[
'logger'] ??
new NullLogger();
230 $this->statusWrapper = $config[
'statusWrapper'] ??
null;
232 if ( isset( $config[
'tmpDirectory'] ) ) {
282 return ( $this->readOnly !=
'' );
291 return ( $this->readOnly !=
'' ) ? $this->readOnly :
false;
313 return ( $this->
getFeatures() & $bitfield ) === $bitfield;
468 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
469 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
476 if ( empty( $opts[
'force'] ) ) {
477 unset( $opts[
'nonLocking'] );
481 $scope = ScopedCallback::newScopedIgnoreUserAbort();
505 final public function doOperation( array $op, array $opts = [] ) {
519 final public function create( array $params, array $opts = [] ) {
520 return $this->
doOperation( [
'op' =>
'create' ] + $params, $opts );
533 final public function store( array $params, array $opts = [] ) {
534 return $this->
doOperation( [
'op' =>
'store' ] + $params, $opts );
547 final public function copy( array $params, array $opts = [] ) {
548 return $this->
doOperation( [
'op' =>
'copy' ] + $params, $opts );
561 final public function move( array $params, array $opts = [] ) {
562 return $this->
doOperation( [
'op' =>
'move' ] + $params, $opts );
575 final public function delete( array $params, array $opts = [] ) {
576 return $this->
doOperation( [
'op' =>
'delete' ] + $params, $opts );
590 final public function describe( array $params, array $opts = [] ) {
591 return $this->
doOperation( [
'op' =>
'describe' ] + $params, $opts );
709 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
710 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
717 foreach ( $ops as &$op ) {
718 $op[
'overwrite'] =
true;
722 $scope = ScopedCallback::newScopedIgnoreUserAbort();
762 final public function quickCreate( array $params, array $opts = [] ) {
777 final public function quickStore( array $params, array $opts = [] ) {
792 final public function quickCopy( array $params, array $opts = [] ) {
807 final public function quickMove( array $params, array $opts = [] ) {
822 final public function quickDelete( array $params, array $opts = [] ) {
873 final public function prepare( array $params ) {
874 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
875 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
878 $scope = ScopedCallback::newScopedIgnoreUserAbort();
905 final public function secure( array $params ) {
906 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
907 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
910 $scope = ScopedCallback::newScopedIgnoreUserAbort();
919 abstract protected function doSecure( array $params );
939 final public function publish( array $params ) {
940 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
941 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
944 $scope = ScopedCallback::newScopedIgnoreUserAbort();
966 final public function clean( array $params ) {
967 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
968 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
971 $scope = ScopedCallback::newScopedIgnoreUserAbort();
972 return $this->
doClean( $params );
980 abstract protected function doClean( array $params );
1026 return $contents[$params[
'src']];
1161 return $fsFiles[$params[
'src']];
1196 $tmpFiles = $this->
getLocalCopyMulti( [
'srcs' => [ $params[
'src'] ] ] + $params );
1198 return $tmpFiles[$params[
'src']];
1348 return $this->
getFileList( [
'topOnly' =>
true ] + $params );
1399 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1401 return $this->
wrapStatus( $this->lockManager->lock( $paths,
$type, $timeout ) );
1412 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1414 return $this->
wrapStatus( $this->lockManager->unlock( $paths,
$type ) );
1436 if (
$type ===
'mixed' ) {
1437 foreach ( $paths as &$typePaths ) {
1438 $typePaths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $typePaths );
1441 $paths = array_map( [ __CLASS__,
'normalizeStoragePath' ], $paths );
1473 return "mwstore://{$this->name}";
1506 foreach ( $ops as &$op ) {
1507 $src = $op[
'src'] ??
null;
1508 if ( $src instanceof
FSFile ) {
1509 $op[
'srcRef'] = $src;
1510 $op[
'src'] = $src->getPath();
1526 return ( strpos(
$path ??
'',
'mwstore://' ) === 0 );
1538 if ( self::isStoragePath( $storagePath ) ) {
1540 $parts = explode(
'/', substr( $storagePath, 10 ), 3 );
1541 if ( count( $parts ) >= 2 && $parts[0] !=
'' && $parts[1] !=
'' ) {
1542 if ( count( $parts ) == 3 ) {
1545 return [ $parts[0], $parts[1],
'' ];
1550 return [
null,
null, null ];
1562 if ( $relPath !==
null ) {
1564 if ( $relPath !==
null ) {
1565 return ( $relPath !=
'' )
1566 ?
"mwstore://{$backend}/{$container}/{$relPath}"
1567 :
"mwstore://{$backend}/{$container}";
1586 $storagePath = dirname( $storagePath );
1589 return ( $rel ===
null ) ? null : $storagePath;
1602 $i = strrpos(
$path,
'.' );
1603 $ext = $i ? substr(
$path, $i + 1 ) :
'';
1605 if ( $case ===
'lowercase' ) {
1607 } elseif ( $case ===
'uppercase' ) {
1622 return ( self::normalizeContainerPath(
$path ) !==
null );
1638 if ( !in_array(
$type, [
'inline',
'attachment' ] ) ) {
1639 throw new InvalidArgumentException(
"Invalid Content-Disposition type '$type'." );
1643 if ( strlen( $filename ) ) {
1644 $parts[] =
"filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1647 return implode(
';', $parts );
1664 $path = preg_replace(
'![/]{2,}!',
'/',
$path );
1668 if ( strpos(
$path,
'.' ) !==
false ) {
1672 strpos(
$path,
'./' ) === 0 ||
1673 strpos(
$path,
'../' ) === 0 ||
1674 strpos(
$path,
'/./' ) !==
false ||
1675 strpos(
$path,
'/../' ) !==
false
1693 if ( count(
$args ) ) {
1694 $sv = StatusValue::newFatal( ...
$args );
1696 $sv = StatusValue::newGood();
1707 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1724 while ( ob_get_status() ) {
1725 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.
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.
Class for handling resource locking.
Simple version of FileJournal that does nothing.
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