Merge "[FileBackend] Added FileBackend::getWikiID() function."
[lhc/web/wiklou.git] / includes / filebackend / FileBackend.php
1 <?php
2 /**
3 * @defgroup FileBackend File backend
4 * @ingroup FileRepo
5 *
6 * File backend is used to interact with file storage systems,
7 * such as the local file system, NFS, or cloud storage systems.
8 */
9
10 /**
11 * Base class for all file backends.
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26 * http://www.gnu.org/copyleft/gpl.html
27 *
28 * @file
29 * @ingroup FileBackend
30 * @author Aaron Schulz
31 */
32
33 /**
34 * @brief Base class for all file backend classes (including multi-write backends).
35 *
36 * This class defines the methods as abstract that subclasses must implement.
37 * Outside callers can assume that all backends will have these functions.
38 *
39 * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
40 * The "<path>" portion is a relative path that uses UNIX file system (FS)
41 * notation, though any particular backend may not actually be using a local
42 * filesystem. Therefore, the relative paths are only virtual.
43 *
44 * Backend contents are stored under wiki-specific container names by default.
45 * For legacy reasons, this has no effect for the FS backend class, and per-wiki
46 * segregation must be done by setting the container paths appropriately.
47 *
48 * FS-based backends are somewhat more restrictive due to the existence of real
49 * directory files; a regular file cannot have the same name as a directory. Other
50 * backends with virtual directories may not have this limitation. Callers should
51 * store files in such a way that no files and directories are under the same path.
52 *
53 * Methods of subclasses should avoid throwing exceptions at all costs.
54 * As a corollary, external dependencies should be kept to a minimum.
55 *
56 * @ingroup FileBackend
57 * @since 1.19
58 */
59 abstract class FileBackend {
60 protected $name; // string; unique backend name
61 protected $wikiId; // string; unique wiki name
62 protected $readOnly; // string; read-only explanation message
63 protected $parallelize; // string; when to do operations in parallel
64 protected $concurrency; // integer; how many operations can be done in parallel
65
66 /** @var LockManager */
67 protected $lockManager;
68 /** @var FileJournal */
69 protected $fileJournal;
70
71 /**
72 * Create a new backend instance from configuration.
73 * This should only be called from within FileBackendGroup.
74 *
75 * $config includes:
76 * - name : The unique name of this backend.
77 * This should consist of alphanumberic, '-', and '_' characters.
78 * This name should not be changed after use.
79 * - wikiId : Prefix to container names that is unique to this wiki.
80 * It should only consist of alphanumberic, '-', and '_' characters.
81 * - lockManager : Registered name of a file lock manager to use.
82 * - fileJournal : File journal configuration; see FileJournal::factory().
83 * Journals simply log changes to files stored in the backend.
84 * - readOnly : Write operations are disallowed if this is a non-empty string.
85 * It should be an explanation for the backend being read-only.
86 * - parallelize : When to do file operations in parallel (when possible).
87 * Allowed values are "implicit", "explicit" and "off".
88 * - concurrency : How many file operations can be done in parallel.
89 *
90 * @param $config Array
91 * @throws MWException
92 */
93 public function __construct( array $config ) {
94 $this->name = $config['name'];
95 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
96 throw new MWException( "Backend name `{$this->name}` is invalid." );
97 }
98 $this->wikiId = isset( $config['wikiId'] )
99 ? $config['wikiId']
100 : wfWikiID(); // e.g. "my_wiki-en_"
101 $this->lockManager = ( $config['lockManager'] instanceof LockManager )
102 ? $config['lockManager']
103 : LockManagerGroup::singleton()->get( $config['lockManager'] );
104 $this->fileJournal = isset( $config['fileJournal'] )
105 ? ( ( $config['fileJournal'] instanceof FileJournal )
106 ? $config['fileJournal']
107 : FileJournal::factory( $config['fileJournal'], $this->name ) )
108 : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
109 $this->readOnly = isset( $config['readOnly'] )
110 ? (string)$config['readOnly']
111 : '';
112 $this->parallelize = isset( $config['parallelize'] )
113 ? (string)$config['parallelize']
114 : 'off';
115 $this->concurrency = isset( $config['concurrency'] )
116 ? (int)$config['concurrency']
117 : 50;
118 }
119
120 /**
121 * Get the unique backend name.
122 * We may have multiple different backends of the same type.
123 * For example, we can have two Swift backends using different proxies.
124 *
125 * @return string
126 */
127 final public function getName() {
128 return $this->name;
129 }
130
131 /**
132 * Get the wiki identifier used for this backend (possibly empty)
133 *
134 * @return string
135 * @since 1.20
136 */
137 final public function getWikiId() {
138 return $this->wikiId;
139 }
140
141 /**
142 * Check if this backend is read-only
143 *
144 * @return bool
145 */
146 final public function isReadOnly() {
147 return ( $this->readOnly != '' );
148 }
149
150 /**
151 * Get an explanatory message if this backend is read-only
152 *
153 * @return string|bool Returns false if the backend is not read-only
154 */
155 final public function getReadOnlyReason() {
156 return ( $this->readOnly != '' ) ? $this->readOnly : false;
157 }
158
159 /**
160 * This is the main entry point into the backend for write operations.
161 * Callers supply an ordered list of operations to perform as a transaction.
162 * Files will be locked, the stat cache cleared, and then the operations attempted.
163 * If any serious errors occur, all attempted operations will be rolled back.
164 *
165 * $ops is an array of arrays. The outer array holds a list of operations.
166 * Each inner array is a set of key value pairs that specify an operation.
167 *
168 * Supported operations and their parameters. The supported actions are:
169 * - create
170 * - store
171 * - copy
172 * - move
173 * - delete
174 * - null
175 *
176 * a) Create a new file in storage with the contents of a string
177 * @code
178 * array(
179 * 'op' => 'create',
180 * 'dst' => <storage path>,
181 * 'content' => <string of new file contents>,
182 * 'overwrite' => <boolean>,
183 * 'overwriteSame' => <boolean>,
184 * 'disposition' => <Content-Disposition header value>
185 * );
186 * @endcode
187 *
188 * b) Copy a file system file into storage
189 * @code
190 * array(
191 * 'op' => 'store',
192 * 'src' => <file system path>,
193 * 'dst' => <storage path>,
194 * 'overwrite' => <boolean>,
195 * 'overwriteSame' => <boolean>,
196 * 'disposition' => <Content-Disposition header value>
197 * )
198 * @endcode
199 *
200 * c) Copy a file within storage
201 * @code
202 * array(
203 * 'op' => 'copy',
204 * 'src' => <storage path>,
205 * 'dst' => <storage path>,
206 * 'overwrite' => <boolean>,
207 * 'overwriteSame' => <boolean>,
208 * 'disposition' => <Content-Disposition header value>
209 * )
210 * @endcode
211 *
212 * d) Move a file within storage
213 * @code
214 * array(
215 * 'op' => 'move',
216 * 'src' => <storage path>,
217 * 'dst' => <storage path>,
218 * 'overwrite' => <boolean>,
219 * 'overwriteSame' => <boolean>,
220 * 'disposition' => <Content-Disposition header value>
221 * )
222 * @endcode
223 *
224 * e) Delete a file within storage
225 * @code
226 * array(
227 * 'op' => 'delete',
228 * 'src' => <storage path>,
229 * 'ignoreMissingSource' => <boolean>
230 * )
231 * @endcode
232 *
233 * f) Do nothing (no-op)
234 * @code
235 * array(
236 * 'op' => 'null',
237 * )
238 * @endcode
239 *
240 * Boolean flags for operations (operation-specific):
241 * - ignoreMissingSource : The operation will simply succeed and do
242 * nothing if the source file does not exist.
243 * - overwrite : Any destination file will be overwritten.
244 * - overwriteSame : An error will not be given if a file already
245 * exists at the destination that has the same
246 * contents as the new contents to be written there.
247 * - disposition : When supplied, the backend will add a Content-Disposition
248 * header when GETs/HEADs of the destination file are made.
249 * Backends that don't support file metadata will ignore this.
250 * See http://tools.ietf.org/html/rfc6266 (since 1.20).
251 *
252 * $opts is an associative of boolean flags, including:
253 * - force : Operation precondition errors no longer trigger an abort.
254 * Any remaining operations are still attempted. Unexpected
255 * failures may still cause remaning operations to be aborted.
256 * - nonLocking : No locks are acquired for the operations.
257 * This can increase performance for non-critical writes.
258 * This has no effect unless the 'force' flag is set.
259 * - allowStale : Don't require the latest available data.
260 * This can increase performance for non-critical writes.
261 * This has no effect unless the 'force' flag is set.
262 * - nonJournaled : Don't log this operation batch in the file journal.
263 * This limits the ability of recovery scripts.
264 * - parallelize : Try to do operations in parallel when possible.
265 * - bypassReadOnly : Allow writes in read-only mode (since 1.20).
266 * - preserveCache : Don't clear the process cache before checking files.
267 * This should only be used if all entries in the process
268 * cache were added after the files were already locked (since 1.20).
269 *
270 * @remarks Remarks on locking:
271 * File system paths given to operations should refer to files that are
272 * already locked or otherwise safe from modification from other processes.
273 * Normally these files will be new temp files, which should be adequate.
274 *
275 * @par Return value:
276 *
277 * This returns a Status, which contains all warnings and fatals that occurred
278 * during the operation. The 'failCount', 'successCount', and 'success' members
279 * will reflect each operation attempted.
280 *
281 * The status will be "OK" unless:
282 * - a) unexpected operation errors occurred (network partitions, disk full...)
283 * - b) significant operation errors occurred and 'force' was not set
284 *
285 * @param $ops Array List of operations to execute in order
286 * @param $opts Array Batch operation options
287 * @return Status
288 */
289 final public function doOperations( array $ops, array $opts = array() ) {
290 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
291 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
292 }
293 if ( empty( $opts['force'] ) ) { // sanity
294 unset( $opts['nonLocking'] );
295 unset( $opts['allowStale'] );
296 }
297 return $this->doOperationsInternal( $ops, $opts );
298 }
299
300 /**
301 * @see FileBackend::doOperations()
302 */
303 abstract protected function doOperationsInternal( array $ops, array $opts );
304
305 /**
306 * Same as doOperations() except it takes a single operation.
307 * If you are doing a batch of operations that should either
308 * all succeed or all fail, then use that function instead.
309 *
310 * @see FileBackend::doOperations()
311 *
312 * @param $op Array Operation
313 * @param $opts Array Operation options
314 * @return Status
315 */
316 final public function doOperation( array $op, array $opts = array() ) {
317 return $this->doOperations( array( $op ), $opts );
318 }
319
320 /**
321 * Performs a single create operation.
322 * This sets $params['op'] to 'create' and passes it to doOperation().
323 *
324 * @see FileBackend::doOperation()
325 *
326 * @param $params Array Operation parameters
327 * @param $opts Array Operation options
328 * @return Status
329 */
330 final public function create( array $params, array $opts = array() ) {
331 return $this->doOperation( array( 'op' => 'create' ) + $params, $opts );
332 }
333
334 /**
335 * Performs a single store operation.
336 * This sets $params['op'] to 'store' and passes it to doOperation().
337 *
338 * @see FileBackend::doOperation()
339 *
340 * @param $params Array Operation parameters
341 * @param $opts Array Operation options
342 * @return Status
343 */
344 final public function store( array $params, array $opts = array() ) {
345 return $this->doOperation( array( 'op' => 'store' ) + $params, $opts );
346 }
347
348 /**
349 * Performs a single copy operation.
350 * This sets $params['op'] to 'copy' and passes it to doOperation().
351 *
352 * @see FileBackend::doOperation()
353 *
354 * @param $params Array Operation parameters
355 * @param $opts Array Operation options
356 * @return Status
357 */
358 final public function copy( array $params, array $opts = array() ) {
359 return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts );
360 }
361
362 /**
363 * Performs a single move operation.
364 * This sets $params['op'] to 'move' and passes it to doOperation().
365 *
366 * @see FileBackend::doOperation()
367 *
368 * @param $params Array Operation parameters
369 * @param $opts Array Operation options
370 * @return Status
371 */
372 final public function move( array $params, array $opts = array() ) {
373 return $this->doOperation( array( 'op' => 'move' ) + $params, $opts );
374 }
375
376 /**
377 * Performs a single delete operation.
378 * This sets $params['op'] to 'delete' and passes it to doOperation().
379 *
380 * @see FileBackend::doOperation()
381 *
382 * @param $params Array Operation parameters
383 * @param $opts Array Operation options
384 * @return Status
385 */
386 final public function delete( array $params, array $opts = array() ) {
387 return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts );
388 }
389
390 /**
391 * Perform a set of independent file operations on some files.
392 *
393 * This does no locking, nor journaling, and possibly no stat calls.
394 * Any destination files that already exist will be overwritten.
395 * This should *only* be used on non-original files, like cache files.
396 *
397 * Supported operations and their parameters:
398 * - create
399 * - store
400 * - copy
401 * - move
402 * - delete
403 * - null
404 *
405 * a) Create a new file in storage with the contents of a string
406 * @code
407 * array(
408 * 'op' => 'create',
409 * 'dst' => <storage path>,
410 * 'content' => <string of new file contents>,
411 * 'disposition' => <Content-Disposition header value>
412 * )
413 * @endcode
414 * b) Copy a file system file into storage
415 * @code
416 * array(
417 * 'op' => 'store',
418 * 'src' => <file system path>,
419 * 'dst' => <storage path>,
420 * 'disposition' => <Content-Disposition header value>
421 * )
422 * @endcode
423 * c) Copy a file within storage
424 * @code
425 * array(
426 * 'op' => 'copy',
427 * 'src' => <storage path>,
428 * 'dst' => <storage path>,
429 * 'disposition' => <Content-Disposition header value>
430 * )
431 * @endcode
432 * d) Move a file within storage
433 * @code
434 * array(
435 * 'op' => 'move',
436 * 'src' => <storage path>,
437 * 'dst' => <storage path>,
438 * 'disposition' => <Content-Disposition header value>
439 * )
440 * @endcode
441 * e) Delete a file within storage
442 * @code
443 * array(
444 * 'op' => 'delete',
445 * 'src' => <storage path>,
446 * 'ignoreMissingSource' => <boolean>
447 * )
448 * @endcode
449 * f) Do nothing (no-op)
450 * @code
451 * array(
452 * 'op' => 'null',
453 * )
454 * @endcode
455 *
456 * @par Boolean flags for operations (operation-specific):
457 * - ignoreMissingSource : The operation will simply succeed and do
458 * nothing if the source file does not exist.
459 * - disposition : When supplied, the backend will add a Content-Disposition
460 * header when GETs/HEADs of the destination file are made.
461 * Backends that don't support file metadata will ignore this.
462 * See http://tools.ietf.org/html/rfc6266 (since 1.20).
463 *
464 * $opts is an associative of boolean flags, including:
465 * - bypassReadOnly : Allow writes in read-only mode (since 1.20)
466 *
467 * @par Return value:
468 * This returns a Status, which contains all warnings and fatals that occurred
469 * during the operation. The 'failCount', 'successCount', and 'success' members
470 * will reflect each operation attempted for the given files. The status will be
471 * considered "OK" as long as no fatal errors occurred.
472 *
473 * @param $ops Array Set of operations to execute
474 * @param $opts Array Batch operation options
475 * @return Status
476 * @since 1.20
477 */
478 final public function doQuickOperations( array $ops, array $opts = array() ) {
479 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
480 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
481 }
482 foreach ( $ops as &$op ) {
483 $op['overwrite'] = true; // avoids RTTs in key/value stores
484 }
485 return $this->doQuickOperationsInternal( $ops );
486 }
487
488 /**
489 * @see FileBackend::doQuickOperations()
490 * @since 1.20
491 */
492 abstract protected function doQuickOperationsInternal( array $ops );
493
494 /**
495 * Same as doQuickOperations() except it takes a single operation.
496 * If you are doing a batch of operations, then use that function instead.
497 *
498 * @see FileBackend::doQuickOperations()
499 *
500 * @param $op Array Operation
501 * @return Status
502 * @since 1.20
503 */
504 final public function doQuickOperation( array $op ) {
505 return $this->doQuickOperations( array( $op ) );
506 }
507
508 /**
509 * Performs a single quick create operation.
510 * This sets $params['op'] to 'create' and passes it to doQuickOperation().
511 *
512 * @see FileBackend::doQuickOperation()
513 *
514 * @param $params Array Operation parameters
515 * @return Status
516 * @since 1.20
517 */
518 final public function quickCreate( array $params ) {
519 return $this->doQuickOperation( array( 'op' => 'create' ) + $params );
520 }
521
522 /**
523 * Performs a single quick store operation.
524 * This sets $params['op'] to 'store' and passes it to doQuickOperation().
525 *
526 * @see FileBackend::doQuickOperation()
527 *
528 * @param $params Array Operation parameters
529 * @return Status
530 * @since 1.20
531 */
532 final public function quickStore( array $params ) {
533 return $this->doQuickOperation( array( 'op' => 'store' ) + $params );
534 }
535
536 /**
537 * Performs a single quick copy operation.
538 * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
539 *
540 * @see FileBackend::doQuickOperation()
541 *
542 * @param $params Array Operation parameters
543 * @return Status
544 * @since 1.20
545 */
546 final public function quickCopy( array $params ) {
547 return $this->doQuickOperation( array( 'op' => 'copy' ) + $params );
548 }
549
550 /**
551 * Performs a single quick move operation.
552 * This sets $params['op'] to 'move' and passes it to doQuickOperation().
553 *
554 * @see FileBackend::doQuickOperation()
555 *
556 * @param $params Array Operation parameters
557 * @return Status
558 * @since 1.20
559 */
560 final public function quickMove( array $params ) {
561 return $this->doQuickOperation( array( 'op' => 'move' ) + $params );
562 }
563
564 /**
565 * Performs a single quick delete operation.
566 * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
567 *
568 * @see FileBackend::doQuickOperation()
569 *
570 * @param $params Array Operation parameters
571 * @return Status
572 * @since 1.20
573 */
574 final public function quickDelete( array $params ) {
575 return $this->doQuickOperation( array( 'op' => 'delete' ) + $params );
576 }
577
578 /**
579 * Concatenate a list of storage files into a single file system file.
580 * The target path should refer to a file that is already locked or
581 * otherwise safe from modification from other processes. Normally,
582 * the file will be a new temp file, which should be adequate.
583 *
584 * @param $params Array Operation parameters
585 * $params include:
586 * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
587 * - dst : file system path to 0-byte temp file
588 * @return Status
589 */
590 abstract public function concatenate( array $params );
591
592 /**
593 * Prepare a storage directory for usage.
594 * This will create any required containers and parent directories.
595 * Backends using key/value stores only need to create the container.
596 *
597 * The 'noAccess' and 'noListing' parameters works the same as in secure(),
598 * except they are only applied *if* the directory/container had to be created.
599 * These flags should always be set for directories that have private files.
600 *
601 * @param $params Array
602 * $params include:
603 * - dir : storage directory
604 * - noAccess : try to deny file access (since 1.20)
605 * - noListing : try to deny file listing (since 1.20)
606 * - bypassReadOnly : allow writes in read-only mode (since 1.20)
607 * @return Status
608 */
609 final public function prepare( array $params ) {
610 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
611 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
612 }
613 return $this->doPrepare( $params );
614 }
615
616 /**
617 * @see FileBackend::prepare()
618 */
619 abstract protected function doPrepare( array $params );
620
621 /**
622 * Take measures to block web access to a storage directory and
623 * the container it belongs to. FS backends might add .htaccess
624 * files whereas key/value store backends might revoke container
625 * access to the storage user representing end-users in web requests.
626 * This is not guaranteed to actually do anything.
627 *
628 * @param $params Array
629 * $params include:
630 * - dir : storage directory
631 * - noAccess : try to deny file access
632 * - noListing : try to deny file listing
633 * - bypassReadOnly : allow writes in read-only mode (since 1.20)
634 * @return Status
635 */
636 final public function secure( array $params ) {
637 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
638 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
639 }
640 return $this->doSecure( $params );
641 }
642
643 /**
644 * @see FileBackend::secure()
645 */
646 abstract protected function doSecure( array $params );
647
648 /**
649 * Remove measures to block web access to a storage directory and
650 * the container it belongs to. FS backends might remove .htaccess
651 * files whereas key/value store backends might grant container
652 * access to the storage user representing end-users in web requests.
653 * This essentially can undo the result of secure() calls.
654 *
655 * @param $params Array
656 * $params include:
657 * - dir : storage directory
658 * - access : try to allow file access
659 * - listing : try to allow file listing
660 * - bypassReadOnly : allow writes in read-only mode (since 1.20)
661 * @return Status
662 * @since 1.20
663 */
664 final public function publish( array $params ) {
665 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
666 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
667 }
668 return $this->doPublish( $params );
669 }
670
671 /**
672 * @see FileBackend::publish()
673 */
674 abstract protected function doPublish( array $params );
675
676 /**
677 * Delete a storage directory if it is empty.
678 * Backends using key/value stores may do nothing unless the directory
679 * is that of an empty container, in which case it will be deleted.
680 *
681 * @param $params Array
682 * $params include:
683 * - dir : storage directory
684 * - recursive : recursively delete empty subdirectories first (since 1.20)
685 * - bypassReadOnly : allow writes in read-only mode (since 1.20)
686 * @return Status
687 */
688 final public function clean( array $params ) {
689 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
690 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
691 }
692 return $this->doClean( $params );
693 }
694
695 /**
696 * @see FileBackend::clean()
697 */
698 abstract protected function doClean( array $params );
699
700 /**
701 * Check if a file exists at a storage path in the backend.
702 * This returns false if only a directory exists at the path.
703 *
704 * @param $params Array
705 * $params include:
706 * - src : source storage path
707 * - latest : use the latest available data
708 * @return bool|null Returns null on failure
709 */
710 abstract public function fileExists( array $params );
711
712 /**
713 * Get the last-modified timestamp of the file at a storage path.
714 *
715 * @param $params Array
716 * $params include:
717 * - src : source storage path
718 * - latest : use the latest available data
719 * @return string|bool TS_MW timestamp or false on failure
720 */
721 abstract public function getFileTimestamp( array $params );
722
723 /**
724 * Get the contents of a file at a storage path in the backend.
725 * This should be avoided for potentially large files.
726 *
727 * @param $params Array
728 * $params include:
729 * - src : source storage path
730 * - latest : use the latest available data
731 * @return string|bool Returns false on failure
732 */
733 abstract public function getFileContents( array $params );
734
735 /**
736 * Get the size (bytes) of a file at a storage path in the backend.
737 *
738 * @param $params Array
739 * $params include:
740 * - src : source storage path
741 * - latest : use the latest available data
742 * @return integer|bool Returns false on failure
743 */
744 abstract public function getFileSize( array $params );
745
746 /**
747 * Get quick information about a file at a storage path in the backend.
748 * If the file does not exist, then this returns false.
749 * Otherwise, the result is an associative array that includes:
750 * - mtime : the last-modified timestamp (TS_MW)
751 * - size : the file size (bytes)
752 * Additional values may be included for internal use only.
753 *
754 * @param $params Array
755 * $params include:
756 * - src : source storage path
757 * - latest : use the latest available data
758 * @return Array|bool|null Returns null on failure
759 */
760 abstract public function getFileStat( array $params );
761
762 /**
763 * Get a SHA-1 hash of the file at a storage path in the backend.
764 *
765 * @param $params Array
766 * $params include:
767 * - src : source storage path
768 * - latest : use the latest available data
769 * @return string|bool Hash string or false on failure
770 */
771 abstract public function getFileSha1Base36( array $params );
772
773 /**
774 * Get the properties of the file at a storage path in the backend.
775 * Returns FSFile::placeholderProps() on failure.
776 *
777 * @param $params Array
778 * $params include:
779 * - src : source storage path
780 * - latest : use the latest available data
781 * @return Array
782 */
783 abstract public function getFileProps( array $params );
784
785 /**
786 * Stream the file at a storage path in the backend.
787 * If the file does not exists, an HTTP 404 error will be given.
788 * Appropriate HTTP headers (Status, Content-Type, Content-Length)
789 * will be sent if streaming began, while none will be sent otherwise.
790 * Implementations should flush the output buffer before sending data.
791 *
792 * @param $params Array
793 * $params include:
794 * - src : source storage path
795 * - headers : list of additional HTTP headers to send on success
796 * - latest : use the latest available data
797 * @return Status
798 */
799 abstract public function streamFile( array $params );
800
801 /**
802 * Returns a file system file, identical to the file at a storage path.
803 * The file returned is either:
804 * - a) A local copy of the file at a storage path in the backend.
805 * The temporary copy will have the same extension as the source.
806 * - b) An original of the file at a storage path in the backend.
807 * Temporary files may be purged when the file object falls out of scope.
808 *
809 * Write operations should *never* be done on this file as some backends
810 * may do internal tracking or may be instances of FileBackendMultiWrite.
811 * In that later case, there are copies of the file that must stay in sync.
812 * Additionally, further calls to this function may return the same file.
813 *
814 * @param $params Array
815 * $params include:
816 * - src : source storage path
817 * - latest : use the latest available data
818 * @return FSFile|null Returns null on failure
819 */
820 abstract public function getLocalReference( array $params );
821
822 /**
823 * Get a local copy on disk of the file at a storage path in the backend.
824 * The temporary copy will have the same file extension as the source.
825 * Temporary files may be purged when the file object falls out of scope.
826 *
827 * @param $params Array
828 * $params include:
829 * - src : source storage path
830 * - latest : use the latest available data
831 * @return TempFSFile|null Returns null on failure
832 */
833 abstract public function getLocalCopy( array $params );
834
835 /**
836 * Check if a directory exists at a given storage path.
837 * Backends using key/value stores will check if the path is a
838 * virtual directory, meaning there are files under the given directory.
839 *
840 * Storage backends with eventual consistency might return stale data.
841 *
842 * @param $params array
843 * $params include:
844 * - dir : storage directory
845 * @return bool|null Returns null on failure
846 * @since 1.20
847 */
848 abstract public function directoryExists( array $params );
849
850 /**
851 * Get an iterator to list *all* directories under a storage directory.
852 * If the directory is of the form "mwstore://backend/container",
853 * then all directories in the container will be listed.
854 * If the directory is of form "mwstore://backend/container/dir",
855 * then all directories directly under that directory will be listed.
856 * Results will be storage directories relative to the given directory.
857 *
858 * Storage backends with eventual consistency might return stale data.
859 *
860 * @param $params array
861 * $params include:
862 * - dir : storage directory
863 * - topOnly : only return direct child dirs of the directory
864 * @return Traversable|Array|null Returns null on failure
865 * @since 1.20
866 */
867 abstract public function getDirectoryList( array $params );
868
869 /**
870 * Same as FileBackend::getDirectoryList() except only lists
871 * directories that are immediately under the given directory.
872 *
873 * Storage backends with eventual consistency might return stale data.
874 *
875 * @param $params array
876 * $params include:
877 * - dir : storage directory
878 * @return Traversable|Array|null Returns null on failure
879 * @since 1.20
880 */
881 final public function getTopDirectoryList( array $params ) {
882 return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
883 }
884
885 /**
886 * Get an iterator to list *all* stored files under a storage directory.
887 * If the directory is of the form "mwstore://backend/container",
888 * then all files in the container will be listed.
889 * If the directory is of form "mwstore://backend/container/dir",
890 * then all files under that directory will be listed.
891 * Results will be storage paths relative to the given directory.
892 *
893 * Storage backends with eventual consistency might return stale data.
894 *
895 * @param $params array
896 * $params include:
897 * - dir : storage directory
898 * - topOnly : only return direct child files of the directory (since 1.20)
899 * @return Traversable|Array|null Returns null on failure
900 */
901 abstract public function getFileList( array $params );
902
903 /**
904 * Same as FileBackend::getFileList() except only lists
905 * files that are immediately under the given directory.
906 *
907 * Storage backends with eventual consistency might return stale data.
908 *
909 * @param $params array
910 * $params include:
911 * - dir : storage directory
912 * @return Traversable|Array|null Returns null on failure
913 * @since 1.20
914 */
915 final public function getTopFileList( array $params ) {
916 return $this->getFileList( array( 'topOnly' => true ) + $params );
917 }
918
919 /**
920 * Preload persistent file stat and property cache into in-process cache.
921 * This should be used when stat calls will be made on a known list of a many files.
922 *
923 * @param $paths Array Storage paths
924 * @return void
925 */
926 public function preloadCache( array $paths ) {}
927
928 /**
929 * Invalidate any in-process file stat and property cache.
930 * If $paths is given, then only the cache for those files will be cleared.
931 *
932 * @param $paths Array Storage paths (optional)
933 * @return void
934 */
935 public function clearCache( array $paths = null ) {}
936
937 /**
938 * Lock the files at the given storage paths in the backend.
939 * This will either lock all the files or none (on failure).
940 *
941 * Callers should consider using getScopedFileLocks() instead.
942 *
943 * @param $paths Array Storage paths
944 * @param $type integer LockManager::LOCK_* constant
945 * @return Status
946 */
947 final public function lockFiles( array $paths, $type ) {
948 return $this->lockManager->lock( $paths, $type );
949 }
950
951 /**
952 * Unlock the files at the given storage paths in the backend.
953 *
954 * @param $paths Array Storage paths
955 * @param $type integer LockManager::LOCK_* constant
956 * @return Status
957 */
958 final public function unlockFiles( array $paths, $type ) {
959 return $this->lockManager->unlock( $paths, $type );
960 }
961
962 /**
963 * Lock the files at the given storage paths in the backend.
964 * This will either lock all the files or none (on failure).
965 * On failure, the status object will be updated with errors.
966 *
967 * Once the return value goes out scope, the locks will be released and
968 * the status updated. Unlock fatals will not change the status "OK" value.
969 *
970 * @param $paths Array Storage paths
971 * @param $type integer LockManager::LOCK_* constant
972 * @param $status Status Status to update on lock/unlock
973 * @return ScopedLock|null Returns null on failure
974 */
975 final public function getScopedFileLocks( array $paths, $type, Status $status ) {
976 return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
977 }
978
979 /**
980 * Get an array of scoped locks needed for a batch of file operations.
981 *
982 * Normally, FileBackend::doOperations() handles locking, unless
983 * the 'nonLocking' param is passed in. This function is useful if you
984 * want the files to be locked for a broader scope than just when the
985 * files are changing. For example, if you need to update DB metadata,
986 * you may want to keep the files locked until finished.
987 *
988 * @see FileBackend::doOperations()
989 *
990 * @param $ops Array List of file operations to FileBackend::doOperations()
991 * @param $status Status Status to update on lock/unlock
992 * @return Array List of ScopedFileLocks or null values
993 * @since 1.20
994 */
995 abstract public function getScopedLocksForOps( array $ops, Status $status );
996
997 /**
998 * Get the root storage path of this backend.
999 * All container paths are "subdirectories" of this path.
1000 *
1001 * @return string Storage path
1002 * @since 1.20
1003 */
1004 final public function getRootStoragePath() {
1005 return "mwstore://{$this->name}";
1006 }
1007
1008 /**
1009 * Get the file journal object for this backend
1010 *
1011 * @return FileJournal
1012 */
1013 final public function getJournal() {
1014 return $this->fileJournal;
1015 }
1016
1017 /**
1018 * Check if a given path is a "mwstore://" path.
1019 * This does not do any further validation or any existence checks.
1020 *
1021 * @param $path string
1022 * @return bool
1023 */
1024 final public static function isStoragePath( $path ) {
1025 return ( strpos( $path, 'mwstore://' ) === 0 );
1026 }
1027
1028 /**
1029 * Split a storage path into a backend name, a container name,
1030 * and a relative file path. The relative path may be the empty string.
1031 * This does not do any path normalization or traversal checks.
1032 *
1033 * @param $storagePath string
1034 * @return Array (backend, container, rel object) or (null, null, null)
1035 */
1036 final public static function splitStoragePath( $storagePath ) {
1037 if ( self::isStoragePath( $storagePath ) ) {
1038 // Remove the "mwstore://" prefix and split the path
1039 $parts = explode( '/', substr( $storagePath, 10 ), 3 );
1040 if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
1041 if ( count( $parts ) == 3 ) {
1042 return $parts; // e.g. "backend/container/path"
1043 } else {
1044 return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
1045 }
1046 }
1047 }
1048 return array( null, null, null );
1049 }
1050
1051 /**
1052 * Normalize a storage path by cleaning up directory separators.
1053 * Returns null if the path is not of the format of a valid storage path.
1054 *
1055 * @param $storagePath string
1056 * @return string|null
1057 */
1058 final public static function normalizeStoragePath( $storagePath ) {
1059 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
1060 if ( $relPath !== null ) { // must be for this backend
1061 $relPath = self::normalizeContainerPath( $relPath );
1062 if ( $relPath !== null ) {
1063 return ( $relPath != '' )
1064 ? "mwstore://{$backend}/{$container}/{$relPath}"
1065 : "mwstore://{$backend}/{$container}";
1066 }
1067 }
1068 return null;
1069 }
1070
1071 /**
1072 * Get the parent storage directory of a storage path.
1073 * This returns a path like "mwstore://backend/container",
1074 * "mwstore://backend/container/...", or null if there is no parent.
1075 *
1076 * @param $storagePath string
1077 * @return string|null
1078 */
1079 final public static function parentStoragePath( $storagePath ) {
1080 $storagePath = dirname( $storagePath );
1081 list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
1082 return ( $rel === null ) ? null : $storagePath;
1083 }
1084
1085 /**
1086 * Get the final extension from a storage or FS path
1087 *
1088 * @param $path string
1089 * @return string
1090 */
1091 final public static function extensionFromPath( $path ) {
1092 $i = strrpos( $path, '.' );
1093 return strtolower( $i ? substr( $path, $i + 1 ) : '' );
1094 }
1095
1096 /**
1097 * Check if a relative path has no directory traversals
1098 *
1099 * @param $path string
1100 * @return bool
1101 * @since 1.20
1102 */
1103 final public static function isPathTraversalFree( $path ) {
1104 return ( self::normalizeContainerPath( $path ) !== null );
1105 }
1106
1107 /**
1108 * Build a Content-Disposition header value per RFC 6266.
1109 *
1110 * @param $type string One of (attachment, inline)
1111 * @param $filename string Suggested file name (should not contain slashes)
1112 * @return string
1113 * @since 1.20
1114 */
1115 final public static function makeContentDisposition( $type, $filename = '' ) {
1116 $parts = array();
1117
1118 $type = strtolower( $type );
1119 if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) {
1120 throw new MWException( "Invalid Content-Disposition type '$type'." );
1121 }
1122 $parts[] = $type;
1123
1124 if ( strlen( $filename ) ) {
1125 $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
1126 }
1127
1128 return implode( ';', $parts );
1129 }
1130
1131 /**
1132 * Validate and normalize a relative storage path.
1133 * Null is returned if the path involves directory traversal.
1134 * Traversal is insecure for FS backends and broken for others.
1135 *
1136 * This uses the same traversal protection as Title::secureAndSplit().
1137 *
1138 * @param $path string Storage path relative to a container
1139 * @return string|null
1140 */
1141 final protected static function normalizeContainerPath( $path ) {
1142 // Normalize directory separators
1143 $path = strtr( $path, '\\', '/' );
1144 // Collapse any consecutive directory separators
1145 $path = preg_replace( '![/]{2,}!', '/', $path );
1146 // Remove any leading directory separator
1147 $path = ltrim( $path, '/' );
1148 // Use the same traversal protection as Title::secureAndSplit()
1149 if ( strpos( $path, '.' ) !== false ) {
1150 if (
1151 $path === '.' ||
1152 $path === '..' ||
1153 strpos( $path, './' ) === 0 ||
1154 strpos( $path, '../' ) === 0 ||
1155 strpos( $path, '/./' ) !== false ||
1156 strpos( $path, '/../' ) !== false
1157 ) {
1158 return null;
1159 }
1160 }
1161 return $path;
1162 }
1163 }