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