Completed
Branch master (098997)
by
unknown
28:44
created

FileBackend::getFileContentsMulti()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
c 0
b 0
f 0
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
 * @author Aaron Schulz
30
 */
31
use Psr\Log\LoggerAwareInterface;
32
use Psr\Log\LoggerInterface;
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( $params['obResetFunc'] )
0 ignored issues
show
Bug introduced by
The variable $params seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
189
			? $params['obResetFunc']
190
			: [ $this, 'resetOutputBuffer' ];
191
		$this->streamMimeFunc = isset( $params['streamMimeFunc'] )
192
			? $params['streamMimeFunc']
193
			: null;
194
		$this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
195
196
		$this->profiler = isset( $params['profiler'] ) ? $params['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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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' ) { // http://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 ) );
0 ignored issues
show
Bug introduced by
It seems like $this->lockManager->lock($paths, $type, $timeout) can be null; however, wrapStatus() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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
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' ] );
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
}
1639