]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/libs/filebackend/FileBackend.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / libs / filebackend / FileBackend.php
1 <?php
2 /**
3  * @defgroup FileBackend File backend
4  *
5  * File backend is used to interact with file storage systems,
6  * such as the local file system, NFS, or cloud storage systems.
7  */
8
9 /**
10  * Base class for all file backends.
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License along
23  * with this program; if not, write to the Free Software Foundation, Inc.,
24  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25  * http://www.gnu.org/copyleft/gpl.html
26  *
27  * @file
28  * @ingroup FileBackend
29  */
30 use Psr\Log\LoggerAwareInterface;
31 use Psr\Log\LoggerInterface;
32 use Wikimedia\ScopedCallback;
33
34 /**
35  * @brief Base class for all file backend classes (including multi-write backends).
36  *
37  * This class defines the methods as abstract that subclasses must implement.
38  * Outside callers can assume that all backends will have these functions.
39  *
40  * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
41  * The "backend" portion is unique name for the application to refer to a backend, while
42  * the "container" portion is a top-level directory of the backend. The "path" portion
43  * is a relative path that uses UNIX file system (FS) notation, though any particular
44  * backend may not actually be using a local filesystem. Therefore, the relative paths
45  * are only virtual.
46  *
47  * Backend contents are stored under "domain"-specific container names by default.
48  * A domain is simply a logical umbrella for entities, such as those belonging to a certain
49  * application or portion of a website, for example. A domain can be local or global.
50  * Global (qualified) backends are achieved by configuring the "domain ID" to a constant.
51  * Global domains are simpler, but local domains can be used by choosing a domain ID based on
52  * the current context, such as which language of a website is being used.
53  *
54  * For legacy reasons, the FSFileBackend class allows manually setting the paths of
55  * containers to ones that do not respect the "domain ID".
56  *
57  * In key/value (object) stores, containers are the only hierarchy (the rest is emulated).
58  * FS-based backends are somewhat more restrictive due to the existence of real
59  * directory files; a regular file cannot have the same name as a directory. Other
60  * backends with virtual directories may not have this limitation. Callers should
61  * store files in such a way that no files and directories are under the same path.
62  *
63  * In general, this class allows for callers to access storage through the same
64  * interface, without regard to the underlying storage system. However, calling code
65  * must follow certain patterns and be aware of certain things to ensure compatibility:
66  *   - a) Always call prepare() on the parent directory before trying to put a file there;
67  *        key/value stores only need the container to exist first, but filesystems need
68  *        all the parent directories to exist first (prepare() is aware of all this)
69  *   - b) Always call clean() on a directory when it might become empty to avoid empty
70  *        directory buildup on filesystems; key/value stores never have empty directories,
71  *        so doing this helps preserve consistency in both cases
72  *   - c) Likewise, do not rely on the existence of empty directories for anything;
73  *        calling directoryExists() on a path that prepare() was previously called on
74  *        will return false for key/value stores if there are no files under that path
75  *   - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
76  *        either be a copy of the source file in /tmp or the original source file itself
77  *   - e) Use a file layout that results in never attempting to store files over directories
78  *        or directories over files; key/value stores allow this but filesystems do not
79  *   - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
80  *   - g) Do not assume that move operations are atomic (difficult with key/value stores)
81  *   - h) Do not assume that file stat or read operations always have immediate consistency;
82  *        various methods have a "latest" flag that should always be used if up-to-date
83  *        information is required (this trades performance for correctness as needed)
84  *   - i) Do not assume that directory listings have immediate consistency
85  *
86  * Methods of subclasses should avoid throwing exceptions at all costs.
87  * As a corollary, external dependencies should be kept to a minimum.
88  *
89  * @ingroup FileBackend
90  * @since 1.19
91  */
92 abstract class FileBackend implements LoggerAwareInterface {
93         /** @var string Unique backend name */
94         protected $name;
95
96         /** @var string Unique domain name */
97         protected $domainId;
98
99         /** @var string Read-only explanation message */
100         protected $readOnly;
101
102         /** @var string When to do operations in parallel */
103         protected $parallelize;
104
105         /** @var int How many operations can be done in parallel */
106         protected $concurrency;
107
108         /** @var string Temporary file directory */
109         protected $tmpDirectory;
110
111         /** @var LockManager */
112         protected $lockManager;
113         /** @var FileJournal */
114         protected $fileJournal;
115         /** @var LoggerInterface */
116         protected $logger;
117         /** @var object|string Class name or object With profileIn/profileOut methods */
118         protected $profiler;
119
120         /** @var callable */
121         protected $obResetFunc;
122         /** @var callable */
123         protected $streamMimeFunc;
124         /** @var callable */
125         protected $statusWrapper;
126
127         /** Bitfield flags for supported features */
128         const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
129         const ATTR_METADATA = 2; // files can be stored with metadata key/values
130         const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
131
132         /**
133          * Create a new backend instance from configuration.
134          * This should only be called from within FileBackendGroup.
135          *
136          * @param array $config Parameters include:
137          *   - name : The unique name of this backend.
138          *      This should consist of alphanumberic, '-', and '_' characters.
139          *      This name should not be changed after use (e.g. with journaling).
140          *      Note that the name is *not* used in actual container names.
141          *   - domainId : Prefix to container names that is unique to this backend.
142          *      It should only consist of alphanumberic, '-', and '_' characters.
143          *      This ID is what avoids collisions if multiple logical backends
144          *      use the same storage system, so this should be set carefully.
145          *   - lockManager : LockManager object to use for any file locking.
146          *      If not provided, then no file locking will be enforced.
147          *   - fileJournal : FileJournal object to use for logging changes to files.
148          *      If not provided, then change journaling will be disabled.
149          *   - readOnly : Write operations are disallowed if this is a non-empty string.
150          *      It should be an explanation for the backend being read-only.
151          *   - parallelize : When to do file operations in parallel (when possible).
152          *      Allowed values are "implicit", "explicit" and "off".
153          *   - concurrency : How many file operations can be done in parallel.
154          *   - tmpDirectory : Directory to use for temporary files. If this is not set or null,
155          *      then the backend will try to discover a usable temporary directory.
156          *   - obResetFunc : alternative callback to clear the output buffer
157          *   - streamMimeFunc : alternative method to determine the content type from the path
158          *   - logger : Optional PSR logger object.
159          *   - profiler : Optional class name or object With profileIn/profileOut methods.
160          * @throws InvalidArgumentException
161          */
162         public function __construct( array $config ) {
163                 $this->name = $config['name'];
164                 $this->domainId = isset( $config['domainId'] )
165                         ? $config['domainId'] // e.g. "my_wiki-en_"
166                         : $config['wikiId']; // b/c alias
167                 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
168                         throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." );
169                 } elseif ( !is_string( $this->domainId ) ) {
170                         throw new InvalidArgumentException(
171                                 "Backend domain ID not provided for '{$this->name}'." );
172                 }
173                 $this->lockManager = isset( $config['lockManager'] )
174                         ? $config['lockManager']
175                         : new NullLockManager( [] );
176                 $this->fileJournal = isset( $config['fileJournal'] )
177                         ? $config['fileJournal']
178                         : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $this->name );
179                 $this->readOnly = isset( $config['readOnly'] )
180                         ? (string)$config['readOnly']
181                         : '';
182                 $this->parallelize = isset( $config['parallelize'] )
183                         ? (string)$config['parallelize']
184                         : 'off';
185                 $this->concurrency = isset( $config['concurrency'] )
186                         ? (int)$config['concurrency']
187                         : 50;
188                 $this->obResetFunc = isset( $config['obResetFunc'] )
189                         ? $config['obResetFunc']
190                         : [ $this, 'resetOutputBuffer' ];
191                 $this->streamMimeFunc = isset( $config['streamMimeFunc'] )
192                         ? $config['streamMimeFunc']
193                         : null;
194                 $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
195
196                 $this->profiler = isset( $config['profiler'] ) ? $config['profiler'] : null;
197                 $this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger();
198                 $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
199                 $this->tmpDirectory = isset( $config['tmpDirectory'] ) ? $config['tmpDirectory'] : null;
200         }
201
202         public function setLogger( LoggerInterface $logger ) {
203                 $this->logger = $logger;
204         }
205
206         /**
207          * Get the unique backend name.
208          * We may have multiple different backends of the same type.
209          * For example, we can have two Swift backends using different proxies.
210          *
211          * @return string
212          */
213         final public function getName() {
214                 return $this->name;
215         }
216
217         /**
218          * Get the domain identifier used for this backend (possibly empty).
219          *
220          * @return string
221          * @since 1.28
222          */
223         final public function getDomainId() {
224                 return $this->domainId;
225         }
226
227         /**
228          * Alias to getDomainId()
229          * @return string
230          * @since 1.20
231          */
232         final public function getWikiId() {
233                 return $this->getDomainId();
234         }
235
236         /**
237          * Check if this backend is read-only
238          *
239          * @return bool
240          */
241         final public function isReadOnly() {
242                 return ( $this->readOnly != '' );
243         }
244
245         /**
246          * Get an explanatory message if this backend is read-only
247          *
248          * @return string|bool Returns false if the backend is not read-only
249          */
250         final public function getReadOnlyReason() {
251                 return ( $this->readOnly != '' ) ? $this->readOnly : false;
252         }
253
254         /**
255          * Get the a bitfield of extra features supported by the backend medium
256          *
257          * @return int Bitfield of FileBackend::ATTR_* flags
258          * @since 1.23
259          */
260         public function getFeatures() {
261                 return self::ATTR_UNICODE_PATHS;
262         }
263
264         /**
265          * Check if the backend medium supports a field of extra features
266          *
267          * @param int $bitfield Bitfield of FileBackend::ATTR_* flags
268          * @return bool
269          * @since 1.23
270          */
271         final public function hasFeatures( $bitfield ) {
272                 return ( $this->getFeatures() & $bitfield ) === $bitfield;
273         }
274
275         /**
276          * This is the main entry point into the backend for write operations.
277          * Callers supply an ordered list of operations to perform as a transaction.
278          * Files will be locked, the stat cache cleared, and then the operations attempted.
279          * If any serious errors occur, all attempted operations will be rolled back.
280          *
281          * $ops is an array of arrays. The outer array holds a list of operations.
282          * Each inner array is a set of key value pairs that specify an operation.
283          *
284          * Supported operations and their parameters. The supported actions are:
285          *  - create
286          *  - store
287          *  - copy
288          *  - move
289          *  - delete
290          *  - describe (since 1.21)
291          *  - null
292          *
293          * FSFile/TempFSFile object support was added in 1.27.
294          *
295          * a) Create a new file in storage with the contents of a string
296          * @code
297          *     [
298          *         'op'                  => 'create',
299          *         'dst'                 => <storage path>,
300          *         'content'             => <string of new file contents>,
301          *         'overwrite'           => <boolean>,
302          *         'overwriteSame'       => <boolean>,
303          *         'headers'             => <HTTP header name/value map> # since 1.21
304          *     ]
305          * @endcode
306          *
307          * b) Copy a file system file into storage
308          * @code
309          *     [
310          *         'op'                  => 'store',
311          *         'src'                 => <file system path, FSFile, or TempFSFile>,
312          *         'dst'                 => <storage path>,
313          *         'overwrite'           => <boolean>,
314          *         'overwriteSame'       => <boolean>,
315          *         'headers'             => <HTTP header name/value map> # since 1.21
316          *     ]
317          * @endcode
318          *
319          * c) Copy a file within storage
320          * @code
321          *     [
322          *         'op'                  => 'copy',
323          *         'src'                 => <storage path>,
324          *         'dst'                 => <storage path>,
325          *         'overwrite'           => <boolean>,
326          *         'overwriteSame'       => <boolean>,
327          *         'ignoreMissingSource' => <boolean>, # since 1.21
328          *         'headers'             => <HTTP header name/value map> # since 1.21
329          *     ]
330          * @endcode
331          *
332          * d) Move a file within storage
333          * @code
334          *     [
335          *         'op'                  => 'move',
336          *         'src'                 => <storage path>,
337          *         'dst'                 => <storage path>,
338          *         'overwrite'           => <boolean>,
339          *         'overwriteSame'       => <boolean>,
340          *         'ignoreMissingSource' => <boolean>, # since 1.21
341          *         'headers'             => <HTTP header name/value map> # since 1.21
342          *     ]
343          * @endcode
344          *
345          * e) Delete a file within storage
346          * @code
347          *     [
348          *         'op'                  => 'delete',
349          *         'src'                 => <storage path>,
350          *         'ignoreMissingSource' => <boolean>
351          *     ]
352          * @endcode
353          *
354          * f) Update metadata for a file within storage
355          * @code
356          *     [
357          *         'op'                  => 'describe',
358          *         'src'                 => <storage path>,
359          *         'headers'             => <HTTP header name/value map>
360          *     ]
361          * @endcode
362          *
363          * g) Do nothing (no-op)
364          * @code
365          *     [
366          *         'op'                  => 'null',
367          *     ]
368          * @endcode
369          *
370          * Boolean flags for operations (operation-specific):
371          *   - ignoreMissingSource : The operation will simply succeed and do
372          *                           nothing if the source file does not exist.
373          *   - overwrite           : Any destination file will be overwritten.
374          *   - overwriteSame       : If a file already exists at the destination with the
375          *                           same contents, then do nothing to the destination file
376          *                           instead of giving an error. This does not compare headers.
377          *                           This option is ignored if 'overwrite' is already provided.
378          *   - headers             : If supplied, the result of merging these headers with any
379          *                           existing source file headers (replacing conflicting ones)
380          *                           will be set as the destination file headers. Headers are
381          *                           deleted if their value is set to the empty string. When a
382          *                           file has headers they are included in responses to GET and
383          *                           HEAD requests to the backing store for that file.
384          *                           Header values should be no larger than 255 bytes, except for
385          *                           Content-Disposition. The system might ignore or truncate any
386          *                           headers that are too long to store (exact limits will vary).
387          *                           Backends that don't support metadata ignore this. (since 1.21)
388          *
389          * $opts is an associative of boolean flags, including:
390          *   - force               : Operation precondition errors no longer trigger an abort.
391          *                           Any remaining operations are still attempted. Unexpected
392          *                           failures may still cause remaining operations to be aborted.
393          *   - nonLocking          : No locks are acquired for the operations.
394          *                           This can increase performance for non-critical writes.
395          *                           This has no effect unless the 'force' flag is set.
396          *   - nonJournaled        : Don't log this operation batch in the file journal.
397          *                           This limits the ability of recovery scripts.
398          *   - parallelize         : Try to do operations in parallel when possible.
399          *   - bypassReadOnly      : Allow writes in read-only mode. (since 1.20)
400          *   - preserveCache       : Don't clear the process cache before checking files.
401          *                           This should only be used if all entries in the process
402          *                           cache were added after the files were already locked. (since 1.20)
403          *
404          * @remarks Remarks on locking:
405          * File system paths given to operations should refer to files that are
406          * already locked or otherwise safe from modification from other processes.
407          * Normally these files will be new temp files, which should be adequate.
408          *
409          * @par Return value:
410          *
411          * This returns a Status, which contains all warnings and fatals that occurred
412          * during the operation. The 'failCount', 'successCount', and 'success' members
413          * will reflect each operation attempted.
414          *
415          * The StatusValue will be "OK" unless:
416          *   - a) unexpected operation errors occurred (network partitions, disk full...)
417          *   - b) significant operation errors occurred and 'force' was not set
418          *
419          * @param array $ops List of operations to execute in order
420          * @param array $opts Batch operation options
421          * @return StatusValue
422          */
423         final public function doOperations( array $ops, array $opts = [] ) {
424                 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
425                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
426                 }
427                 if ( !count( $ops ) ) {
428                         return $this->newStatus(); // nothing to do
429                 }
430
431                 $ops = $this->resolveFSFileObjects( $ops );
432                 if ( empty( $opts['force'] ) ) { // sanity
433                         unset( $opts['nonLocking'] );
434                 }
435
436                 /** @noinspection PhpUnusedLocalVariableInspection */
437                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
438
439                 return $this->doOperationsInternal( $ops, $opts );
440         }
441
442         /**
443          * @see FileBackend::doOperations()
444          * @param array $ops
445          * @param array $opts
446          */
447         abstract protected function doOperationsInternal( array $ops, array $opts );
448
449         /**
450          * Same as doOperations() except it takes a single operation.
451          * If you are doing a batch of operations that should either
452          * all succeed or all fail, then use that function instead.
453          *
454          * @see FileBackend::doOperations()
455          *
456          * @param array $op Operation
457          * @param array $opts Operation options
458          * @return StatusValue
459          */
460         final public function doOperation( array $op, array $opts = [] ) {
461                 return $this->doOperations( [ $op ], $opts );
462         }
463
464         /**
465          * Performs a single create operation.
466          * This sets $params['op'] to 'create' and passes it to doOperation().
467          *
468          * @see FileBackend::doOperation()
469          *
470          * @param array $params Operation parameters
471          * @param array $opts Operation options
472          * @return StatusValue
473          */
474         final public function create( array $params, array $opts = [] ) {
475                 return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
476         }
477
478         /**
479          * Performs a single store operation.
480          * This sets $params['op'] to 'store' and passes it to doOperation().
481          *
482          * @see FileBackend::doOperation()
483          *
484          * @param array $params Operation parameters
485          * @param array $opts Operation options
486          * @return StatusValue
487          */
488         final public function store( array $params, array $opts = [] ) {
489                 return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
490         }
491
492         /**
493          * Performs a single copy operation.
494          * This sets $params['op'] to 'copy' and passes it to doOperation().
495          *
496          * @see FileBackend::doOperation()
497          *
498          * @param array $params Operation parameters
499          * @param array $opts Operation options
500          * @return StatusValue
501          */
502         final public function copy( array $params, array $opts = [] ) {
503                 return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
504         }
505
506         /**
507          * Performs a single move operation.
508          * This sets $params['op'] to 'move' and passes it to doOperation().
509          *
510          * @see FileBackend::doOperation()
511          *
512          * @param array $params Operation parameters
513          * @param array $opts Operation options
514          * @return StatusValue
515          */
516         final public function move( array $params, array $opts = [] ) {
517                 return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
518         }
519
520         /**
521          * Performs a single delete operation.
522          * This sets $params['op'] to 'delete' and passes it to doOperation().
523          *
524          * @see FileBackend::doOperation()
525          *
526          * @param array $params Operation parameters
527          * @param array $opts Operation options
528          * @return StatusValue
529          */
530         final public function delete( array $params, array $opts = [] ) {
531                 return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
532         }
533
534         /**
535          * Performs a single describe operation.
536          * This sets $params['op'] to 'describe' and passes it to doOperation().
537          *
538          * @see FileBackend::doOperation()
539          *
540          * @param array $params Operation parameters
541          * @param array $opts Operation options
542          * @return StatusValue
543          * @since 1.21
544          */
545         final public function describe( array $params, array $opts = [] ) {
546                 return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
547         }
548
549         /**
550          * Perform a set of independent file operations on some files.
551          *
552          * This does no locking, nor journaling, and possibly no stat calls.
553          * Any destination files that already exist will be overwritten.
554          * This should *only* be used on non-original files, like cache files.
555          *
556          * Supported operations and their parameters:
557          *  - create
558          *  - store
559          *  - copy
560          *  - move
561          *  - delete
562          *  - describe (since 1.21)
563          *  - null
564          *
565          * FSFile/TempFSFile object support was added in 1.27.
566          *
567          * a) Create a new file in storage with the contents of a string
568          * @code
569          *     [
570          *         'op'                  => 'create',
571          *         'dst'                 => <storage path>,
572          *         'content'             => <string of new file contents>,
573          *         'headers'             => <HTTP header name/value map> # since 1.21
574          *     ]
575          * @endcode
576          *
577          * b) Copy a file system file into storage
578          * @code
579          *     [
580          *         'op'                  => 'store',
581          *         'src'                 => <file system path, FSFile, or TempFSFile>,
582          *         'dst'                 => <storage path>,
583          *         'headers'             => <HTTP header name/value map> # since 1.21
584          *     ]
585          * @endcode
586          *
587          * c) Copy a file within storage
588          * @code
589          *     [
590          *         'op'                  => 'copy',
591          *         'src'                 => <storage path>,
592          *         'dst'                 => <storage path>,
593          *         'ignoreMissingSource' => <boolean>, # since 1.21
594          *         'headers'             => <HTTP header name/value map> # since 1.21
595          *     ]
596          * @endcode
597          *
598          * d) Move a file within storage
599          * @code
600          *     [
601          *         'op'                  => 'move',
602          *         'src'                 => <storage path>,
603          *         'dst'                 => <storage path>,
604          *         'ignoreMissingSource' => <boolean>, # since 1.21
605          *         'headers'             => <HTTP header name/value map> # since 1.21
606          *     ]
607          * @endcode
608          *
609          * e) Delete a file within storage
610          * @code
611          *     [
612          *         'op'                  => 'delete',
613          *         'src'                 => <storage path>,
614          *         'ignoreMissingSource' => <boolean>
615          *     ]
616          * @endcode
617          *
618          * f) Update metadata for a file within storage
619          * @code
620          *     [
621          *         'op'                  => 'describe',
622          *         'src'                 => <storage path>,
623          *         'headers'             => <HTTP header name/value map>
624          *     ]
625          * @endcode
626          *
627          * g) Do nothing (no-op)
628          * @code
629          *     [
630          *         'op'                  => 'null',
631          *     ]
632          * @endcode
633          *
634          * @par Boolean flags for operations (operation-specific):
635          *   - ignoreMissingSource : The operation will simply succeed and do
636          *                           nothing if the source file does not exist.
637          *   - headers             : If supplied with a header name/value map, the backend will
638          *                           reply with these headers when GETs/HEADs of the destination
639          *                           file are made. Header values should be smaller than 256 bytes.
640          *                           Content-Disposition headers can be longer, though the system
641          *                           might ignore or truncate ones that are too long to store.
642          *                           Existing headers will remain, but these will replace any
643          *                           conflicting previous headers, and headers will be removed
644          *                           if they are set to an empty string.
645          *                           Backends that don't support metadata ignore this. (since 1.21)
646          *
647          * $opts is an associative of boolean flags, including:
648          *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20)
649          *
650          * @par Return value:
651          * This returns a Status, which contains all warnings and fatals that occurred
652          * during the operation. The 'failCount', 'successCount', and 'success' members
653          * will reflect each operation attempted for the given files. The StatusValue will be
654          * considered "OK" as long as no fatal errors occurred.
655          *
656          * @param array $ops Set of operations to execute
657          * @param array $opts Batch operation options
658          * @return StatusValue
659          * @since 1.20
660          */
661         final public function doQuickOperations( array $ops, array $opts = [] ) {
662                 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
663                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
664                 }
665                 if ( !count( $ops ) ) {
666                         return $this->newStatus(); // nothing to do
667                 }
668
669                 $ops = $this->resolveFSFileObjects( $ops );
670                 foreach ( $ops as &$op ) {
671                         $op['overwrite'] = true; // avoids RTTs in key/value stores
672                 }
673
674                 /** @noinspection PhpUnusedLocalVariableInspection */
675                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
676
677                 return $this->doQuickOperationsInternal( $ops );
678         }
679
680         /**
681          * @see FileBackend::doQuickOperations()
682          * @param array $ops
683          * @since 1.20
684          */
685         abstract protected function doQuickOperationsInternal( array $ops );
686
687         /**
688          * Same as doQuickOperations() except it takes a single operation.
689          * If you are doing a batch of operations, then use that function instead.
690          *
691          * @see FileBackend::doQuickOperations()
692          *
693          * @param array $op Operation
694          * @return StatusValue
695          * @since 1.20
696          */
697         final public function doQuickOperation( array $op ) {
698                 return $this->doQuickOperations( [ $op ] );
699         }
700
701         /**
702          * Performs a single quick create operation.
703          * This sets $params['op'] to 'create' and passes it to doQuickOperation().
704          *
705          * @see FileBackend::doQuickOperation()
706          *
707          * @param array $params Operation parameters
708          * @return StatusValue
709          * @since 1.20
710          */
711         final public function quickCreate( array $params ) {
712                 return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
713         }
714
715         /**
716          * Performs a single quick store operation.
717          * This sets $params['op'] to 'store' and passes it to doQuickOperation().
718          *
719          * @see FileBackend::doQuickOperation()
720          *
721          * @param array $params Operation parameters
722          * @return StatusValue
723          * @since 1.20
724          */
725         final public function quickStore( array $params ) {
726                 return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
727         }
728
729         /**
730          * Performs a single quick copy operation.
731          * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
732          *
733          * @see FileBackend::doQuickOperation()
734          *
735          * @param array $params Operation parameters
736          * @return StatusValue
737          * @since 1.20
738          */
739         final public function quickCopy( array $params ) {
740                 return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
741         }
742
743         /**
744          * Performs a single quick move operation.
745          * This sets $params['op'] to 'move' and passes it to doQuickOperation().
746          *
747          * @see FileBackend::doQuickOperation()
748          *
749          * @param array $params Operation parameters
750          * @return StatusValue
751          * @since 1.20
752          */
753         final public function quickMove( array $params ) {
754                 return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
755         }
756
757         /**
758          * Performs a single quick delete operation.
759          * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
760          *
761          * @see FileBackend::doQuickOperation()
762          *
763          * @param array $params Operation parameters
764          * @return StatusValue
765          * @since 1.20
766          */
767         final public function quickDelete( array $params ) {
768                 return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
769         }
770
771         /**
772          * Performs a single quick describe operation.
773          * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
774          *
775          * @see FileBackend::doQuickOperation()
776          *
777          * @param array $params Operation parameters
778          * @return StatusValue
779          * @since 1.21
780          */
781         final public function quickDescribe( array $params ) {
782                 return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
783         }
784
785         /**
786          * Concatenate a list of storage files into a single file system file.
787          * The target path should refer to a file that is already locked or
788          * otherwise safe from modification from other processes. Normally,
789          * the file will be a new temp file, which should be adequate.
790          *
791          * @param array $params Operation parameters, include:
792          *   - srcs        : ordered source storage paths (e.g. chunk1, chunk2, ...)
793          *   - dst         : file system path to 0-byte temp file
794          *   - parallelize : try to do operations in parallel when possible
795          * @return StatusValue
796          */
797         abstract public function concatenate( array $params );
798
799         /**
800          * Prepare a storage directory for usage.
801          * This will create any required containers and parent directories.
802          * Backends using key/value stores only need to create the container.
803          *
804          * The 'noAccess' and 'noListing' parameters works the same as in secure(),
805          * except they are only applied *if* the directory/container had to be created.
806          * These flags should always be set for directories that have private files.
807          * However, setting them is not guaranteed to actually do anything.
808          * Additional server configuration may be needed to achieve the desired effect.
809          *
810          * @param array $params Parameters include:
811          *   - dir            : storage directory
812          *   - noAccess       : try to deny file access (since 1.20)
813          *   - noListing      : try to deny file listing (since 1.20)
814          *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
815          * @return StatusValue
816          */
817         final public function prepare( array $params ) {
818                 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
819                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
820                 }
821                 /** @noinspection PhpUnusedLocalVariableInspection */
822                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
823                 return $this->doPrepare( $params );
824         }
825
826         /**
827          * @see FileBackend::prepare()
828          * @param array $params
829          */
830         abstract protected function doPrepare( array $params );
831
832         /**
833          * Take measures to block web access to a storage directory and
834          * the container it belongs to. FS backends might add .htaccess
835          * files whereas key/value store backends might revoke container
836          * access to the storage user representing end-users in web requests.
837          *
838          * This is not guaranteed to actually make files or listings publically hidden.
839          * Additional server configuration may be needed to achieve the desired effect.
840          *
841          * @param array $params Parameters include:
842          *   - dir            : storage directory
843          *   - noAccess       : try to deny file access
844          *   - noListing      : try to deny file listing
845          *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
846          * @return StatusValue
847          */
848         final public function secure( array $params ) {
849                 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
850                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
851                 }
852                 /** @noinspection PhpUnusedLocalVariableInspection */
853                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
854                 return $this->doSecure( $params );
855         }
856
857         /**
858          * @see FileBackend::secure()
859          * @param array $params
860          */
861         abstract protected function doSecure( array $params );
862
863         /**
864          * Remove measures to block web access to a storage directory and
865          * the container it belongs to. FS backends might remove .htaccess
866          * files whereas key/value store backends might grant container
867          * access to the storage user representing end-users in web requests.
868          * This essentially can undo the result of secure() calls.
869          *
870          * This is not guaranteed to actually make files or listings publically viewable.
871          * Additional server configuration may be needed to achieve the desired effect.
872          *
873          * @param array $params Parameters include:
874          *   - dir            : storage directory
875          *   - access         : try to allow file access
876          *   - listing        : try to allow file listing
877          *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
878          * @return StatusValue
879          * @since 1.20
880          */
881         final public function publish( array $params ) {
882                 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
883                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
884                 }
885                 /** @noinspection PhpUnusedLocalVariableInspection */
886                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
887                 return $this->doPublish( $params );
888         }
889
890         /**
891          * @see FileBackend::publish()
892          * @param array $params
893          */
894         abstract protected function doPublish( array $params );
895
896         /**
897          * Delete a storage directory if it is empty.
898          * Backends using key/value stores may do nothing unless the directory
899          * is that of an empty container, in which case it will be deleted.
900          *
901          * @param array $params Parameters include:
902          *   - dir            : storage directory
903          *   - recursive      : recursively delete empty subdirectories first (since 1.20)
904          *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
905          * @return StatusValue
906          */
907         final public function clean( array $params ) {
908                 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
909                         return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
910                 }
911                 /** @noinspection PhpUnusedLocalVariableInspection */
912                 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
913                 return $this->doClean( $params );
914         }
915
916         /**
917          * @see FileBackend::clean()
918          * @param array $params
919          */
920         abstract protected function doClean( array $params );
921
922         /**
923          * Enter file operation scope.
924          * This just makes PHP ignore user aborts/disconnects until the return
925          * value leaves scope. This returns null and does nothing in CLI mode.
926          *
927          * @return ScopedCallback|null
928          */
929         final protected function getScopedPHPBehaviorForOps() {
930                 if ( PHP_SAPI != 'cli' ) { // https://bugs.php.net/bug.php?id=47540
931                         $old = ignore_user_abort( true ); // avoid half-finished operations
932                         return new ScopedCallback( function () use ( $old ) {
933                                 ignore_user_abort( $old );
934                         } );
935                 }
936
937                 return null;
938         }
939
940         /**
941          * Check if a file exists at a storage path in the backend.
942          * This returns false if only a directory exists at the path.
943          *
944          * @param array $params Parameters include:
945          *   - src    : source storage path
946          *   - latest : use the latest available data
947          * @return bool|null Returns null on failure
948          */
949         abstract public function fileExists( array $params );
950
951         /**
952          * Get the last-modified timestamp of the file at a storage path.
953          *
954          * @param array $params Parameters include:
955          *   - src    : source storage path
956          *   - latest : use the latest available data
957          * @return string|bool TS_MW timestamp or false on failure
958          */
959         abstract public function getFileTimestamp( array $params );
960
961         /**
962          * Get the contents of a file at a storage path in the backend.
963          * This should be avoided for potentially large files.
964          *
965          * @param array $params Parameters include:
966          *   - src    : source storage path
967          *   - latest : use the latest available data
968          * @return string|bool Returns false on failure
969          */
970         final public function getFileContents( array $params ) {
971                 $contents = $this->getFileContentsMulti(
972                         [ 'srcs' => [ $params['src'] ] ] + $params );
973
974                 return $contents[$params['src']];
975         }
976
977         /**
978          * Like getFileContents() except it takes an array of storage paths
979          * and returns a map of storage paths to strings (or null on failure).
980          * The map keys (paths) are in the same order as the provided list of paths.
981          *
982          * @see FileBackend::getFileContents()
983          *
984          * @param array $params Parameters include:
985          *   - srcs        : list of source storage paths
986          *   - latest      : use the latest available data
987          *   - parallelize : try to do operations in parallel when possible
988          * @return array Map of (path name => string or false on failure)
989          * @since 1.20
990          */
991         abstract public function getFileContentsMulti( array $params );
992
993         /**
994          * Get metadata about a file at a storage path in the backend.
995          * If the file does not exist, then this returns false.
996          * Otherwise, the result is an associative array that includes:
997          *   - headers  : map of HTTP headers used for GET/HEAD requests (name => value)
998          *   - metadata : map of file metadata (name => value)
999          * Metadata keys and headers names will be returned in all lower-case.
1000          * Additional values may be included for internal use only.
1001          *
1002          * Use FileBackend::hasFeatures() to check how well this is supported.
1003          *
1004          * @param array $params
1005          * $params include:
1006          *   - src    : source storage path
1007          *   - latest : use the latest available data
1008          * @return array|bool Returns false on failure
1009          * @since 1.23
1010          */
1011         abstract public function getFileXAttributes( array $params );
1012
1013         /**
1014          * Get the size (bytes) of a file at a storage path in the backend.
1015          *
1016          * @param array $params Parameters include:
1017          *   - src    : source storage path
1018          *   - latest : use the latest available data
1019          * @return int|bool Returns false on failure
1020          */
1021         abstract public function getFileSize( array $params );
1022
1023         /**
1024          * Get quick information about a file at a storage path in the backend.
1025          * If the file does not exist, then this returns false.
1026          * Otherwise, the result is an associative array that includes:
1027          *   - mtime  : the last-modified timestamp (TS_MW)
1028          *   - size   : the file size (bytes)
1029          * Additional values may be included for internal use only.
1030          *
1031          * @param array $params Parameters include:
1032          *   - src    : source storage path
1033          *   - latest : use the latest available data
1034          * @return array|bool|null Returns null on failure
1035          */
1036         abstract public function getFileStat( array $params );
1037
1038         /**
1039          * Get a SHA-1 hash of the file at a storage path in the backend.
1040          *
1041          * @param array $params Parameters include:
1042          *   - src    : source storage path
1043          *   - latest : use the latest available data
1044          * @return string|bool Hash string or false on failure
1045          */
1046         abstract public function getFileSha1Base36( array $params );
1047
1048         /**
1049          * Get the properties of the file at a storage path in the backend.
1050          * This gives the result of FSFile::getProps() on a local copy of the file.
1051          *
1052          * @param array $params Parameters include:
1053          *   - src    : source storage path
1054          *   - latest : use the latest available data
1055          * @return array Returns FSFile::placeholderProps() on failure
1056          */
1057         abstract public function getFileProps( array $params );
1058
1059         /**
1060          * Stream the file at a storage path in the backend.
1061          *
1062          * If the file does not exists, an HTTP 404 error will be given.
1063          * Appropriate HTTP headers (Status, Content-Type, Content-Length)
1064          * will be sent if streaming began, while none will be sent otherwise.
1065          * Implementations should flush the output buffer before sending data.
1066          *
1067          * @param array $params Parameters include:
1068          *   - src      : source storage path
1069          *   - headers  : list of additional HTTP headers to send if the file exists
1070          *   - options  : HTTP request header map with lower case keys (since 1.28). Supports:
1071          *                range             : format is "bytes=(\d*-\d*)"
1072          *                if-modified-since : format is an HTTP date
1073          *   - headless : only include the body (and headers from "headers") (since 1.28)
1074          *   - latest   : use the latest available data
1075          *   - allowOB  : preserve any output buffers (since 1.28)
1076          * @return StatusValue
1077          */
1078         abstract public function streamFile( array $params );
1079
1080         /**
1081          * Returns a file system file, identical to the file at a storage path.
1082          * The file returned is either:
1083          *   - a) A local copy of the file at a storage path in the backend.
1084          *        The temporary copy will have the same extension as the source.
1085          *   - b) An original of the file at a storage path in the backend.
1086          * Temporary files may be purged when the file object falls out of scope.
1087          *
1088          * Write operations should *never* be done on this file as some backends
1089          * may do internal tracking or may be instances of FileBackendMultiWrite.
1090          * In that latter case, there are copies of the file that must stay in sync.
1091          * Additionally, further calls to this function may return the same file.
1092          *
1093          * @param array $params Parameters include:
1094          *   - src    : source storage path
1095          *   - latest : use the latest available data
1096          * @return FSFile|null Returns null on failure
1097          */
1098         final public function getLocalReference( array $params ) {
1099                 $fsFiles = $this->getLocalReferenceMulti(
1100                         [ 'srcs' => [ $params['src'] ] ] + $params );
1101
1102                 return $fsFiles[$params['src']];
1103         }
1104
1105         /**
1106          * Like getLocalReference() except it takes an array of storage paths
1107          * and returns a map of storage paths to FSFile objects (or null on failure).
1108          * The map keys (paths) are in the same order as the provided list of paths.
1109          *
1110          * @see FileBackend::getLocalReference()
1111          *
1112          * @param array $params Parameters include:
1113          *   - srcs        : list of source storage paths
1114          *   - latest      : use the latest available data
1115          *   - parallelize : try to do operations in parallel when possible
1116          * @return array Map of (path name => FSFile or null on failure)
1117          * @since 1.20
1118          */
1119         abstract public function getLocalReferenceMulti( array $params );
1120
1121         /**
1122          * Get a local copy on disk of the file at a storage path in the backend.
1123          * The temporary copy will have the same file extension as the source.
1124          * Temporary files may be purged when the file object falls out of scope.
1125          *
1126          * @param array $params Parameters include:
1127          *   - src    : source storage path
1128          *   - latest : use the latest available data
1129          * @return TempFSFile|null Returns null on failure
1130          */
1131         final public function getLocalCopy( array $params ) {
1132                 $tmpFiles = $this->getLocalCopyMulti(
1133                         [ 'srcs' => [ $params['src'] ] ] + $params );
1134
1135                 return $tmpFiles[$params['src']];
1136         }
1137
1138         /**
1139          * Like getLocalCopy() except it takes an array of storage paths and
1140          * returns a map of storage paths to TempFSFile objects (or null on failure).
1141          * The map keys (paths) are in the same order as the provided list of paths.
1142          *
1143          * @see FileBackend::getLocalCopy()
1144          *
1145          * @param array $params Parameters include:
1146          *   - srcs        : list of source storage paths
1147          *   - latest      : use the latest available data
1148          *   - parallelize : try to do operations in parallel when possible
1149          * @return array Map of (path name => TempFSFile or null on failure)
1150          * @since 1.20
1151          */
1152         abstract public function getLocalCopyMulti( array $params );
1153
1154         /**
1155          * Return an HTTP URL to a given file that requires no authentication to use.
1156          * The URL may be pre-authenticated (via some token in the URL) and temporary.
1157          * This will return null if the backend cannot make an HTTP URL for the file.
1158          *
1159          * This is useful for key/value stores when using scripts that seek around
1160          * large files and those scripts (and the backend) support HTTP Range headers.
1161          * Otherwise, one would need to use getLocalReference(), which involves loading
1162          * the entire file on to local disk.
1163          *
1164          * @param array $params Parameters include:
1165          *   - src : source storage path
1166          *   - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
1167          * @return string|null
1168          * @since 1.21
1169          */
1170         abstract public function getFileHttpUrl( array $params );
1171
1172         /**
1173          * Check if a directory exists at a given storage path.
1174          * Backends using key/value stores will check if the path is a
1175          * virtual directory, meaning there are files under the given directory.
1176          *
1177          * Storage backends with eventual consistency might return stale data.
1178          *
1179          * @param array $params Parameters include:
1180          *   - dir : storage directory
1181          * @return bool|null Returns null on failure
1182          * @since 1.20
1183          */
1184         abstract public function directoryExists( array $params );
1185
1186         /**
1187          * Get an iterator to list *all* directories under a storage directory.
1188          * If the directory is of the form "mwstore://backend/container",
1189          * then all directories in the container will be listed.
1190          * If the directory is of form "mwstore://backend/container/dir",
1191          * then all directories directly under that directory will be listed.
1192          * Results will be storage directories relative to the given directory.
1193          *
1194          * Storage backends with eventual consistency might return stale data.
1195          *
1196          * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1197          *
1198          * @param array $params Parameters include:
1199          *   - dir     : storage directory
1200          *   - topOnly : only return direct child dirs of the directory
1201          * @return Traversable|array|null Returns null on failure
1202          * @since 1.20
1203          */
1204         abstract public function getDirectoryList( array $params );
1205
1206         /**
1207          * Same as FileBackend::getDirectoryList() except only lists
1208          * directories that are immediately under the given directory.
1209          *
1210          * Storage backends with eventual consistency might return stale data.
1211          *
1212          * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1213          *
1214          * @param array $params Parameters include:
1215          *   - dir : storage directory
1216          * @return Traversable|array|null Returns null on failure
1217          * @since 1.20
1218          */
1219         final public function getTopDirectoryList( array $params ) {
1220                 return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
1221         }
1222
1223         /**
1224          * Get an iterator to list *all* stored files under a storage directory.
1225          * If the directory is of the form "mwstore://backend/container",
1226          * then all files in the container will be listed.
1227          * If the directory is of form "mwstore://backend/container/dir",
1228          * then all files under that directory will be listed.
1229          * Results will be storage paths relative to the given directory.
1230          *
1231          * Storage backends with eventual consistency might return stale data.
1232          *
1233          * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1234          *
1235          * @param array $params Parameters include:
1236          *   - dir        : storage directory
1237          *   - topOnly    : only return direct child files of the directory (since 1.20)
1238          *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
1239          * @return Traversable|array|null Returns null on failure
1240          */
1241         abstract public function getFileList( array $params );
1242
1243         /**
1244          * Same as FileBackend::getFileList() except only lists
1245          * files that are immediately under the given directory.
1246          *
1247          * Storage backends with eventual consistency might return stale data.
1248          *
1249          * Failures during iteration can result in FileBackendError exceptions (since 1.22).
1250          *
1251          * @param array $params Parameters include:
1252          *   - dir        : storage directory
1253          *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
1254          * @return Traversable|array|null Returns null on failure
1255          * @since 1.20
1256          */
1257         final public function getTopFileList( array $params ) {
1258                 return $this->getFileList( [ 'topOnly' => true ] + $params );
1259         }
1260
1261         /**
1262          * Preload persistent file stat cache and property cache into in-process cache.
1263          * This should be used when stat calls will be made on a known list of a many files.
1264          *
1265          * @see FileBackend::getFileStat()
1266          *
1267          * @param array $paths Storage paths
1268          */
1269         abstract public function preloadCache( array $paths );
1270
1271         /**
1272          * Invalidate any in-process file stat and property cache.
1273          * If $paths is given, then only the cache for those files will be cleared.
1274          *
1275          * @see FileBackend::getFileStat()
1276          *
1277          * @param array $paths Storage paths (optional)
1278          */
1279         abstract public function clearCache( array $paths = null );
1280
1281         /**
1282          * Preload file stat information (concurrently if possible) into in-process cache.
1283          *
1284          * This should be used when stat calls will be made on a known list of a many files.
1285          * This does not make use of the persistent file stat cache.
1286          *
1287          * @see FileBackend::getFileStat()
1288          *
1289          * @param array $params Parameters include:
1290          *   - srcs        : list of source storage paths
1291          *   - latest      : use the latest available data
1292          * @return bool All requests proceeded without I/O errors (since 1.24)
1293          * @since 1.23
1294          */
1295         abstract public function preloadFileStat( array $params );
1296
1297         /**
1298          * Lock the files at the given storage paths in the backend.
1299          * This will either lock all the files or none (on failure).
1300          *
1301          * Callers should consider using getScopedFileLocks() instead.
1302          *
1303          * @param array $paths Storage paths
1304          * @param int $type LockManager::LOCK_* constant
1305          * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
1306          * @return StatusValue
1307          */
1308         final public function lockFiles( array $paths, $type, $timeout = 0 ) {
1309                 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1310
1311                 return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) );
1312         }
1313
1314         /**
1315          * Unlock the files at the given storage paths in the backend.
1316          *
1317          * @param array $paths Storage paths
1318          * @param int $type LockManager::LOCK_* constant
1319          * @return StatusValue
1320          */
1321         final public function unlockFiles( array $paths, $type ) {
1322                 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1323
1324                 return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) );
1325         }
1326
1327         /**
1328          * Lock the files at the given storage paths in the backend.
1329          * This will either lock all the files or none (on failure).
1330          * On failure, the StatusValue object will be updated with errors.
1331          *
1332          * Once the return value goes out scope, the locks will be released and
1333          * the StatusValue updated. Unlock fatals will not change the StatusValue "OK" value.
1334          *
1335          * @see ScopedLock::factory()
1336          *
1337          * @param array $paths List of storage paths or map of lock types to path lists
1338          * @param int|string $type LockManager::LOCK_* constant or "mixed"
1339          * @param StatusValue $status StatusValue to update on lock/unlock
1340          * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
1341          * @return ScopedLock|null Returns null on failure
1342          */
1343         final public function getScopedFileLocks(
1344                 array $paths, $type, StatusValue $status, $timeout = 0
1345         ) {
1346                 if ( $type === 'mixed' ) {
1347                         foreach ( $paths as &$typePaths ) {
1348                                 $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
1349                         }
1350                 } else {
1351                         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1352                 }
1353
1354                 return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
1355         }
1356
1357         /**
1358          * Get an array of scoped locks needed for a batch of file operations.
1359          *
1360          * Normally, FileBackend::doOperations() handles locking, unless
1361          * the 'nonLocking' param is passed in. This function is useful if you
1362          * want the files to be locked for a broader scope than just when the
1363          * files are changing. For example, if you need to update DB metadata,
1364          * you may want to keep the files locked until finished.
1365          *
1366          * @see FileBackend::doOperations()
1367          *
1368          * @param array $ops List of file operations to FileBackend::doOperations()
1369          * @param StatusValue $status StatusValue to update on lock/unlock
1370          * @return ScopedLock|null
1371          * @since 1.20
1372          */
1373         abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
1374
1375         /**
1376          * Get the root storage path of this backend.
1377          * All container paths are "subdirectories" of this path.
1378          *
1379          * @return string Storage path
1380          * @since 1.20
1381          */
1382         final public function getRootStoragePath() {
1383                 return "mwstore://{$this->name}";
1384         }
1385
1386         /**
1387          * Get the storage path for the given container for this backend
1388          *
1389          * @param string $container Container name
1390          * @return string Storage path
1391          * @since 1.21
1392          */
1393         final public function getContainerStoragePath( $container ) {
1394                 return $this->getRootStoragePath() . "/{$container}";
1395         }
1396
1397         /**
1398          * Get the file journal object for this backend
1399          *
1400          * @return FileJournal
1401          */
1402         final public function getJournal() {
1403                 return $this->fileJournal;
1404         }
1405
1406         /**
1407          * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
1408          *
1409          * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it
1410          * around as long it needs (which may vary greatly depending on configuration)
1411          *
1412          * @param array $ops File operation batch for FileBaclend::doOperations()
1413          * @return array File operation batch
1414          */
1415         protected function resolveFSFileObjects( array $ops ) {
1416                 foreach ( $ops as &$op ) {
1417                         $src = isset( $op['src'] ) ? $op['src'] : null;
1418                         if ( $src instanceof FSFile ) {
1419                                 $op['srcRef'] = $src;
1420                                 $op['src'] = $src->getPath();
1421                         }
1422                 }
1423                 unset( $op );
1424
1425                 return $ops;
1426         }
1427
1428         /**
1429          * Check if a given path is a "mwstore://" path.
1430          * This does not do any further validation or any existence checks.
1431          *
1432          * @param string $path
1433          * @return bool
1434          */
1435         final public static function isStoragePath( $path ) {
1436                 return ( strpos( $path, 'mwstore://' ) === 0 );
1437         }
1438
1439         /**
1440          * Split a storage path into a backend name, a container name,
1441          * and a relative file path. The relative path may be the empty string.
1442          * This does not do any path normalization or traversal checks.
1443          *
1444          * @param string $storagePath
1445          * @return array (backend, container, rel object) or (null, null, null)
1446          */
1447         final public static function splitStoragePath( $storagePath ) {
1448                 if ( self::isStoragePath( $storagePath ) ) {
1449                         // Remove the "mwstore://" prefix and split the path
1450                         $parts = explode( '/', substr( $storagePath, 10 ), 3 );
1451                         if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
1452                                 if ( count( $parts ) == 3 ) {
1453                                         return $parts; // e.g. "backend/container/path"
1454                                 } else {
1455                                         return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
1456                                 }
1457                         }
1458                 }
1459
1460                 return [ null, null, null ];
1461         }
1462
1463         /**
1464          * Normalize a storage path by cleaning up directory separators.
1465          * Returns null if the path is not of the format of a valid storage path.
1466          *
1467          * @param string $storagePath
1468          * @return string|null
1469          */
1470         final public static function normalizeStoragePath( $storagePath ) {
1471                 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
1472                 if ( $relPath !== null ) { // must be for this backend
1473                         $relPath = self::normalizeContainerPath( $relPath );
1474                         if ( $relPath !== null ) {
1475                                 return ( $relPath != '' )
1476                                         ? "mwstore://{$backend}/{$container}/{$relPath}"
1477                                         : "mwstore://{$backend}/{$container}";
1478                         }
1479                 }
1480
1481                 return null;
1482         }
1483
1484         /**
1485          * Get the parent storage directory of a storage path.
1486          * This returns a path like "mwstore://backend/container",
1487          * "mwstore://backend/container/...", or null if there is no parent.
1488          *
1489          * @param string $storagePath
1490          * @return string|null
1491          */
1492         final public static function parentStoragePath( $storagePath ) {
1493                 $storagePath = dirname( $storagePath );
1494                 list( , , $rel ) = self::splitStoragePath( $storagePath );
1495
1496                 return ( $rel === null ) ? null : $storagePath;
1497         }
1498
1499         /**
1500          * Get the final extension from a storage or FS path
1501          *
1502          * @param string $path
1503          * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24)
1504          * @return string
1505          */
1506         final public static function extensionFromPath( $path, $case = 'lowercase' ) {
1507                 $i = strrpos( $path, '.' );
1508                 $ext = $i ? substr( $path, $i + 1 ) : '';
1509
1510                 if ( $case === 'lowercase' ) {
1511                         $ext = strtolower( $ext );
1512                 } elseif ( $case === 'uppercase' ) {
1513                         $ext = strtoupper( $ext );
1514                 }
1515
1516                 return $ext;
1517         }
1518
1519         /**
1520          * Check if a relative path has no directory traversals
1521          *
1522          * @param string $path
1523          * @return bool
1524          * @since 1.20
1525          */
1526         final public static function isPathTraversalFree( $path ) {
1527                 return ( self::normalizeContainerPath( $path ) !== null );
1528         }
1529
1530         /**
1531          * Build a Content-Disposition header value per RFC 6266.
1532          *
1533          * @param string $type One of (attachment, inline)
1534          * @param string $filename Suggested file name (should not contain slashes)
1535          * @throws FileBackendError
1536          * @return string
1537          * @since 1.20
1538          */
1539         final public static function makeContentDisposition( $type, $filename = '' ) {
1540                 $parts = [];
1541
1542                 $type = strtolower( $type );
1543                 if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
1544                         throw new InvalidArgumentException( "Invalid Content-Disposition type '$type'." );
1545                 }
1546                 $parts[] = $type;
1547
1548                 if ( strlen( $filename ) ) {
1549                         $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1550                 }
1551
1552                 return implode( ';', $parts );
1553         }
1554
1555         /**
1556          * Validate and normalize a relative storage path.
1557          * Null is returned if the path involves directory traversal.
1558          * Traversal is insecure for FS backends and broken for others.
1559          *
1560          * This uses the same traversal protection as Title::secureAndSplit().
1561          *
1562          * @param string $path Storage path relative to a container
1563          * @return string|null
1564          */
1565         final protected static function normalizeContainerPath( $path ) {
1566                 // Normalize directory separators
1567                 $path = strtr( $path, '\\', '/' );
1568                 // Collapse any consecutive directory separators
1569                 $path = preg_replace( '![/]{2,}!', '/', $path );
1570                 // Remove any leading directory separator
1571                 $path = ltrim( $path, '/' );
1572                 // Use the same traversal protection as Title::secureAndSplit()
1573                 if ( strpos( $path, '.' ) !== false ) {
1574                         if (
1575                                 $path === '.' ||
1576                                 $path === '..' ||
1577                                 strpos( $path, './' ) === 0 ||
1578                                 strpos( $path, '../' ) === 0 ||
1579                                 strpos( $path, '/./' ) !== false ||
1580                                 strpos( $path, '/../' ) !== false
1581                         ) {
1582                                 return null;
1583                         }
1584                 }
1585
1586                 return $path;
1587         }
1588
1589         /**
1590          * Yields the result of the status wrapper callback on either:
1591          *   - StatusValue::newGood() if this method is called without parameters
1592          *   - StatusValue::newFatal() with all parameters to this method if passed in
1593          *
1594          * @param string $args,...
1595          * @return StatusValue
1596          */
1597         final protected function newStatus() {
1598                 $args = func_get_args();
1599                 if ( count( $args ) ) {
1600                         $sv = call_user_func_array( [ 'StatusValue', 'newFatal' ], $args );
1601                 } else {
1602                         $sv = StatusValue::newGood();
1603                 }
1604
1605                 return $this->wrapStatus( $sv );
1606         }
1607
1608         /**
1609          * @param StatusValue $sv
1610          * @return StatusValue Modified status or StatusValue subclass
1611          */
1612         final protected function wrapStatus( StatusValue $sv ) {
1613                 return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
1614         }
1615
1616         /**
1617          * @param string $section
1618          * @return ScopedCallback|null
1619          */
1620         protected function scopedProfileSection( $section ) {
1621                 if ( $this->profiler ) {
1622                         call_user_func( [ $this->profiler, 'profileIn' ], $section );
1623                         return new ScopedCallback( [ $this->profiler, 'profileOut' ], [ $section ] );
1624                 }
1625
1626                 return null;
1627         }
1628
1629         protected function resetOutputBuffer() {
1630                 while ( ob_get_status() ) {
1631                         if ( !ob_end_clean() ) {
1632                                 // Could not remove output buffer handler; abort now
1633                                 // to avoid getting in some kind of infinite loop.
1634                                 break;
1635                         }
1636                 }
1637         }
1638 }