Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.30% covered (success)
98.30%
173 / 176
96.30% covered (success)
96.30%
52 / 54
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileBackend
98.86% covered (success)
98.86%
173 / 175
96.30% covered (success)
96.30%
52 / 54
111
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
10
 header
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resetOutputBuffer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDomainId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReadOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReadOnlyReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doOperations
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 doOperationsInternal
n/a
0 / 0
n/a
0 / 0
0
 doOperation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 store
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 move
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 describe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doQuickOperations
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 doQuickOperationsInternal
n/a
0 / 0
n/a
0 / 0
0
 doQuickOperation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickCreate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickStore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickMove
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quickDescribe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 concatenate
n/a
0 / 0
n/a
0 / 0
0
 prepare
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doPrepare
n/a
0 / 0
n/a
0 / 0
0
 secure
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doSecure
n/a
0 / 0
n/a
0 / 0
0
 publish
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doPublish
n/a
0 / 0
n/a
0 / 0
0
 clean
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 doClean
n/a
0 / 0
n/a
0 / 0
0
 fileExists
n/a
0 / 0
n/a
0 / 0
0
 getFileTimestamp
n/a
0 / 0
n/a
0 / 0
0
 getFileContents
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFileContentsMulti
n/a
0 / 0
n/a
0 / 0
0
 getFileXAttributes
n/a
0 / 0
n/a
0 / 0
0
 getFileSize
n/a
0 / 0
n/a
0 / 0
0
 getFileStat
n/a
0 / 0
n/a
0 / 0
0
 getFileSha1Base36
n/a
0 / 0
n/a
0 / 0
0
 getFileProps
n/a
0 / 0
n/a
0 / 0
0
 streamFile
n/a
0 / 0
n/a
0 / 0
0
 getLocalReference
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLocalReferenceMulti
n/a
0 / 0
n/a
0 / 0
0
 getLocalCopy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLocalCopyMulti
n/a
0 / 0
n/a
0 / 0
0
 getFileHttpUrl
n/a
0 / 0
n/a
0 / 0
0
 directoryExists
n/a
0 / 0
n/a
0 / 0
0
 getDirectoryList
n/a
0 / 0
n/a
0 / 0
0
 getTopDirectoryList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileList
n/a
0 / 0
n/a
0 / 0
0
 getTopFileList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preloadCache
n/a
0 / 0
n/a
0 / 0
0
 clearCache
n/a
0 / 0
n/a
0 / 0
0
 preloadFileStat
n/a
0 / 0
n/a
0 / 0
0
 lockFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unlockFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getScopedFileLocks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getScopedLocksForOps
n/a
0 / 0
n/a
0 / 0
0
 getRootStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveFSFileObjects
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isStoragePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 splitStoragePath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 normalizeStoragePath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 parentStoragePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 extensionFromPath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 isPathTraversalFree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeContentDisposition
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 normalizeContainerPath
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 newStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 wrapStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 scopedProfileSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 resetOutputBufferTheDefaultWay
n/a
0 / 0
n/a
0 / 0
3
 getStreamerOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @defgroup FileBackend File backend
