MediaWiki master
FileBackend.php
Go to the documentation of this file.
1<?php
32use Psr\Log\LoggerAwareInterface;
33use Psr\Log\LoggerInterface;
34use Psr\Log\NullLogger;
35use Wikimedia\ScopedCallback;
36
99abstract class FileBackend implements LoggerAwareInterface {
101 protected $name;
102
104 protected $domainId;
105
107 protected $readOnly;
108
110 protected $parallelize;
111
113 protected $concurrency;
114
117
119 protected $lockManager;
121 protected $logger;
123 protected $profiler;
124
126 protected $obResetFunc;
130 protected $statusWrapper;
131
133 public const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
134 public const ATTR_METADATA = 2; // files can be stored with metadata key/values
135 public const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
136
138 protected const STAT_ABSENT = false;
139
141 public const STAT_ERROR = null;
143 public const LIST_ERROR = null;
145 public const TEMPURL_ERROR = null;
147 public const EXISTENCE_ERROR = null;
148
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;
159
193 public function __construct( array $config ) {
194 if ( !array_key_exists( 'name', $config ) ) {
195 throw new InvalidArgumentException( 'Backend name not specified.' );
196 }
197 $this->name = $config['name'];
198 $this->domainId = $config['domainId'] // e.g. "my_wiki-en_"
199 ?? $config['wikiId'] // b/c alias
200 ?? null;
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." );
203 }
204 if ( !is_string( $this->domainId ) ) {
205 throw new InvalidArgumentException(
206 "Backend domain ID not provided for '{$this->name}'." );
207 }
208 $this->lockManager = $config['lockManager'] ?? new NullLockManager( [] );
209 $this->readOnly = isset( $config['readOnly'] )
210 ? (string)$config['readOnly']
211 : '';
212 $this->parallelize = isset( $config['parallelize'] )
213 ? (string)$config['parallelize']
214 : 'off';
215 $this->concurrency = isset( $config['concurrency'] )
216 ? (int)$config['concurrency']
217 : 50;
218 $this->obResetFunc = $config['obResetFunc'] ?? [ $this, 'resetOutputBuffer' ];
219 $this->streamMimeFunc = $config['streamMimeFunc'] ?? null;
220
221 $this->profiler = $config['profiler'] ?? null;
222 if ( !is_callable( $this->profiler ) ) {
223 $this->profiler = null;
224 }
225 $this->logger = $config['logger'] ?? new NullLogger();
226 $this->statusWrapper = $config['statusWrapper'] ?? null;
227 // tmpDirectory gets precedence for backward compatibility
228 if ( isset( $config['tmpDirectory'] ) ) {
229 $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] );
230 } else {
231 $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory();
232 }
233 }
234
235 public function setLogger( LoggerInterface $logger ) {
236 $this->logger = $logger;
237 }
238
247 final public function getName() {
248 return $this->name;
249 }
250
257 final public function getDomainId() {
258 return $this->domainId;
259 }
260
268 final public function getWikiId() {
269 return $this->getDomainId();
270 }
271
277 final public function isReadOnly() {
278 return ( $this->readOnly != '' );
279 }
280
286 final public function getReadOnlyReason() {
287 return ( $this->readOnly != '' ) ? $this->readOnly : false;
288 }
289
297 public function getFeatures() {
299 }
300
308 final public function hasFeatures( $bitfield ) {
309 return ( $this->getFeatures() & $bitfield ) === $bitfield;
310 }
311
461 final public function doOperations( array $ops, array $opts = [] ) {
462 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
463 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
464 }
465 if ( $ops === [] ) {
466 return $this->newStatus(); // nothing to do
467 }
468
469 $ops = $this->resolveFSFileObjects( $ops );
470 if ( empty( $opts['force'] ) ) {
471 unset( $opts['nonLocking'] );
472 }
473
475 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
476
477 return $this->doOperationsInternal( $ops, $opts );
478 }
479
486 abstract protected function doOperationsInternal( array $ops, array $opts );
487
499 final public function doOperation( array $op, array $opts = [] ) {
500 return $this->doOperations( [ $op ], $opts );
501 }
502
513 final public function create( array $params, array $opts = [] ) {
514 return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
515 }
516
527 final public function store( array $params, array $opts = [] ) {
528 return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
529 }
530
541 final public function copy( array $params, array $opts = [] ) {
542 return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
543 }
544
555 final public function move( array $params, array $opts = [] ) {
556 return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
557 }
558
569 final public function delete( array $params, array $opts = [] ) {
570 return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
571 }
572
584 final public function describe( array $params, array $opts = [] ) {
585 return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
586 }
587
702 final public function doQuickOperations( array $ops, array $opts = [] ) {
703 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
704 return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
705 }
706 if ( $ops === [] ) {
707 return $this->newStatus(); // nothing to do
708 }
709
710 $ops = $this->resolveFSFileObjects( $ops );
711 foreach ( $ops as &$op ) {
712 $op['overwrite'] = true; // avoids RTTs in key/value stores
713 }
714
716 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
717
718 return $this->doQuickOperationsInternal( $ops, $opts );
719 }
720
728 abstract protected function doQuickOperationsInternal( array $ops, array $opts );
729
741 final public function doQuickOperation( array $op, array $opts = [] ) {
742 return $this->doQuickOperations( [ $op ], $opts );
743 }
744
756 final public function quickCreate( array $params, array $opts = [] ) {
757 return $this->doQuickOperation( [ 'op' => 'create' ] + $params, $opts );
758 }
759
771 final public function quickStore( array $params, array $opts = [] ) {
772 return $this->doQuickOperation( [ 'op' => 'store' ] + $params, $opts );
773 }
774
786 final public function quickCopy( array $params, array $opts = [] ) {
787 return $this->doQuickOperation( [ 'op' => 'copy' ] + $params, $opts );
788 }
789
801 final public function quickMove( array $params, array $opts = [] ) {
802 return $this->doQuickOperation( [ 'op' => 'move' ] + $params, $opts );
803 }
804
816 final public function quickDelete( array $params, array $opts = [] ) {
817 return $this->doQuickOperation( [ 'op' => 'delete' ] + $params, $opts );
818 }
819
831 final public function quickDescribe( array $params, array $opts = [] ) {
832 return $this->doQuickOperation( [ 'op' => 'describe' ] + $params, $opts );
833 }
834
847 abstract public function concatenate( array $params );
848
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 );
870 }
872 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
873 return $this->doPrepare( $params );
874 }
875
881 abstract protected function doPrepare( array $params );
882
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 );
902 }
904 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
905 return $this->doSecure( $params );
906 }
907
913 abstract protected function doSecure( array $params );
914
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 );
936 }
938 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
939 return $this->doPublish( $params );
940 }
941
947 abstract protected function doPublish( array $params );
948
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 );
963 }
965 $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
966 return $this->doClean( $params );
967 }
968
974 abstract protected function doClean( array $params );
975
992 abstract public function fileExists( array $params );
993
1004 abstract public function getFileTimestamp( array $params );
1005
1017 final public function getFileContents( array $params ) {
1018 $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1019
1020 return $contents[$params['src']];
1021 }
1022
1036 abstract public function getFileContentsMulti( array $params );
1037
1058 abstract public function getFileXAttributes( array $params );
1059
1070 abstract public function getFileSize( array $params );
1071
1088 abstract public function getFileStat( array $params );
1089
1100 abstract public function getFileSha1Base36( array $params );
1101
1111 abstract public function getFileProps( array $params );
1112
1132 abstract public function streamFile( array $params );
1133
1152 final public function getLocalReference( array $params ) {
1153 $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1154
1155 return $fsFiles[$params['src']];
1156 }
1157
1175 abstract public function getLocalReferenceMulti( array $params );
1176
1189 final public function getLocalCopy( array $params ) {
1190 $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
1191
1192 return $tmpFiles[$params['src']];
1193 }
1194
1210 abstract public function getLocalCopyMulti( array $params );
1211
1230 abstract public function getFileHttpUrl( array $params );
1231
1257 abstract public function directoryExists( array $params );
1258
1281 abstract public function getDirectoryList( array $params );
1282
1299 final public function getTopDirectoryList( array $params ) {
1300 return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
1301 }
1302
1324 abstract public function getFileList( array $params );
1325
1342 final public function getTopFileList( array $params ) {
1343 return $this->getFileList( [ 'topOnly' => true ] + $params );
1344 }
1345
1354 abstract public function preloadCache( array $paths );
1355
1364 abstract public function clearCache( array $paths = null );
1365
1380 abstract public function preloadFileStat( array $params );
1381
1393 final public function lockFiles( array $paths, $type, $timeout = 0 ) {
1394 $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1395
1396 return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) );
1397 }
1398
1406 final public function unlockFiles( array $paths, $type ) {
1407 $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1408
1409 return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) );
1410 }
1411
1428 final public function getScopedFileLocks(
1429 array $paths, $type, StatusValue $status, $timeout = 0
1430 ) {
1431 if ( $type === 'mixed' ) {
1432 foreach ( $paths as &$typePaths ) {
1433 $typePaths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $typePaths );
1434 }
1435 } else {
1436 $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths );
1437 }
1438
1439 return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
1440 }
1441
1458 abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
1459
1467 final public function getRootStoragePath() {
1468 return "mwstore://{$this->name}";
1469 }
1470
1478 final public function getContainerStoragePath( $container ) {
1479 return $this->getRootStoragePath() . "/{$container}";
1480 }
1481
1491 protected function resolveFSFileObjects( array $ops ) {
1492 foreach ( $ops as &$op ) {
1493 $src = $op['src'] ?? null;
1494 if ( $src instanceof FSFile ) {
1495 $op['srcRef'] = $src;
1496 $op['src'] = $src->getPath();
1497 }
1498 }
1499 unset( $op );
1500
1501 return $ops;
1502 }
1503
1511 final public static function isStoragePath( $path ) {
1512 return ( strpos( $path ?? '', 'mwstore://' ) === 0 );
1513 }
1514
1523 final public static function splitStoragePath( $storagePath ) {
1524 if ( self::isStoragePath( $storagePath ) ) {
1525 // Remove the "mwstore://" prefix and split the path
1526 $parts = explode( '/', substr( $storagePath, 10 ), 3 );
1527 if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
1528 if ( count( $parts ) == 3 ) {
1529 return $parts; // e.g. "backend/container/path"
1530 } else {
1531 return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
1532 }
1533 }
1534 }
1535
1536 return [ null, null, null ];
1537 }
1538
1546 final public static function normalizeStoragePath( $storagePath ) {
1547 [ $backend, $container, $relPath ] = self::splitStoragePath( $storagePath );
1548 if ( $relPath !== null ) { // must be for this backend
1549 $relPath = self::normalizeContainerPath( $relPath );
1550 if ( $relPath !== null ) {
1551 return ( $relPath != '' )
1552 ? "mwstore://{$backend}/{$container}/{$relPath}"
1553 : "mwstore://{$backend}/{$container}";
1554 }
1555 }
1556
1557 return null;
1558 }
1559
1568 final public static function parentStoragePath( $storagePath ) {
1569 // XXX dirname() depends on platform and locale! If nothing enforces that the storage path
1570 // doesn't contain characters like '\', behavior can vary by platform. We should use
1571 // explode() instead.
1572 $storagePath = dirname( $storagePath );
1573 [ , , $rel ] = self::splitStoragePath( $storagePath );
1574
1575 return ( $rel === null ) ? null : $storagePath;
1576 }
1577
1585 final public static function extensionFromPath( $path, $case = 'lowercase' ) {
1586 // This will treat a string starting with . as not having an extension, but store paths have
1587 // to start with 'mwstore://', so "garbage in, garbage out".
1588 $i = strrpos( $path, '.' );
1589 $ext = $i ? substr( $path, $i + 1 ) : '';
1590
1591 if ( $case === 'lowercase' ) {
1592 $ext = strtolower( $ext );
1593 } elseif ( $case === 'uppercase' ) {
1594 $ext = strtoupper( $ext );
1595 }
1596
1597 return $ext;
1598 }
1599
1607 final public static function isPathTraversalFree( $path ) {
1608 return ( self::normalizeContainerPath( $path ) !== null );
1609 }
1610
1620 final public static function makeContentDisposition( $type, $filename = '' ) {
1621 $parts = [];
1622
1623 $type = strtolower( $type );
1624 if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
1625 throw new InvalidArgumentException( "Invalid Content-Disposition type '$type'." );
1626 }
1627 $parts[] = $type;
1628
1629 if ( strlen( $filename ) ) {
1630 $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1631 }
1632
1633 return implode( ';', $parts );
1634 }
1635
1646 final protected static function normalizeContainerPath( $path ) {
1647 // Normalize directory separators
1648 $path = strtr( $path, '\\', '/' );
1649 // Collapse any consecutive directory separators
1650 $path = preg_replace( '![/]{2,}!', '/', $path );
1651 // Remove any leading directory separator
1652 $path = ltrim( $path, '/' );
1653 // Use the same traversal protection as Title::secureAndSplit()
1654 if ( strpos( $path, '.' ) !== false ) {
1655 if (
1656 $path === '.' ||
1657 $path === '..' ||
1658 strpos( $path, './' ) === 0 ||
1659 strpos( $path, '../' ) === 0 ||
1660 strpos( $path, '/./' ) !== false ||
1661 strpos( $path, '/../' ) !== false
1662 ) {
1663 return null;
1664 }
1665 }
1666
1667 return $path;
1668 }
1669
1678 final protected function newStatus( ...$args ) {
1679 if ( count( $args ) ) {
1680 $sv = StatusValue::newFatal( ...$args );
1681 } else {
1682 $sv = StatusValue::newGood();
1683 }
1684
1685 return $this->wrapStatus( $sv );
1686 }
1687
1692 final protected function wrapStatus( StatusValue $sv ) {
1693 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1694 }
1695
1700 protected function scopedProfileSection( $section ) {
1701 return $this->profiler ? ( $this->profiler )( $section ) : null;
1702 }
1703
1707 protected function resetOutputBuffer() {
1708 // XXX According to documentation, ob_get_status() always returns a non-empty array and this
1709 // condition will always be true
1710 while ( ob_get_status() ) {
1711 if ( !ob_end_clean() ) {
1712 // Could not remove output buffer handler; abort now
1713 // to avoid getting in some kind of infinite loop.
1714 break;
1715 }
1716 }
1717 }
1718}
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.
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.
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.
callable null $statusWrapper
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.
quickDelete(array $params, array $opts=[])
Performs a single quick delete operation.
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.
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)
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.
resolveFSFileObjects(array $ops)
Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
callable null $streamMimeFunc
quickMove(array $params, array $opts=[])
Performs a single quick move operation.
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.
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.
doSecure(array $params)
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.
Resource locking handling.
Simple lock management based on in-process 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.