31use Psr\Log\LoggerAwareInterface;
32use Psr\Log\LoggerInterface;
33use Wikimedia\ScopedCallback;
34use Psr\Log\NullLogger;
135 const STAT_ABSENT =
false;
138 const STAT_ERROR =
null;
140 const LIST_ERROR =
null;
142 const TEMPURL_ERROR =
null;
144 const EXISTENCE_ERROR =
null;
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;
191 $this->name = $config[
'name'];
192 $this->domainId = $config[
'domainId']
193 ?? $config[
'wikiId'];
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}'." );
200 $this->lockManager = $config[
'lockManager'] ??
new NullLockManager( [] );
201 $this->fileJournal = $config[
'fileJournal']
203 $this->readOnly = isset( $config[
'readOnly'] )
204 ? (string)$config[
'readOnly']
206 $this->parallelize = isset( $config[
'parallelize'] )
207 ? (string)$config[
'parallelize']
209 $this->concurrency = isset( $config[
'concurrency'] )
210 ? (int)$config[
'concurrency']
212 $this->obResetFunc = $config[
'obResetFunc'] ?? [ $this,
'resetOutputBuffer' ];
213 $this->streamMimeFunc = $config[
'streamMimeFunc'] ??
null;
214 $this->statusWrapper = $config[
'statusWrapper'] ??
null;
216 $this->profiler = $config[
'profiler'] ??
null;
217 if ( !is_callable( $this->profiler ) ) {
218 $this->profiler =
null;
220 $this->logger = $config[
'logger'] ??
new NullLogger();
221 $this->statusWrapper = $config[
'statusWrapper'] ??
null;
223 if ( isset( $config[
'tmpDirectory'] ) ) {
273 return ( $this->readOnly !=
'' );
282 return ( $this->readOnly !=
'' ) ? $this->readOnly :
false;
303 return ( $this->
getFeatures() & $bitfield ) === $bitfield;
459 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
460 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
467 if ( empty( $opts[
'force'] ) ) {
468 unset( $opts[
'nonLocking'] );
472 $scope = ScopedCallback::newScopedIgnoreUserAbort();
496 final public function doOperation( array $op, array $opts = [] ) {
510 final public function create( array $params, array $opts = [] ) {
511 return $this->
doOperation( [
'op' =>
'create' ] + $params, $opts );
524 final public function store( array $params, array $opts = [] ) {
525 return $this->
doOperation( [
'op' =>
'store' ] + $params, $opts );
538 final public function copy( array $params, array $opts = [] ) {
539 return $this->
doOperation( [
'op' =>
'copy' ] + $params, $opts );
552 final public function move( array $params, array $opts = [] ) {
553 return $this->
doOperation( [
'op' =>
'move' ] + $params, $opts );
566 final public function delete( array $params, array $opts = [] ) {
567 return $this->
doOperation( [
'op' =>
'delete' ] + $params, $opts );
581 final public function describe( array $params, array $opts = [] ) {
582 return $this->
doOperation( [
'op' =>
'describe' ] + $params, $opts );
700 if ( empty( $opts[
'bypassReadOnly'] ) && $this->
isReadOnly() ) {
701 return $this->
newStatus(
'backend-fail-readonly', $this->name, $this->readOnly );
708 foreach ( $ops as &$op ) {
709 $op[
'overwrite'] =
true;
713 $scope = ScopedCallback::newScopedIgnoreUserAbort();
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 );
861 $scope = ScopedCallback::newScopedIgnoreUserAbort();
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 );
893 $scope = ScopedCallback::newScopedIgnoreUserAbort();
902 abstract protected function doSecure( array $params );
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 );
927 $scope = ScopedCallback::newScopedIgnoreUserAbort();
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 );
954 $scope = ScopedCallback::newScopedIgnoreUserAbort();
955 return $this->
doClean( $params );
963 abstract protected function doClean( array $params );
1009 return $contents[$params[
'src']];
1144 return $fsFiles[$params[
'src']];
1179 $tmpFiles = $this->
getLocalCopyMulti( [
'srcs' => [ $params[
'src'] ] ] + $params );
1181 return $tmpFiles[$params[
'src']];
1331 return $this->
getFileList( [
'topOnly' =>
true ] + $params );
1382 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1384 return $this->
wrapStatus( $this->lockManager->lock( $paths,
$type, $timeout ) );
1395 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1397 return $this->
wrapStatus( $this->lockManager->unlock( $paths,
$type ) );
1419 if (
$type ===
'mixed' ) {
1420 foreach ( $paths as &$typePaths ) {
1421 $typePaths = array_map(
'FileBackend::normalizeStoragePath', $typePaths );
1424 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1456 return "mwstore://{$this->name}";
1489 foreach ( $ops as &$op ) {
1490 $src = $op[
'src'] ??
null;
1491 if ( $src instanceof
FSFile ) {
1492 $op[
'srcRef'] = $src;
1493 $op[
'src'] = $src->getPath();
1509 return ( strpos(
$path,
'mwstore://' ) === 0 );
1521 if ( self::isStoragePath( $storagePath ) ) {
1523 $parts = explode(
'/', substr( $storagePath, 10 ), 3 );
1524 if ( count( $parts ) >= 2 && $parts[0] !=
'' && $parts[1] !=
'' ) {
1525 if ( count( $parts ) == 3 ) {
1528 return [ $parts[0], $parts[1],
'' ];
1533 return [
null,
null, null ];
1545 if ( $relPath !==
null ) {
1547 if ( $relPath !==
null ) {
1548 return ( $relPath !=
'' )
1549 ?
"mwstore://{$backend}/{$container}/{$relPath}"
1550 :
"mwstore://{$backend}/{$container}";
1566 $storagePath = dirname( $storagePath );
1569 return ( $rel ===
null ) ? null : $storagePath;
1580 $i = strrpos(
$path,
'.' );
1581 $ext = $i ? substr(
$path, $i + 1 ) :
'';
1583 if ( $case ===
'lowercase' ) {
1585 } elseif ( $case ===
'uppercase' ) {
1600 return ( self::normalizeContainerPath(
$path ) !==
null );
1616 if ( !in_array(
$type, [
'inline',
'attachment' ] ) ) {
1617 throw new InvalidArgumentException(
"Invalid Content-Disposition type '$type'." );
1621 if ( strlen( $filename ) ) {
1622 $parts[] =
"filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1625 return implode(
';', $parts );
1642 $path = preg_replace(
'![/]{2,}!',
'/',
$path );
1646 if ( strpos(
$path,
'.' ) !==
false ) {
1650 strpos(
$path,
'./' ) === 0 ||
1651 strpos(
$path,
'../' ) === 0 ||
1652 strpos(
$path,
'/./' ) !==
false ||
1653 strpos(
$path,
'/../' ) !==
false
1671 if ( count(
$args ) ) {
1672 $sv = StatusValue::newFatal( ...
$args );
1674 $sv = StatusValue::newGood();
1685 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1697 while ( ob_get_status() ) {
1698 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.
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.
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.
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.
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)
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)
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.
static isPathTraversalFree( $path)
Check if a relative path has no directory traversals.
doQuickOperation(array $op)
Same as doQuickOperations() except it takes a single operation.
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.
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