4 *
5 * File backend is used to interact with file storage systems,
6 * such as the local file system, NFS, or cloud storage systems.
7 * See [the architecture doc](@ref filebackendarch) for more information.
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 */
31
32namespace Wikimedia\FileBackend;
33
34use FSFile;
35use InvalidArgumentException;
36use LockManager;
37use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
38use NullLockManager;
39use Psr\Log\LoggerAwareInterface;
40use Psr\Log\LoggerInterface;
41use Psr\Log\NullLogger;
42use ScopedLock;
43use StatusValue;
44use TempFSFile;
45use Wikimedia\ScopedCallback;
46
47/**
48 * @brief Base class for all file backend classes (including multi-write backends).
49 *
50 * This class defines the methods as abstract that subclasses must implement.
51 * Outside callers can assume that all backends will have these functions.
52 *
53 * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
54 * The "backend" portion is unique name for the application to refer to a backend, while
55 * the "container" portion is a top-level directory of the backend. The "path" portion
56 * is a relative path that uses UNIX file system (FS) notation, though any particular
57 * backend may not actually be using a local filesystem. Therefore, the relative paths
58 * are only virtual.
59 *
60 * Backend contents are stored under "domain"-specific container names by default.
61 * A domain is simply a logical umbrella for entities, such as those belonging to a certain
62 * application or portion of a website, for example. A domain can be local or global.
63 * Global (qualified) backends are achieved by configuring the "domain ID" to a constant.
64 * Global domains are simpler, but local domains can be used by choosing a domain ID based on
65 * the current context, such as which language of a website is being used.
66 *
67 * For legacy reasons, the FSFileBackend class allows manually setting the paths of
68 * containers to ones that do not respect the "domain ID".
69 *
70 * In key/value (object) stores, containers are the only hierarchy (the rest is emulated).
71 * FS-based backends are somewhat more restrictive due to the existence of real
72 * directory files; a regular file cannot have the same name as a directory. Other
73 * backends with virtual directories may not have this limitation. Callers should
74 * store files in such a way that no files and directories are under the same path.
75 *
76 * In general, this class allows for callers to access storage through the same
77 * interface, without regard to the underlying storage system. However, calling code
78 * must follow certain patterns and be aware of certain things to ensure compatibility:
79 *   - a) Always call prepare() on the parent directory before trying to put a file there;
80 *        key/value stores only need the container to exist first, but filesystems need
81 *        all the parent directories to exist first (prepare() is aware of all this)
82 *   - b) Always call clean() on a directory when it might become empty to avoid empty
83 *        directory buildup on filesystems; key/value stores never have empty directories,
84 *        so doing this helps preserve consistency in both cases
85 *   - c) Likewise, do not rely on the existence of empty directories for anything;
86 *        calling directoryExists() on a path that prepare() was previously called on
87 *        will return false for key/value stores if there are no files under that path
88 *   - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
89 *        either be a copy of the source file in /tmp or the original source file itself
90 *   - e) Use a file layout that results in never attempting to store files over directories
91 *        or directories over files; key/value stores allow this but filesystems do not
92 *   - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
93 *   - g) Do not assume that move operations are atomic (difficult with key/value stores)
94 *   - h) Do not assume that file stat or read operations always have immediate consistency;
95 *        various methods have a "latest" flag that should always be used if up-to-date
96 *        information is required (this trades performance for correctness as needed)
97 *   - i) Do not assume that directory listings have immediate consistency
98 *
99 * Methods of subclasses should avoid throwing exceptions at all costs.
100 * As a corollary, external dependencies should be kept to a minimum.
101 *
102 * See [the architecture doc](@ref filebackendarch) for more information.
103 *
104 * @stable to extend
105 *
106 * @ingroup FileBackend
107 * @since 1.19
108 */
109abstract class FileBackend implements LoggerAwareInterface {
110    /** @var string Unique backend name */
111    protected $name;
112
113    /** @var string Unique domain name */
114    protected $domainId;
115
116    /** @var string Read-only explanation message */
117    protected $readOnly;
118
119    /** @var string When to do operations in parallel */
120    protected $parallelize;
121
122    /** @var int How many operations can be done in parallel */
123    protected $concurrency;
124
125    /** @var TempFSFileFactory */
126    protected $tmpFileFactory;
127
128    /** @var LockManager */
129    protected $lockManager;
130    /** @var LoggerInterface */
131    protected $logger;
132    /** @var callable|null */
133    protected $profiler;
134
135    /** @var callable */
136    private $obResetFunc;
137    /** @var callable */
138    private $headerFunc;
139    /** @var array Option map for use with HTTPFileStreamer */
140    protected $streamerOptions;
141    /** @var callable|null */
142    protected $statusWrapper;
143
144    /** Bitfield flags for supported features */
145    public const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
146    public const ATTR_METADATA = 2; // files can be stored with metadata key/values
147    public const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
148
149    /** @var false Idiom for "no info; non-existant file" (since 1.34) */
150    protected const STAT_ABSENT = false;
151
152    /** @var null Idiom for "no info; I/O errors" (since 1.34) */
153    public const STAT_ERROR = null;
154    /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */
155    public const LIST_ERROR = null;
156    /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */
157    public const TEMPURL_ERROR = null;
158    /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */
159    public const EXISTENCE_ERROR = null;
160
161    /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */
162    public const TIMESTAMP_FAIL = false;
163    /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */
164    public const CONTENT_FAIL = false;
165    /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */
166    public const XATTRS_FAIL = false;
167    /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */
168    public const SIZE_FAIL = false;
169    /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */
170    public const SHA1_FAIL = false;
171
172    /**
173     * Create a new backend instance from configuration.
174     * This should only be called from within FileBackendGroup.
175     * @stable to call
176     *
177     * @param array $config Parameters include:
178     *   - name : The unique name of this backend.
179     *      This should consist of alphanumberic, '-', and '_' characters.
180     *      This name should not be changed after use.
181     *      Note that the name is *not* used in actual container names.
182     *   - domainId : Prefix to container names that is unique to this backend.
183     *      It should only consist of alphanumberic, '-', and '_' characters.
184     *      This ID is what avoids collisions if multiple logical backends
185     *      use the same storage system, so this should be set carefully.
186     *   - lockManager : LockManager object to use for any file locking.
187     *      If not provided, then no file locking will be enforced.
188     *   - readOnly : Write operations are disallowed if this is a non-empty string.
189     *      It should be an explanation for the backend being read-only.
190     *   - parallelize : When to do file operations in parallel (when possible).
191     *      Allowed values are "implicit", "explicit" and "off".
192     *   - concurrency : How many file operations can be done in parallel.
193     *   - tmpDirectory : Directory to use for temporary files.
194     *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if
195     *      tmpDirectory is not set. If both are unset or null, then the backend will
196     *      try to discover a usable temporary directory.
197     *   - obResetFunc : alternative callback to clear the output buffer
198     *   - streamMimeFunc : alternative method to determine the content type from the path
199     *   - headerFunc : alternative callback for sending response headers
200     *   - logger : Optional PSR logger object.
201     *   - profiler : Optional callback that takes a section name argument and returns
202     *      a ScopedCallback instance that ends the profile section in its destructor.
203     *   - statusWrapper : Optional callback that is used to wrap returned StatusValues
204     * @throws \InvalidArgumentException
205     */
206    public function __construct( array $config ) {
207        if ( !array_key_exists( 'name', $config ) ) {
208            throw new InvalidArgumentException( 'Backend name not specified.' );
209        }
210        $this->name = $config['name'];
211        $this->domainId = $config['domainId'] // e.g. "my_wiki-en_"
212            ?? $config['wikiId'] // b/c alias
213            ?? null;
214        if ( !is_string( $this->name ) || !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
215            throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." );
216        }
217        if ( !is_string( $this->domainId ) ) {
218            throw new InvalidArgumentException(
219                "Backend domain ID not provided for '{$this->name}'." );
220        }
221        $this->lockManager = $config['lockManager'] ?? new NullLockManager( [] );
222        $this->readOnly = isset( $config['readOnly'] )
223            ? (string)$config['readOnly']
224            : '';
225        $this->parallelize = isset( $config['parallelize'] )
226            ? (string)$config['parallelize']
227            : 'off';
228        $this->concurrency = isset( $config['concurrency'] )
229            ? (int)$config['concurrency']
230            : 50;
231        $this->obResetFunc = $config['obResetFunc']
232            ?? [ self::class, 'resetOutputBufferTheDefaultWay' ];
233        $this->headerFunc = $config['headerFunc'] ?? 'header';
234        $this->streamerOptions = [
235            'obResetFunc' => $this->obResetFunc,
236            'headerFunc' => $this->headerFunc,
237            'streamMimeFunc' => $config['streamMimeFunc'] ?? null,
238        ];
239
240        $this->profiler = $config['profiler'] ?? null;
241        if ( !is_callable( $this->profiler ) ) {
242            $this->profiler = null;
243        }
244        $this->logger = $config['logger'] ?? new NullLogger();
245        $this->statusWrapper = $config['statusWrapper'] ?? null;
246        // tmpDirectory gets precedence for backward compatibility
247        if ( isset( $config['tmpDirectory'] ) ) {
248            $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] );
249        } else {
250            $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory();
251        }
252    }
253
254    protected function header( $header ) {
255        ( $this->headerFunc )( $header );
256    }
257
258    protected function resetOutputBuffer() {
259        // By default, this ends up calling $this->defaultOutputBufferReset
260        ( $this->obResetFunc )();
261    }
262
263    public function setLogger( LoggerInterface $logger ) {
264        $this->logger = $logger;
265    }
266
267    /**
268     * Get the unique backend name
269     *
270     * We may have multiple different backends of the same type.
271     * For example, we can have two Swift backends using different proxies.
272     *
273     * @return string
274     */
275    final public function getName() {
276        return $this->name;
277    }
278
279    /**
280     * Get the domain identifier used for this backend (possibly empty).
281     *
282     * @return string
283     * @since 1.28
284     */
285    final public function getDomainId() {
286        return $this->domainId;
287    }
288
289    /**
290     * Alias to getDomainId()
291     *
292     * @return string
293     * @since 1.20
294     * @deprecated Since 1.34 Use getDomainId()
295     */
296    final public function getWikiId() {
297        return $this->getDomainId();
298    }
299
300    /**
301     * Check if this backend is read-only
302     *
303     * @return bool
304     */
305    final public function isReadOnly() {