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.
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.
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.
21 * Methods should avoid throwing exceptions at all costs.
22 * As a corollary, external dependencies should be kept to a minimum.
24 * @ingroup FileBackend
27 abstract class FileBackendBase
{
28 protected $name; // unique backend name
29 protected $wikiId; // unique wiki name
30 /** @var LockManager */
31 protected $lockManager;
34 * Build a new object from configuration.
35 * This should only be called from within FileBackendGroup.
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
42 * @param $config Array
44 public function __construct( array $config ) {
45 $this->name
= $config['name'];
46 $this->wikiId
= isset( $config['wikiId'] )
49 $this->lockManager
= LockManagerGroup
::singleton()->get( $config['lockManager'] );
53 * Get the unique backend name.
55 * We may have multiple different backends of the same type.
56 * For example, we can have two Swift backends using different proxies.
60 final public function getName() {
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.
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.
72 * Supported operations and their parameters:
73 * a) Create a new file in storage with the contents of a string
76 * 'dst' => <storage path>,
77 * 'content' => <string of new file contents>,
78 * 'overwriteDest' => <boolean>,
79 * 'overwriteSame' => <boolean>
81 * b) Copy a file system file into storage
84 * 'src' => <file system path>,
85 * 'dst' => <storage path>,
86 * 'overwriteDest' => <boolean>,
87 * 'overwriteSame' => <boolean>
89 * c) Copy a file within storage
92 * 'src' => <storage path>,
93 * 'dst' => <storage path>,
94 * 'overwriteDest' => <boolean>,
95 * 'overwriteSame' => <boolean>
97 * d) Move a file within storage
100 * 'src' => <storage path>,
101 * 'dst' => <storage path>,
102 * 'overwriteDest' => <boolean>,
103 * 'overwriteSame' => <boolean>
105 * e) Delete a file within storage
108 * 'src' => <storage path>,
109 * 'ignoreMissingSource' => <boolean>
111 * f) Concatenate a list of files into a single file within storage
113 * 'op' => 'concatenate',
114 * 'srcs' => <ordered array of storage paths>,
115 * 'dst' => <storage path>,
116 * 'overwriteDest' => <boolean>
118 * g) Do nothing (no-op)
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.
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.
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.
143 * @param $ops Array List of operations to execute in order
144 * @param $opts Array Batch operation options
147 abstract public function doOperations( array $ops, array $opts = array() );
150 * Same as doOperations() except it takes a single operation array
156 final public function doOperation( array $op, array $opts = array() ) {
157 return $this->doOperations( array( $op ), $opts );
161 * Prepare a storage path for usage. This will create containers
162 * that don't yet exist or, on FS backends, create parent directories.
165 * dir : storage directory
167 * @param $params Array
170 abstract public function prepare( array $params );
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.
180 * dir : storage directory
181 * noAccess : try to deny file access
182 * noListing : try to deny file listing
184 * @param $params Array
187 abstract public function secure( array $params );
190 * Clean up an empty storage directory.
191 * On FS backends, the directory will be deleted. Others may do nothing.
194 * dir : storage directory
196 * @param $params Array
199 abstract public function clean( array $params );
202 * Check if a file exists at a storage path in the backend.
205 * src : source storage path
207 * @param $params Array
210 abstract public function fileExists( array $params );
213 * Get a SHA-1 hash of the file at a storage path in the backend.
216 * src : source storage path
218 * @param $params Array
219 * @return string|false Hash string or false on failure
221 abstract public function getFileSha1Base36( array $params );
224 * Get the last-modified timestamp of the file at a storage path.
227 * src : source storage path
229 * @param $params Array
230 * @return string|false TS_MW timestamp or false on failure
232 abstract public function getFileTimestamp( array $params );
235 * Get the properties of the file at a storage path in the backend.
236 * Returns FSFile::placeholderProps() on failure.
239 * src : source storage path
241 * @param $params Array
244 abstract public function getFileProps( array $params );
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.
253 * src : source storage path
254 * headers : additional HTTP headers to send on success
256 * @param $params Array
259 abstract public function streamFile( array $params );
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.
269 * dir : storage path directory
271 * @return Traversable|Array|null Returns null on failure
273 abstract public function getFileList( array $params );
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.
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.
288 * src : source storage path
290 * @param $params Array
291 * @return FSFile|null Returns null on failure
293 abstract public function getLocalReference( array $params );
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.
301 * src : source storage path
303 * @param $params Array
304 * @return TempFSFile|null Returns null on failure
306 abstract public function getLocalCopy( array $params );
309 * Lock the files at the given storage paths in the backend.
310 * This will either lock all the files or none (on failure).
312 * Callers should consider using getScopedFileLocks() instead.
314 * @param $paths Array Storage paths
315 * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH
318 final public function lockFiles( array $paths, $type ) {
319 return $this->lockManager
->lock( $paths, $type );
323 * Unlock the files at the given storage paths in the backend.
325 * @param $paths Array Storage paths
326 * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH
329 final public function unlockFiles( array $paths, $type ) {
330 return $this->lockManager
->unlock( $paths, $type );
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.
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.
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
346 final public function getScopedFileLocks( array $paths, $type, Status
$status ) {
347 return ScopedLock
::factory( $this->lockManager
, $paths, $type, $status );
352 * Base class for all single-write backends.
353 * This class defines the methods as abstract that subclasses must implement.
355 * @ingroup FileBackend
358 abstract class FileBackend
extends FileBackendBase
{
360 protected $cache = array(); // (storage path => key => value)
361 protected $maxCacheSize = 50; // integer; max paths with entries
364 * Store a file into the backend from a file on disk.
365 * Do not call this function from places outside FileBackend and FileOp.
367 * src : source path on disk
368 * dst : destination storage path
369 * overwriteDest : do nothing and pass if an identical file exists at destination
371 * @param $params Array
374 final public function store( array $params ) {
375 $status = $this->doStore( $params );
376 $this->clearCache( array( $params['dst'] ) );
381 * @see FileBackend::store()
383 abstract protected function doStore( array $params );
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.
389 * src : source storage path
390 * dst : destination storage path
391 * overwriteDest : do nothing and pass if an identical file exists at destination
393 * @param $params Array
396 final public function copy( array $params ) {
397 $status = $this->doCopy( $params );
398 $this->clearCache( array( $params['dst'] ) );
403 * @see FileBackend::copy()
405 abstract protected function doCopy( array $params );
408 * Delete a file at the storage path.
409 * Do not call this function from places outside FileBackend and FileOp.
411 * src : source storage path
413 * @param $params Array
416 final public function delete( array $params ) {
417 $status = $this->doDelete( $params );
418 $this->clearCache( array( $params['src'] ) );
423 * @see FileBackend::delete()
425 abstract protected function doDelete( array $params );
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.
431 * src : source storage path
432 * dst : destination storage path
433 * overwriteDest : do nothing and pass if an identical file exists at destination
435 * @param $params Array
438 final public function move( array $params ) {
439 $status = $this->doMove( $params );
440 $this->clearCache( array( $params['src'], $params['dst'] ) );
445 * @see FileBackend::move()
447 protected function doMove( array $params ) {
448 // Copy source to dest
449 $status = $this->backend
->copy( $params );
450 if ( !$status->isOK() ) {
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
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.
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
467 * @param $params Array
470 final public function concatenate( array $params ) {
471 $status = $this->doConcatenate( $params );
472 $this->clearCache( array( $params['dst'] ) );
477 * @see FileBackend::concatenate()
479 abstract protected function doConcatenate( array $params );
482 * Create a file in the backend with the given contents.
483 * Do not call this function from places outside FileBackend and FileOp.
485 * content : the raw file contents
486 * dst : destination storage path
487 * overwriteDest : do nothing and pass if an identical file exists at destination
489 * @param $params Array
492 final public function create( array $params ) {
493 $status = $this->doCreate( $params );
494 $this->clearCache( array( $params['dst'] ) );
499 * @see FileBackend::create()
501 abstract protected function doCreate( array $params );
504 * @see FileBackendBase::prepare()
506 public function prepare( array $params ) {
507 return Status
::newGood();
511 * @see FileBackendBase::secure()
513 public function secure( array $params ) {
514 return Status
::newGood();
518 * @see FileBackendBase::clean()
520 public function clean( array $params ) {
521 return Status
::newGood();
525 * @see FileBackendBase::getFileSha1Base36()
527 public function getFileSha1Base36( array $params ) {
528 $path = $params['src'];
529 if ( isset( $this->cache
[$path]['sha1'] ) ) {
530 return $this->cache
[$path]['sha1'];
532 $fsFile = $this->getLocalReference( array( 'src' => $path ) );
536 $sha1 = $fsFile->getSha1Base36();
537 if ( $sha1 !== false ) { // don't cache negatives
538 $this->trimCache(); // limit memory
539 $this->cache
[$path]['sha1'] = $sha1;
546 * @see FileBackendBase::getFileProps()
548 public function getFileProps( array $params ) {
549 $fsFile = $this->getLocalReference( array( 'src' => $params['src'] ) );
551 return FSFile
::placeholderProps();
553 return $fsFile->getProps();
558 * @see FileBackendBase::getLocalReference()
560 public function getLocalReference( array $params ) {
561 return $this->getLocalCopy( $params );
565 * @see FileBackendBase::streamFile()
567 function streamFile( array $params ) {
568 $status = Status
::newGood();
570 $fsFile = $this->getLocalReference( array( 'src' => $params['src'] ) );
572 $status->fatal( 'backend-fail-stream', $params['src'] );
576 $extraHeaders = isset( $params['headers'] )
580 $ok = StreamFile
::stream( $fsFile->getPath(), $extraHeaders, false );
582 $status->fatal( 'backend-fail-stream', $params['src'] );
590 * Get the list of supported operations and their corresponding FileOp classes.
594 protected function supportedOperations() {
596 'store' => 'StoreFileOp',
597 'copy' => 'CopyFileOp',
598 'move' => 'MoveFileOp',
599 'delete' => 'DeleteFileOp',
600 'concatenate' => 'ConcatenateFileOp',
601 'create' => 'CreateFileOp',
602 'null' => 'NullFileOp'
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.
611 * @param $ops Array Same format as doOperations()
612 * @return Array List of FileOp objects
613 * @throws MWException
615 final public function getOperations( array $ops ) {
616 $supportedOps = $this->supportedOperations();
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 );
629 throw new MWException( "Operation `$opName` is not supported." );
637 * @see FileBackendBase::doOperations()
639 final public function doOperations( array $ops, array $opts = array() ) {
640 $status = Status
::newGood();
642 // Build up a list of FileOps...
643 $performOps = $this->getOperations( $ops );
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() );
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
660 // Clear any cache entries (after locks acquired)
662 // Actually attempt the operation batch...
663 $status->merge( FileOp
::attemptBatch( $performOps, $opts ) );
669 * Invalidate the file existence and property cache
671 * @param $paths Array Clear cache for specific files
674 final public function clearCache( array $paths = null ) {
675 if ( $paths === null ) {
676 $this->cache
= array();
678 foreach ( $paths as $path ) {
679 unset( $this->cache
[$path] );
685 * Prune the cache if it is too big to add an item
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] );
698 * Check if a given path is a mwstore:// path.
699 * This does not do any actual validation or existence checks.
701 * @param $path string
704 final public static function isStoragePath( $path ) {
705 return ( strpos( $path, 'mwstore://' ) === 0 );
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.
712 * @param $storagePath string
713 * @return Array (backend, container, rel object) or (null, null, null)
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"
725 return array( null, null, null );
729 * Validate a container name.
730 * Null is returned if the name has illegal characters.
732 * @param $container string
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 );
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.
747 * @param $path string
748 * @return string|null
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 ) {
758 strpos( $path, './' ) === 0 ||
759 strpos( $path, '../' ) === 0 ||
760 strpos( $path, '/./' ) !== false ||
761 strpos( $path, '/../' ) !== false
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.
774 * @param $storagePath string
775 * @return Array (container, object name) or (null, null) if path is invalid
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 );
794 return array( null, null );
798 * Get the full container name, including the wiki ID prefix
800 * @param $container string
803 final protected function fullContainerName( $container ) {
804 if ( $this->wikiId
!= '' ) {
805 return "{$this->wikiId}-$container";
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.
816 * @param $container string
817 * @return string|null
819 protected function resolveContainerName( $container ) {
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).
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
832 protected function resolveContainerPath( $container, $relStoragePath ) {
833 return $relStoragePath;