37ae39995b39a93ee2c7dc6f5333492314b820c6
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackend.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Aaron Schulz
6 */
7
8 /**
9 * Base class for all file backend classes (including multi-write backends).
10 * This class defines the methods as abstract that subclasses must implement.
11 * Outside callers can assume that all backends will have these functions.
12 *
13 * All "storage paths" are of the format "mwstore://backend/container/path".
14 * The paths use typical file system (FS) notation, though any particular backend may
15 * not actually be using a local filesystem. Therefore, the paths are only virtual.
16 *
17 * FS-based backends are somewhat more restrictive due to the existence of real
18 * directory files; a regular file cannot have the same name as a directory. Other
19 * backends with virtual directories may not have this limitation. Callers should
20 * store files in such a way that no files and directories under the same path.
21 *
22 * Methods should avoid throwing exceptions at all costs.
23 * As a corollary, external dependencies should be kept to a minimum.
24 *
25 * @ingroup FileBackend
26 * @since 1.19
27 */
28 abstract class FileBackendBase {
29 protected $name; // unique backend name
30 protected $wikiId; // unique wiki name
31 /** @var LockManager */
32 protected $lockManager;
33
34 /**
35 * Build a new object from configuration.
36 * This should only be called from within FileBackendGroup.
37 *
38 * $config includes:
39 * 'name' : The name of this backend
40 * 'wikiId' : Prefix to container names that is unique to this wiki
41 * 'lockManager' : Registered name of the file lock manager to use
42 *
43 * @param $config Array
44 */
45 public function __construct( array $config ) {
46 $this->name = $config['name'];
47 $this->wikiId = isset( $config['wikiId'] )
48 ? $config['wikiId']
49 : wfWikiID();
50 $this->lockManager = LockManagerGroup::singleton()->get( $config['lockManager'] );
51 }
52
53 /**
54 * Get the unique backend name.
55 *
56 * We may have multiple different backends of the same type.
57 * For example, we can have two Swift backends using different proxies.
58 *
59 * @return string
60 */
61 final public function getName() {
62 return $this->name;
63 }
64
65 /**
66 * This is the main entry point into the backend for write operations.
67 * Callers supply an ordered list of operations to perform as a transaction.
68 * If any serious errors occur, all attempted operations will be rolled back.
69 *
70 * $ops is an array of arrays. The outer array holds a list of operations.
71 * Each inner array is a set of key value pairs that specify an operation.
72 *
73 * Supported operations and their parameters:
74 * a) Create a new file in storage with the contents of a string
75 * array(
76 * 'op' => 'create',
77 * 'dst' => <storage path>,
78 * 'content' => <string of new file contents>,
79 * 'overwriteDest' => <boolean>,
80 * 'overwriteSame' => <boolean>
81 * )
82 * b) Copy a file system file into storage
83 * array(
84 * 'op' => 'store',
85 * 'src' => <file system path>,
86 * 'dst' => <storage path>,
87 * 'overwriteDest' => <boolean>,
88 * 'overwriteSame' => <boolean>
89 * )
90 * c) Copy a file within storage
91 * array(
92 * 'op' => 'copy',
93 * 'src' => <storage path>,
94 * 'dst' => <storage path>,
95 * 'overwriteDest' => <boolean>,
96 * 'overwriteSame' => <boolean>
97 * )
98 * d) Move a file within storage
99 * array(
100 * 'op' => 'move',
101 * 'src' => <storage path>,
102 * 'dst' => <storage path>,
103 * 'overwriteDest' => <boolean>,
104 * 'overwriteSame' => <boolean>
105 * )
106 * e) Delete a file within storage
107 * array(
108 * 'op' => 'delete',
109 * 'src' => <storage path>,
110 * 'ignoreMissingSource' => <boolean>
111 * )
112 * f) Concatenate a list of files within storage into a single temp file
113 * array(
114 * 'op' => 'concatenate',
115 * 'srcs' => <ordered array of storage paths>,
116 * 'dst' => <file system path to 0-byte temp file>
117 * )
118 * g) Do nothing (no-op)
119 * array(
120 * 'op' => 'null',
121 * )
122 *
123 * Boolean flags for operations (operation-specific):
124 * 'ignoreMissingSource' : The operation will simply succeed and do
125 * nothing if the source file does not exist.
126 * 'overwriteDest' : Any destination file will be overwritten.
127 * 'overwriteSame' : An error will not be given if a file already
128 * exists at the destination that has the same
129 * contents as the new contents to be written there.
130 *
131 * $opts is an associative of boolean flags, including:
132 * 'ignoreErrors' : Errors that would normally cause a rollback do not.
133 * The remaining operations are still attempted if any fail.
134 * 'nonLocking' : No locks are acquired for the operations.
135 * This can increase performance for non-critical writes.
136 * This has no effect unless the 'ignoreErrors' flag is set.
137 * 'allowStale' : Don't require the latest available data.
138 * This can increase performance for non-critical writes.
139 * This has no effect unless the 'ignoreErrors' flag is set.
140 *
141 * Return value:
142 * This returns a Status, which contains all warnings and fatals that occured
143 * during the operation. The 'failCount', 'successCount', and 'success' members
144 * will reflect each operation attempted. The status will be "OK" unless any
145 * of the operations failed and the 'ignoreErrors' parameter was not set.
146 *
147 * @param $ops Array List of operations to execute in order
148 * @param $opts Array Batch operation options
149 * @return Status
150 */
151 final public function doOperations( array $ops, array $opts = array() ) {
152 if ( empty( $opts['ignoreErrors'] ) ) { // sanity
153 unset( $opts['nonLocking'] );
154 unset( $opts['allowStale'] );
155 }
156 return $this->doOperationsInternal( $ops, $opts );
157 }
158
159 /**
160 * @see FileBackendBase::doOperations()
161 */
162 abstract protected function doOperationsInternal( array $ops, array $opts );
163
164 /**
165 * Same as doOperations() except it takes a single operation.
166 * If you are doing a batch of operations that should either
167 * all succeed or all fail, then use that function instead.
168 *
169 * @see FileBackendBase::doOperations()
170 *
171 * @param $op Array Operation
172 * @param $opts Array Operation options
173 * @return Status
174 */
175 final public function doOperation( array $op, array $opts = array() ) {
176 return $this->doOperations( array( $op ), $opts );
177 }
178
179 /**
180 * Performs a single store operation.
181 * This sets $params['op'] to 'store' and passes it to doOperation().
182 *
183 * @see FileBackendBase::doOperation()
184 *
185 * @param $params Array Operation parameters
186 * @param $opts Array Operation options
187 * @return Status
188 */
189 final public function store( array $params, array $opts = array() ) {
190 $params['op'] = 'store';
191 return $this->doOperation( $params, $opts );
192 }
193
194 /**
195 * Performs a single copy operation.
196 * This sets $params['op'] to 'copy' and passes it to doOperation().
197 *
198 * @see FileBackendBase::doOperation()
199 *
200 * @param $params Array Operation parameters
201 * @param $opts Array Operation options
202 * @return Status
203 */
204 final public function copy( array $params, array $opts = array() ) {
205 $params['op'] = 'copy';
206 return $this->doOperation( $params, $opts );
207 }
208
209 /**
210 * Performs a single move operation.
211 * This sets $params['op'] to 'move' and passes it to doOperation().
212 *
213 * @see FileBackendBase::doOperation()
214 *
215 * @param $params Array Operation parameters
216 * @param $opts Array Operation options
217 * @return Status
218 */
219 final public function move( array $params, array $opts = array() ) {
220 $params['op'] = 'move';
221 return $this->doOperation( $params, $opts );
222 }
223
224 /**
225 * Performs a single delete operation.
226 * This sets $params['op'] to 'delete' and passes it to doOperation().
227 *
228 * @see FileBackendBase::doOperation()
229 *
230 * @param $params Array Operation parameters
231 * @param $opts Array Operation options
232 * @return Status
233 */
234 final public function delete( array $params, array $opts = array() ) {
235 $params['op'] = 'delete';
236 return $this->doOperation( $params, $opts );
237 }
238
239 /**
240 * Performs a single create operation.
241 * This sets $params['op'] to 'create' and passes it to doOperation().
242 *
243 * @see FileBackendBase::doOperation()
244 *
245 * @param $params Array Operation parameters
246 * @param $opts Array Operation options
247 * @return Status
248 */
249 final public function create( array $params, array $opts = array() ) {
250 $params['op'] = 'create';
251 return $this->doOperation( $params, $opts );
252 }
253
254 /**
255 * Performs a single concatenate operation.
256 * This sets $params['op'] to 'concatenate' and passes it to doOperation().
257 *
258 * @see FileBackendBase::doOperation()
259 *
260 * @param $params Array Operation parameters
261 * @param $opts Array Operation options
262 * @return Status
263 */
264 final public function concatenate( array $params, array $opts = array() ) {
265 $params['op'] = 'concatenate';
266 return $this->doOperation( $params, $opts );
267 }
268
269 /**
270 * Prepare a storage path for usage. This will create containers
271 * that don't yet exist or, on FS backends, create parent directories.
272 *
273 * $params include:
274 * dir : storage directory
275 *
276 * @param $params Array
277 * @return Status
278 */
279 abstract public function prepare( array $params );
280
281 /**
282 * Take measures to block web access to a directory and
283 * the container it belongs to. FS backends might add .htaccess
284 * files wheras backends like Swift this might restrict container
285 * access to backend user that represents end-users in web request.
286 * This is not guaranteed to actually do anything.
287 *
288 * $params include:
289 * dir : storage directory
290 * noAccess : try to deny file access
291 * noListing : try to deny file listing
292 *
293 * @param $params Array
294 * @return Status
295 */
296 abstract public function secure( array $params );
297
298 /**
299 * Clean up an empty storage directory.
300 * On FS backends, the directory will be deleted. Others may do nothing.
301 *
302 * $params include:
303 * dir : storage directory
304 *
305 * @param $params Array
306 * @return Status
307 */
308 abstract public function clean( array $params );
309
310 /**
311 * Check if a file exists at a storage path in the backend.
312 *
313 * $params include:
314 * src : source storage path
315 * latest : use the latest available data
316 *
317 * @param $params Array
318 * @return bool|null Returns null on failure
319 */
320 abstract public function fileExists( array $params );
321
322 /**
323 * Get a SHA-1 hash of the file at a storage path in the backend.
324 *
325 * $params include:
326 * src : source storage path
327 * latest : use the latest available data
328 *
329 * @param $params Array
330 * @return string|false Hash string or false on failure
331 */
332 abstract public function getFileSha1Base36( array $params );
333
334 /**
335 * Get the last-modified timestamp of the file at a storage path.
336 *
337 * $params include:
338 * src : source storage path
339 * latest : use the latest available data
340 *
341 * @param $params Array
342 * @return string|false TS_MW timestamp or false on failure
343 */
344 abstract public function getFileTimestamp( array $params );
345
346 /**
347 * Get the properties of the file at a storage path in the backend.
348 * Returns FSFile::placeholderProps() on failure.
349 *
350 * $params include:
351 * src : source storage path
352 * latest : use the latest available data
353 *
354 * @param $params Array
355 * @return Array
356 */
357 abstract public function getFileProps( array $params );
358
359 /**
360 * Stream the file at a storage path in the backend.
361 * Appropriate HTTP headers (Status, Content-Type, Content-Length)
362 * must be sent if streaming began, while none should be sent otherwise.
363 * Implementations should flush the output buffer before sending data.
364 *
365 * $params include:
366 * src : source storage path
367 * headers : additional HTTP headers to send on success
368 * latest : use the latest available data
369 *
370 * @param $params Array
371 * @return Status
372 */
373 abstract public function streamFile( array $params );
374
375 /**
376 * Returns a file system file, identical to the file at a storage path.
377 * The file returned is either:
378 * a) A local copy of the file at a storage path in the backend.
379 * The temporary copy will have the same extension as the source.
380 * b) An original of the file at a storage path in the backend.
381 * Temporary files may be purged when the file object falls out of scope.
382 *
383 * Write operations should *never* be done on this file as some backends
384 * may do internal tracking or may be instances of FileBackendMultiWrite.
385 * In that later case, there are copies of the file that must stay in sync.
386 *
387 * $params include:
388 * src : source storage path
389 * latest : use the latest available data
390 *
391 * @param $params Array
392 * @return FSFile|null Returns null on failure
393 */
394 abstract public function getLocalReference( array $params );
395
396 /**
397 * Get a local copy on disk of the file at a storage path in the backend.
398 * The temporary copy will have the same file extension as the source.
399 * Temporary files may be purged when the file object falls out of scope.
400 *
401 * $params include:
402 * src : source storage path
403 * latest : use the latest available data
404 *
405 * @param $params Array
406 * @return TempFSFile|null Returns null on failure
407 */
408 abstract public function getLocalCopy( array $params );
409
410 /**
411 * Get an iterator to list out all object files under a storage directory.
412 * If the directory is of the form "mwstore://container", then all items in
413 * the container should be listed. If of the form "mwstore://container/dir",
414 * then all items under that container directory should be listed.
415 * Results should be storage paths relative to the given directory.
416 *
417 * $params include:
418 * dir : storage path directory
419 *
420 * @return Traversable|Array|null Returns null on failure
421 */
422 abstract public function getFileList( array $params );
423
424 /**
425 * Lock the files at the given storage paths in the backend.
426 * This will either lock all the files or none (on failure).
427 *
428 * Callers should consider using getScopedFileLocks() instead.
429 *
430 * @param $paths Array Storage paths
431 * @param $type integer LockManager::LOCK_* constant
432 * @return Status
433 */
434 final public function lockFiles( array $paths, $type ) {
435 return $this->lockManager->lock( $paths, $type );
436 }
437
438 /**
439 * Unlock the files at the given storage paths in the backend.
440 *
441 * @param $paths Array Storage paths
442 * @param $type integer LockManager::LOCK_* constant
443 * @return Status
444 */
445 final public function unlockFiles( array $paths, $type ) {
446 return $this->lockManager->unlock( $paths, $type );
447 }
448
449 /**
450 * Lock the files at the given storage paths in the backend.
451 * This will either lock all the files or none (on failure).
452 * On failure, the status object will be updated with errors.
453 *
454 * Once the return value goes out scope, the locks will be released and
455 * the status updated. Unlock fatals will not change the status "OK" value.
456 *
457 * @param $paths Array Storage paths
458 * @param $type integer LockManager::LOCK_* constant
459 * @param $status Status Status to update on lock/unlock
460 * @return ScopedLock|null Returns null on failure
461 */
462 final public function getScopedFileLocks( array $paths, $type, Status $status ) {
463 return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
464 }
465 }
466
467 /**
468 * Base class for all single-write backends.
469 * This class defines the methods as abstract that subclasses must implement.
470 *
471 * @ingroup FileBackend
472 * @since 1.19
473 */
474 abstract class FileBackend extends FileBackendBase {
475 /** @var Array */
476 protected $cache = array(); // (storage path => key => value)
477 protected $maxCacheSize = 50; // integer; max paths with entries
478
479 /**
480 * Create a file in the backend with the given contents.
481 * Do not call this function from places outside FileBackend and FileOp.
482 * $params include:
483 * content : the raw file contents
484 * dst : destination storage path
485 * overwriteDest : overwrite any file that exists at the destination
486 *
487 * @param $params Array
488 * @return Status
489 */
490 final public function createInternal( array $params ) {
491 $status = $this->doCreateInternal( $params );
492 $this->clearCache( array( $params['dst'] ) );
493 return $status;
494 }
495
496 /**
497 * @see FileBackend::createInternal()
498 */
499 abstract protected function doCreateInternal( array $params );
500
501 /**
502 * Store a file into the backend from a file on disk.
503 * Do not call this function from places outside FileBackend and FileOp.
504 * $params include:
505 * src : source path on disk
506 * dst : destination storage path
507 * overwriteDest : overwrite any file that exists at the destination
508 *
509 * @param $params Array
510 * @return Status
511 */
512 final public function storeInternal( array $params ) {
513 $status = $this->doStoreInternal( $params );
514 $this->clearCache( array( $params['dst'] ) );
515 return $status;
516 }
517
518 /**
519 * @see FileBackend::storeInternal()
520 */
521 abstract protected function doStoreInternal( array $params );
522
523 /**
524 * Copy a file from one storage path to another in the backend.
525 * Do not call this function from places outside FileBackend and FileOp.
526 * $params include:
527 * src : source storage path
528 * dst : destination storage path
529 * overwriteDest : overwrite any file that exists at the destination
530 *
531 * @param $params Array
532 * @return Status
533 */
534 final public function copyInternal( array $params ) {
535 $status = $this->doCopyInternal( $params );
536 $this->clearCache( array( $params['dst'] ) );
537 return $status;
538 }
539
540 /**
541 * @see FileBackend::copyInternal()
542 */
543 abstract protected function doCopyInternal( array $params );
544
545 /**
546 * Delete a file at the storage path.
547 * Do not call this function from places outside FileBackend and FileOp.
548 * $params include:
549 * src : source storage path
550 * ignoreMissingSource : do nothing if the source file does not exist
551 *
552 * @param $params Array
553 * @return Status
554 */
555 final public function deleteInternal( array $params ) {
556 $status = $this->doDeleteInternal( $params );
557 $this->clearCache( array( $params['src'] ) );
558 return $status;
559 }
560
561 /**
562 * @see FileBackend::deleteInternal()
563 */
564 abstract protected function doDeleteInternal( array $params );
565
566 /**
567 * Move a file from one storage path to another in the backend.
568 * Do not call this function from places outside FileBackend and FileOp.
569 * $params include:
570 * src : source storage path
571 * dst : destination storage path
572 * overwriteDest : overwrite any file that exists at the destination
573 *
574 * @param $params Array
575 * @return Status
576 */
577 final public function moveInternal( array $params ) {
578 $status = $this->doMoveInternal( $params );
579 $this->clearCache( array( $params['src'], $params['dst'] ) );
580 return $status;
581 }
582
583 /**
584 * @see FileBackend::moveInternal()
585 */
586 protected function doMoveInternal( array $params ) {
587 // Copy source to dest
588 $status = $this->copy( $params );
589 if ( !$status->isOK() ) {
590 return $status;
591 }
592 // Delete source (only fails due to races or medium going down)
593 $status->merge( $this->delete( array( 'src' => $params['src'] ) ) );
594 $status->setResult( true, $status->value ); // ignore delete() errors
595 return $status;
596 }
597
598 /**
599 * Combines files from several storage paths into a new file in the backend.
600 * Do not call this function from places outside FileBackend and FileOp.
601 * $params include:
602 * srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
603 * dst : destination storage path
604 * overwriteDest : overwrite any file that exists at the destination
605 *
606 * @param $params Array
607 * @return Status
608 */
609 final public function concatenateInternal( array $params ) {
610 $status = $this->doConcatenateInternal( $params );
611 $this->clearCache( array( $params['dst'] ) );
612 return $status;
613 }
614
615 /**
616 * @see FileBackend::concatenateInternal()
617 */
618 protected function doConcatenateInternal( array $params ) {
619 $status = Status::newGood();
620 $tmpPath = $params['dst']; // convenience
621
622 // Check that the specified temp file is valid...
623 wfSuppressWarnings();
624 $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
625 wfRestoreWarnings();
626 if ( !$ok ) { // not present or not empty
627 $status->fatal( 'backend-fail-opentemp', $tmpPath );
628 return $status;
629 }
630
631 // Build up the temp file using the source chunks (in order)...
632 $tmpHandle = fopen( $tmpPath, 'a' );
633 if ( $tmpHandle === false ) {
634 $status->fatal( 'backend-fail-opentemp', $tmpPath );
635 return $status;
636 }
637 foreach ( $params['srcs'] as $virtualSource ) {
638 // Get a local FS version of the chunk
639 $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) );
640 if ( !$tmpFile ) {
641 $status->fatal( 'backend-fail-read', $virtualSource );
642 return $status;
643 }
644 // Get a handle to the local FS version
645 $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
646 if ( $sourceHandle === false ) {
647 fclose( $tmpHandle );
648 $status->fatal( 'backend-fail-read', $virtualSource );
649 return $status;
650 }
651 // Append chunk to file (pass chunk size to avoid magic quotes)
652 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
653 fclose( $sourceHandle );
654 fclose( $tmpHandle );
655 $status->fatal( 'backend-fail-writetemp', $tmpPath );
656 return $status;
657 }
658 fclose( $sourceHandle );
659 }
660 if ( !fclose( $tmpHandle ) ) {
661 $status->fatal( 'backend-fail-closetemp', $tmpPath );
662 return $status;
663 }
664
665 return $status;
666 }
667
668 /**
669 * @see FileBackendBase::prepare()
670 */
671 public function prepare( array $params ) {
672 return Status::newGood();
673 }
674
675 /**
676 * @see FileBackendBase::secure()
677 */
678 public function secure( array $params ) {
679 return Status::newGood();
680 }
681
682 /**
683 * @see FileBackendBase::clean()
684 */
685 public function clean( array $params ) {
686 return Status::newGood();
687 }
688
689 /**
690 * @see FileBackendBase::getFileSha1Base36()
691 */
692 public function getFileSha1Base36( array $params ) {
693 $path = $params['src'];
694 if ( isset( $this->cache[$path]['sha1'] ) ) {
695 return $this->cache[$path]['sha1'];
696 }
697 $fsFile = $this->getLocalReference( $params );
698 if ( !$fsFile ) {
699 return false;
700 } else {
701 $sha1 = $fsFile->getSha1Base36();
702 if ( $sha1 !== false ) { // don't cache negatives
703 $this->trimCache(); // limit memory
704 $this->cache[$path]['sha1'] = $sha1;
705 }
706 return $sha1;
707 }
708 }
709
710 /**
711 * @see FileBackendBase::getFileProps()
712 */
713 public function getFileProps( array $params ) {
714 $fsFile = $this->getLocalReference( $params );
715 if ( !$fsFile ) {
716 return FSFile::placeholderProps();
717 } else {
718 return $fsFile->getProps();
719 }
720 }
721
722 /**
723 * @see FileBackendBase::getLocalReference()
724 */
725 public function getLocalReference( array $params ) {
726 return $this->getLocalCopy( $params );
727 }
728
729 /**
730 * @see FileBackendBase::streamFile()
731 */
732 public function streamFile( array $params ) {
733 $status = Status::newGood();
734
735 $fsFile = $this->getLocalReference( $params );
736 if ( !$fsFile ) {
737 $status->fatal( 'backend-fail-stream', $params['src'] );
738 return $status;
739 }
740
741 $extraHeaders = isset( $params['headers'] )
742 ? $params['headers']
743 : array();
744
745 $ok = StreamFile::stream( $fsFile->getPath(), $extraHeaders, false );
746 if ( !$ok ) {
747 $status->fatal( 'backend-fail-stream', $params['src'] );
748 return $status;
749 }
750
751 return $status;
752 }
753
754 /**
755 * Get the list of supported operations and their corresponding FileOp classes.
756 *
757 * @return Array
758 */
759 protected function supportedOperations() {
760 return array(
761 'store' => 'StoreFileOp',
762 'copy' => 'CopyFileOp',
763 'move' => 'MoveFileOp',
764 'delete' => 'DeleteFileOp',
765 'concatenate' => 'ConcatenateFileOp',
766 'create' => 'CreateFileOp',
767 'null' => 'NullFileOp'
768 );
769 }
770
771 /**
772 * Return a list of FileOp objects from a list of operations.
773 * Do not call this function from places outside FileBackend.
774 *
775 * The result must have the same number of items as the input.
776 * An exception is thrown if an unsupported operation is requested.
777 *
778 * @param $ops Array Same format as doOperations()
779 * @return Array List of FileOp objects
780 * @throws MWException
781 */
782 final public function getOperations( array $ops ) {
783 $supportedOps = $this->supportedOperations();
784
785 $performOps = array(); // array of FileOp objects
786 // Build up ordered array of FileOps...
787 foreach ( $ops as $operation ) {
788 $opName = $operation['op'];
789 if ( isset( $supportedOps[$opName] ) ) {
790 $class = $supportedOps[$opName];
791 // Get params for this operation
792 $params = $operation;
793 // Append the FileOp class
794 $performOps[] = new $class( $this, $params );
795 } else {
796 throw new MWException( "Operation `$opName` is not supported." );
797 }
798 }
799
800 return $performOps;
801 }
802
803 /**
804 * @see FileBackendBase::doOperationsInternal()
805 */
806 protected function doOperationsInternal( array $ops, array $opts ) {
807 $status = Status::newGood();
808
809 // Build up a list of FileOps...
810 $performOps = $this->getOperations( $ops );
811
812 // Acquire any locks as needed...
813 if ( empty( $opts['nonLocking'] ) ) {
814 // Build up a list of files to lock...
815 $filesLockEx = $filesLockSh = array();
816 foreach ( $performOps as $fileOp ) {
817 $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
818 $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
819 }
820 // Optimization: if doing an EX lock anyway, don't also set an SH one
821 $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
822 // Try to lock those files for the scope of this function...
823 $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
824 $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
825 if ( !$status->isOK() ) {
826 return $status; // abort
827 }
828 }
829
830 // Clear any cache entries (after locks acquired)
831 $this->clearCache();
832 // Actually attempt the operation batch...
833 $status->merge( FileOp::attemptBatch( $performOps, $opts ) );
834
835 return $status;
836 }
837
838 /**
839 * Invalidate the file existence and property cache
840 *
841 * @param $paths Array Clear cache for specific files
842 * @return void
843 */
844 final public function clearCache( array $paths = null ) {
845 if ( $paths === null ) {
846 $this->cache = array();
847 } else {
848 foreach ( $paths as $path ) {
849 unset( $this->cache[$path] );
850 }
851 }
852 }
853
854 /**
855 * Prune the cache if it is too big to add an item
856 *
857 * @return void
858 */
859 protected function trimCache() {
860 if ( count( $this->cache ) >= $this->maxCacheSize ) {
861 reset( $this->cache );
862 $key = key( $this->cache );
863 unset( $this->cache[$key] );
864 }
865 }
866
867 /**
868 * Check if a given path is a mwstore:// path.
869 * This does not do any actual validation or existence checks.
870 *
871 * @param $path string
872 * @return bool
873 */
874 final public static function isStoragePath( $path ) {
875 return ( strpos( $path, 'mwstore://' ) === 0 );
876 }
877
878 /**
879 * Split a storage path (e.g. "mwstore://backend/container/path/to/object")
880 * into a backend name, a container name, and a relative object path.
881 *
882 * @param $storagePath string
883 * @return Array (backend, container, rel object) or (null, null, null)
884 */
885 final public static function splitStoragePath( $storagePath ) {
886 if ( self::isStoragePath( $storagePath ) ) {
887 // Note: strlen( 'mwstore://' ) = 10
888 $parts = explode( '/', substr( $storagePath, 10 ), 3 );
889 if ( count( $parts ) == 3 ) {
890 return $parts; // e.g. "backend/container/path"
891 } elseif ( count( $parts ) == 2 ) {
892 return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
893 }
894 }
895 return array( null, null, null );
896 }
897
898 /**
899 * Check if a container name is valid.
900 * This checks for for length and illegal characters.
901 *
902 * @param $container string
903 * @return bool
904 */
905 final protected static function isValidContainerName( $container ) {
906 // This accounts for Swift and S3 restrictions. Also note
907 // that these urlencode to the same string, which is useful
908 // since the Swift size limit is *after* URL encoding.
909 return preg_match( '/^[a-zA-Z0-9._-]{1,256}$/u', $container );
910 }
911
912 /**
913 * Validate and normalize a relative storage path.
914 * Null is returned if the path involves directory traversal.
915 * Traversal is insecure for FS backends and broken for others.
916 *
917 * @param $path string
918 * @return string|null
919 */
920 final protected static function normalizeStoragePath( $path ) {
921 // Normalize directory separators
922 $path = strtr( $path, '\\', '/' );
923 // Use the same traversal protection as Title::secureAndSplit()
924 if ( strpos( $path, '.' ) !== false ) {
925 if (
926 $path === '.' ||
927 $path === '..' ||
928 strpos( $path, './' ) === 0 ||
929 strpos( $path, '../' ) === 0 ||
930 strpos( $path, '/./' ) !== false ||
931 strpos( $path, '/../' ) !== false
932 ) {
933 return null;
934 }
935 }
936 return $path;
937 }
938
939 /**
940 * Split a storage path (e.g. "mwstore://backend/container/path/to/object")
941 * into an internal container name and an internal relative object name.
942 * This also checks that the storage path is valid and is within this backend.
943 *
944 * @param $storagePath string
945 * @return Array (container, object name) or (null, null) if path is invalid
946 */
947 final protected function resolveStoragePath( $storagePath ) {
948 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
949 if ( $backend === $this->name ) { // must be for this backend
950 $relPath = self::normalizeStoragePath( $relPath );
951 if ( $relPath !== null ) {
952 $relPath = $this->resolveContainerPath( $container, $relPath );
953 if ( $relPath !== null ) {
954 $container = $this->fullContainerName( $container );
955 if ( self::isValidContainerName( $container ) ) {
956 $container = $this->resolveContainerName( $container );
957 if ( $container !== null ) {
958 return array( $container, $relPath );
959 }
960 }
961 }
962 }
963 }
964 return array( null, null );
965 }
966
967 /**
968 * Get the full container name, including the wiki ID prefix
969 *
970 * @param $container string
971 * @return string
972 */
973 final protected function fullContainerName( $container ) {
974 if ( $this->wikiId != '' ) {
975 return "{$this->wikiId}-$container";
976 } else {
977 return $container;
978 }
979 }
980
981 /**
982 * Resolve a container name, checking if it's allowed by the backend.
983 * This is intended for internal use, such as encoding illegal chars.
984 * Subclasses can override this to be more restrictive.
985 *
986 * @param $container string
987 * @return string|null
988 */
989 protected function resolveContainerName( $container ) {
990 return $container;
991 }
992
993 /**
994 * Resolve a relative storage path, checking if it's allowed by the backend.
995 * This is intended for internal use, such as encoding illegal chars or perhaps
996 * getting absolute paths (e.g. FS based backends). Note that the relative path
997 * may be the empty string (e.g. the path is simply to the container).
998 *
999 * @param $container string Container the path is relative to
1000 * @param $relStoragePath string Relative storage path
1001 * @return string|null Path or null if not valid
1002 */
1003 protected function resolveContainerPath( $container, $relStoragePath ) {
1004 return $relStoragePath;
1005 }
1006
1007 /**
1008 * Get the final extension from a storage or FS path
1009 *
1010 * @param $path string
1011 * @return string
1012 */
1013 final public static function extensionFromPath( $path ) {
1014 $i = strrpos( $path, '.' );
1015 return strtolower( $i ? substr( $path, $i + 1 ) : '' );
1016 }
1017 }