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;
469 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
470 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
477 if ( empty( $opts[
'force'] ) ) {
478 unset( $opts[
'nonLocking'] );
482 $scope = ScopedCallback::newScopedIgnoreUserAbort();
506 final public function doOperation( array $op, array $opts = [] ) {
520 final public function create( array $params, array $opts = [] ) {
521 return $this->
doOperation( [
'op' =>
'create' ] + $params, $opts );
534 final public function store( array $params, array $opts = [] ) {
535 return $this->
doOperation( [
'op' =>
'store' ] + $params, $opts );
548 final public function copy( array $params, array $opts = [] ) {
549 return $this->
doOperation( [
'op' =>
'copy' ] + $params, $opts );
562 final public function move( array $params, array $opts = [] ) {
563 return $this->
doOperation( [
'op' =>
'move' ] + $params, $opts );
576 final public function delete( array $params, array $opts = [] ) {
577 return $this->
doOperation( [
'op' =>
'delete' ] + $params, $opts );
591 final public function describe( array $params, array $opts = [] ) {
592 return $this->
doOperation( [
'op' =>
'describe' ] + $params, $opts );
712 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
713 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
720 foreach ( $ops as &$op ) {
721 $op[
'overwrite'] =
true;
725 $scope = ScopedCallback::newScopedIgnoreUserAbort();
765 final public function quickCreate( array $params, array $opts = [] ) {
780 final public function quickStore( array $params, array $opts = [] ) {
795 final public function quickCopy( array $params, array $opts = [] ) {
810 final public function quickMove( array $params, array $opts = [] ) {
825 final public function quickDelete( array $params, array $opts = [] ) {
876 final public function prepare( array $params ) {
877 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
878 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
881 $scope = ScopedCallback::newScopedIgnoreUserAbort();
908 final public function secure( array $params ) {
909 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
910 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
913 $scope = ScopedCallback::newScopedIgnoreUserAbort();
922 abstract protected function doSecure( array $params );
942 final public function publish( array $params ) {
943 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
944 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
947 $scope = ScopedCallback::newScopedIgnoreUserAbort();
969 final public function clean( array $params ) {
970 if ( empty( $params[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
971 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
974 $scope = ScopedCallback::newScopedIgnoreUserAbort();
975 return $this->
doClean( $params );
983 abstract protected function doClean( array $params );
1029 return $contents[$params[
'src']];
1164 return $fsFiles[$params[
'src']];
1199 $tmpFiles = $this->
getLocalCopyMulti( [
'srcs' => [ $params[
'src'] ] ] + $params );
1201 return $tmpFiles[$params[
'src']];
1351 return $this->
getFileList( [
'topOnly' =>
true ] + $params );
1402 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1404 return $this->
wrapStatus( $this->lockManager->lock( $paths,
$type, $timeout ) );
1415 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1417 return $this->
wrapStatus( $this->lockManager->unlock( $paths,
$type ) );
1439 if (
$type ===
'mixed' ) {
1440 foreach ( $paths as &$typePaths ) {
1441 $typePaths = array_map(
'FileBackend::normalizeStoragePath', $typePaths );
1444 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1476 return "mwstore://{$this->name}";
1509 foreach ( $ops as &$op ) {
1510 $src = $op[
'src'] ??
null;
1511 if ( $src instanceof
FSFile ) {
1512 $op[
'srcRef'] = $src;
1513 $op[
'src'] = $src->getPath();
1529 return ( strpos(
$path ??
'',
'mwstore://' ) === 0 );
1541 if ( self::isStoragePath( $storagePath ) ) {
1543 $parts = explode(
'/', substr( $storagePath, 10 ), 3 );
1544 if ( count( $parts ) >= 2 && $parts[0] !=
'' && $parts[1] !=
'' ) {
1545 if ( count( $parts ) == 3 ) {
1548 return [ $parts[0], $parts[1],
'' ];
1553 return [
null,
null, null ];
1565 if ( $relPath !==
null ) {
1567 if ( $relPath !==
null ) {
1568 return ( $relPath !=
'' )
1569 ?
"mwstore://{$backend}/{$container}/{$relPath}"
1570 :
"mwstore://{$backend}/{$container}";
1589 $storagePath = dirname( $storagePath );
1592 return ( $rel ===
null ) ? null : $storagePath;
1605 $i = strrpos(
$path,
'.' );
1606 $ext = $i ? substr(
$path, $i + 1 ) :
'';
1608 if ( $case ===
'lowercase' ) {
1610 } elseif ( $case ===
'uppercase' ) {
1625 return ( self::normalizeContainerPath(
$path ) !==
null );
1641 if ( !in_array(
$type, [
'inline',
'attachment' ] ) ) {
1642 throw new InvalidArgumentException(
"Invalid Content-Disposition type '$type'." );
1646 if ( strlen( $filename ) ) {
1647 $parts[] =
"filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1650 return implode(
';', $parts );
1667 $path = preg_replace(
'![/]{2,}!',
'/',
$path );
1671 if ( strpos(
$path,
'.' ) !==
false ) {
1675 strpos(
$path,
'./' ) === 0 ||
1676 strpos(
$path,
'../' ) === 0 ||
1677 strpos(
$path,
'/./' ) !==
false ||
1678 strpos(
$path,
'/../' ) !==
false
1696 if ( count(
$args ) ) {
1697 $sv = StatusValue::newFatal( ...
$args );
1699 $sv = StatusValue::newGood();
1710 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1727 while ( ob_get_status() ) {
1728 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 Stable to override.
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.
resetOutputBuffer()
Let's not reset output buffering during tests.
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