Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
166 / 166 |
|
100.00% |
51 / 51 |
CRAP | |
100.00% |
1 / 1 |
FileBackend | |
100.00% |
166 / 166 |
|
100.00% |
51 / 51 |
108 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
10 | |||
setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDomainId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getWikiId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isReadOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReadOnlyReason | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getFeatures | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasFeatures | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doOperations | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
doOperationsInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
doOperation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
create | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
store | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
copy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
move | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
delete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
describe | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doQuickOperations | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
doQuickOperationsInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
doQuickOperation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickCreate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickStore | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickCopy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickMove | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickDelete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
quickDescribe | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
concatenate | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
prepare | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
doPrepare | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
secure | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
doSecure | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
publish | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
doPublish | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
clean | |
100.00% |
4 / 4 |
|
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% |
2 / 2 |
|
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% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getLocalReferenceMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getLocalCopy | |
100.00% |
2 / 2 |
|
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% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFileList | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getTopFileList | |
100.00% |
1 / 1 |
|
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% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unlockFiles | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getScopedFileLocks | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getScopedLocksForOps | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getRootStoragePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContainerStoragePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resolveFSFileObjects | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
isStoragePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
splitStoragePath | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 | |||
normalizeStoragePath | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
parentStoragePath | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
extensionFromPath | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
isPathTraversalFree | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeContentDisposition | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
normalizeContainerPath | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
8 | |||
newStatus | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
wrapStatus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
scopedProfileSection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
resetOutputBuffer | n/a |
0 / 0 |
n/a |
0 / 0 |
3 |
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 | use MediaWiki\FileBackend\FSFile\TempFSFileFactory; |
32 | use Psr\Log\LoggerAwareInterface; |
33 | use Psr\Log\LoggerInterface; |
34 | use Psr\Log\NullLogger; |
35 | use Wikimedia\ScopedCallback; |
36 | |
37 | /** |
38 | * @brief Base class for all file backend classes (including multi-write backends). |
39 | * |
40 | * This class defines the methods as abstract that subclasses must implement. |
41 | * Outside callers can assume that all backends will have these functions. |
42 | * |
43 | * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>". |
44 | * The "backend" portion is unique name for the application to refer to a backend, while |
45 | * the "container" portion is a top-level directory of the backend. The "path" portion |
46 | * is a relative path that uses UNIX file system (FS) notation, though any particular |
47 | * backend may not actually be using a local filesystem. Therefore, the relative paths |
48 | * are only virtual. |
49 | * |
50 | * Backend contents are stored under "domain"-specific container names by default. |
51 | * A domain is simply a logical umbrella for entities, such as those belonging to a certain |
52 | * application or portion of a website, for example. A domain can be local or global. |
53 | * Global (qualified) backends are achieved by configuring the "domain ID" to a constant. |
54 | * Global domains are simpler, but local domains can be used by choosing a domain ID based on |
55 | * the current context, such as which language of a website is being used. |
56 | * |
57 | * For legacy reasons, the FSFileBackend class allows manually setting the paths of |
58 | * containers to ones that do not respect the "domain ID". |
59 | * |
60 | * In key/value (object) stores, containers are the only hierarchy (the rest is emulated). |
61 | * FS-based backends are somewhat more restrictive due to the existence of real |
62 | * directory files; a regular file cannot have the same name as a directory. Other |
63 | * backends with virtual directories may not have this limitation. Callers should |
64 | * store files in such a way that no files and directories are under the same path. |
65 | * |
66 | * In general, this class allows for callers to access storage through the same |
67 | * interface, without regard to the underlying storage system. However, calling code |
68 | * must follow certain patterns and be aware of certain things to ensure compatibility: |
69 | * - a) Always call prepare() on the parent directory before trying to put a file there; |
70 | * key/value stores only need the container to exist first, but filesystems need |
71 | * all the parent directories to exist first (prepare() is aware of all this) |
72 | * - b) Always call clean() on a directory when it might become empty to avoid empty |
73 | * directory buildup on filesystems; key/value stores never have empty directories, |
74 | * so doing this helps preserve consistency in both cases |
75 | * - c) Likewise, do not rely on the existence of empty directories for anything; |
76 | * calling directoryExists() on a path that prepare() was previously called on |
77 | * will return false for key/value stores if there are no files under that path |
78 | * - d) Never alter the resulting FSFile returned from getLocalReference(), as it could |
79 | * either be a copy of the source file in /tmp or the original source file itself |
80 | * - e) Use a file layout that results in never attempting to store files over directories |
81 | * or directories over files; key/value stores allow this but filesystems do not |
82 | * - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows |
83 | * - g) Do not assume that move operations are atomic (difficult with key/value stores) |
84 | * - h) Do not assume that file stat or read operations always have immediate consistency; |
85 | * various methods have a "latest" flag that should always be used if up-to-date |
86 | * information is required (this trades performance for correctness as needed) |
87 | * - i) Do not assume that directory listings have immediate consistency |
88 | * |
89 | * Methods of subclasses should avoid throwing exceptions at all costs. |
90 | * As a corollary, external dependencies should be kept to a minimum. |
91 | * |
92 | * See [the architecture doc](@ref filebackendarch) for more information. |
93 | * |
94 | * @stable to extend |
95 | * |
96 | * @ingroup FileBackend |
97 | * @since 1.19 |
98 | */ |
99 | abstract class FileBackend implements LoggerAwareInterface { |
100 | /** @var string Unique backend name */ |
101 | protected $name; |
102 | |
103 | /** @var string Unique domain name */ |
104 | protected $domainId; |
105 | |
106 | /** @var string Read-only explanation message */ |
107 | protected $readOnly; |
108 | |
109 | /** @var string When to do operations in parallel */ |
110 | protected $parallelize; |
111 | |
112 | /** @var int How many operations can be done in parallel */ |
113 | protected $concurrency; |
114 | |
115 | /** @var TempFSFileFactory */ |
116 | protected $tmpFileFactory; |
117 | |
118 | /** @var LockManager */ |
119 | protected $lockManager; |
120 | /** @var LoggerInterface */ |
121 | protected $logger; |
122 | /** @var callable|null */ |
123 | protected $profiler; |
124 | |
125 | /** @var callable */ |
126 | protected $obResetFunc; |
127 | /** @var callable|null */ |
128 | protected $streamMimeFunc; |
129 | /** @var callable|null */ |
130 | protected $statusWrapper; |
131 | |
132 | /** Bitfield flags for supported features */ |
133 | public const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers |
134 | public const ATTR_METADATA = 2; // files can be stored with metadata key/values |
135 | public const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII) |
136 | |
137 | /** @var false Idiom for "no info; non-existant file" (since 1.34) */ |
138 | protected const STAT_ABSENT = false; |
139 | |
140 | /** @var null Idiom for "no info; I/O errors" (since 1.34) */ |
141 | public const STAT_ERROR = null; |
142 | /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */ |
143 | public const LIST_ERROR = null; |
144 | /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */ |
145 | public const TEMPURL_ERROR = null; |
146 | /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */ |
147 | public const EXISTENCE_ERROR = null; |
148 | |
149 | /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */ |
150 | public const TIMESTAMP_FAIL = false; |
151 | /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */ |
152 | public const CONTENT_FAIL = false; |
153 | /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */ |
154 | public const XATTRS_FAIL = false; |
155 | /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */ |
156 | public const SIZE_FAIL = false; |
157 | /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */ |
158 | public const SHA1_FAIL = false; |
159 | |
160 | /** |
161 | * Create a new backend instance from configuration. |
162 | * This should only be called from within FileBackendGroup. |
163 | * @stable to call |
164 | * |
165 | * @param array $config Parameters include: |
166 | * - name : The unique name of this backend. |
167 | * This should consist of alphanumberic, '-', and '_' characters. |
168 | * This name should not be changed after use. |
169 | * Note that the name is *not* used in actual container names. |
170 | * - domainId : Prefix to container names that is unique to this backend. |
171 | * It should only consist of alphanumberic, '-', and '_' characters. |
172 | * This ID is what avoids collisions if multiple logical backends |
173 | * use the same storage system, so this should be set carefully. |
174 | * - lockManager : LockManager object to use for any file locking. |
175 | * If not provided, then no file locking will be enforced. |
176 | * - readOnly : Write operations are disallowed if this is a non-empty string. |
177 | * It should be an explanation for the backend being read-only. |
178 | * - parallelize : When to do file operations in parallel (when possible). |
179 | * Allowed values are "implicit", "explicit" and "off". |
180 | * - concurrency : How many file operations can be done in parallel. |
181 | * - tmpDirectory : Directory to use for temporary files. |
182 | * - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if |
183 | * tmpDirectory is not set. If both are unset or null, then the backend will |
184 | * try to discover a usable temporary directory. |
185 | * - obResetFunc : alternative callback to clear the output buffer |
186 | * - streamMimeFunc : alternative method to determine the content type from the path |
187 | * - logger : Optional PSR logger object. |
188 | * - profiler : Optional callback that takes a section name argument and returns |
189 | * a ScopedCallback instance that ends the profile section in its destructor. |
190 | * - statusWrapper : Optional callback that is used to wrap returned StatusValues |
191 | * @throws InvalidArgumentException |
192 | */ |
193 | public function __construct( array $config ) { |
194 | if ( !array_key_exists( 'name', $config ) ) { |
195 | throw new InvalidArgumentException( 'Backend name not specified.' ); |
196 | } |
197 | $this->name = $config['name']; |
198 | $this->domainId = $config['domainId'] // e.g. "my_wiki-en_" |
199 | ?? $config['wikiId'] // b/c alias |
200 | ?? null; |
201 | if ( !is_string( $this->name ) || !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { |
202 | throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." ); |
203 | } |
204 | if ( !is_string( $this->domainId ) ) { |
205 | throw new InvalidArgumentException( |
206 | "Backend domain ID not provided for '{$this->name}'." ); |
207 | } |
208 | $this->lockManager = $config['lockManager'] ?? new NullLockManager( [] ); |
209 | $this->readOnly = isset( $config['readOnly'] ) |
210 | ? (string)$config['readOnly'] |
211 | : ''; |
212 | $this->parallelize = isset( $config['parallelize'] ) |
213 | ? (string)$config['parallelize'] |
214 | : 'off'; |
215 | $this->concurrency = isset( $config['concurrency'] ) |
216 | ? (int)$config['concurrency'] |
217 | : 50; |
218 | $this->obResetFunc = $config['obResetFunc'] ?? [ $this, 'resetOutputBuffer' ]; |
219 | $this->streamMimeFunc = $config['streamMimeFunc'] ?? null; |
220 | |
221 | $this->profiler = $config['profiler'] ?? null; |
222 | if ( !is_callable( $this->profiler ) ) { |
223 | $this->profiler = null; |
224 | } |
225 | $this->logger = $config['logger'] ?? new NullLogger(); |
226 | $this->statusWrapper = $config['statusWrapper'] ?? null; |
227 | // tmpDirectory gets precedence for backward compatibility |
228 | if ( isset( $config['tmpDirectory'] ) ) { |
229 | $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] ); |
230 | } else { |
231 | $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory(); |
232 | } |
233 | } |
234 | |
235 | public function setLogger( LoggerInterface $logger ) { |
236 | $this->logger = $logger; |
237 | } |
238 | |
239 | /** |
240 | * Get the unique backend name |
241 | * |
242 | * We may have multiple different backends of the same type. |
243 | * For example, we can have two Swift backends using different proxies. |
244 | * |
245 | * @return string |
246 | */ |
247 | final public function getName() { |
248 | return $this->name; |
249 | } |
250 | |
251 | /** |
252 | * Get the domain identifier used for this backend (possibly empty). |
253 | * |
254 | * @return string |
255 | * @since 1.28 |
256 | */ |
257 | final public function getDomainId() { |
258 | return $this->domainId; |
259 | } |
260 | |
261 | /** |
262 | * Alias to getDomainId() |
263 | * |
264 | * @return string |
265 | * @since 1.20 |
266 | * @deprecated Since 1.34 Use getDomainId() |
267 | */ |
268 | final public function getWikiId() { |
269 | return $this->getDomainId(); |
270 | } |
271 | |
272 | /** |
273 | * Check if this backend is read-only |
274 | * |
275 | * @return bool |
276 | */ |
277 | final public function isReadOnly() { |
278 | return ( $this->readOnly != '' ); |
279 | } |
280 | |
281 | /** |
282 | * Get an explanatory message if this backend is read-only |
283 | * |
284 | * @return string|bool Returns false if the backend is not read-only |
285 | */ |
286 | final public function getReadOnlyReason() { |
287 | return ( $this->readOnly != '' ) ? $this->readOnly : false; |
288 | } |
289 | |
290 | /** |
291 | * Get the a bitfield of extra features supported by the backend medium |
292 | * @stable to override |
293 | * |
294 | * @return int Bitfield of FileBackend::ATTR_* flags |
295 | * @since 1.23 |
296 | */ |
297 | public function getFeatures() { |
298 | return self::ATTR_UNICODE_PATHS; |
299 | } |
300 | |
301 | /** |
302 | * Check if the backend medium supports a field of extra features |
303 | * |
304 | * @param int $bitfield Bitfield of FileBackend::ATTR_* flags |
305 | * @return bool |
306 | * @since 1.23 |
307 | */ |
308 | final public function hasFeatures( $bitfield ) { |
309 | return ( $this->getFeatures() & $bitfield ) === $bitfield; |
310 | } |
311 | |
312 | /** |
313 | * This is the main entry point into the backend for write operations. |
314 | * Callers supply an ordered list of operations to perform as a transaction. |
315 | * Files will be locked, the stat cache cleared, and then the operations attempted. |
316 | * If any serious errors occur, all attempted operations will be rolled back. |
317 | * |
318 | * $ops is an array of arrays. The outer array holds a list of operations. |
319 | * Each inner array is a set of key value pairs that specify an operation. |
320 | * |
321 | * Supported operations and their parameters. The supported actions are: |
322 | * - create |
323 | * - store |
324 | * - copy |
325 | * - move |
326 | * - delete |
327 | * - describe (since 1.21) |
328 | * - null |
329 | * |
330 | * FSFile/TempFSFile object support was added in 1.27. |
331 | * |
332 | * a) Create a new file in storage with the contents of a string |
333 | * @code |
334 | * [ |
335 | * 'op' => 'create', |
336 | * 'dst' => <storage path>, |
337 | * 'content' => <string of new file contents>, |
338 | * 'overwrite' => <boolean>, |
339 | * 'overwriteSame' => <boolean>, |
340 | * 'headers' => <HTTP header name/value map> # since 1.21 |
341 | * ] |
342 | * @endcode |
343 | * |
344 | * b) Copy a file system file into storage |
345 | * @code |
346 | * [ |
347 | * 'op' => 'store', |
348 | * 'src' => <file system path, FSFile, or TempFSFile>, |
349 | * 'dst' => <storage path>, |
350 | * 'overwrite' => <boolean>, |
351 | * 'overwriteSame' => <boolean>, |
352 | * 'headers' => <HTTP header name/value map> # since 1.21 |
353 | * ] |
354 | * @endcode |
355 | * |
356 | * c) Copy a file within storage |
357 | * @code |
358 | * [ |
359 | * 'op' => 'copy', |
360 | * 'src' => <storage path>, |
361 | * 'dst' => <storage path>, |
362 | * 'overwrite' => <boolean>, |
363 | * 'overwriteSame' => <boolean>, |
364 | * 'ignoreMissingSource' => <boolean>, # since 1.21 |
365 | * 'headers' => <HTTP header name/value map> # since 1.21 |
366 | * ] |
367 | * @endcode |
368 | * |
369 | * d) Move a file within storage |
370 | * @code |
371 | * [ |
372 | * 'op' => 'move', |
373 | * 'src' => <storage path>, |
374 | * 'dst' => <storage path>, |
375 | * 'overwrite' => <boolean>, |
376 | * 'overwriteSame' => <boolean>, |
377 | * 'ignoreMissingSource' => <boolean>, # since 1.21 |
378 | * 'headers' => <HTTP header name/value map> # since 1.21 |
379 | * ] |
380 | * @endcode |
381 | * |
382 | * e) Delete a file within storage |
383 | * @code |
384 | * [ |
385 | * 'op' => 'delete', |
386 | * 'src' => <storage path>, |
387 | * 'ignoreMissingSource' => <boolean> |
388 | * ] |
389 | * @endcode |
390 | * |
391 | * f) Update metadata for a file within storage |
392 | * @code |
393 | * [ |
394 | * 'op' => 'describe', |
395 | * 'src' => <storage path>, |
396 | * 'headers' => <HTTP header name/value map> |
397 | * ] |
398 | * @endcode |
399 | * |
400 | * g) Do nothing (no-op) |
401 | * @code |
402 | * [ |
403 | * 'op' => 'null', |
404 | * ] |
405 | * @endcode |
406 | * |
407 | * Boolean flags for operations (operation-specific): |
408 | * - ignoreMissingSource : The operation will simply succeed and do |
409 | * nothing if the source file does not exist. |
410 | * - overwrite : Any destination file will be overwritten. |
411 | * - overwriteSame : If a file already exists at the destination with the |
412 | * same contents, then do nothing to the destination file |
413 | * instead of giving an error. This does not compare headers. |
414 | * This option is ignored if 'overwrite' is already provided. |
415 | * - headers : If supplied, the result of merging these headers with any |
416 | * existing source file headers (replacing conflicting ones) |
417 | * will be set as the destination file headers. Headers are |
418 | * deleted if their value is set to the empty string. When a |
419 | * file has headers they are included in responses to GET and |
420 | * HEAD requests to the backing store for that file. |
421 | * Header values should be no larger than 255 bytes, except for |
422 | * Content-Disposition. The system might ignore or truncate any |
423 | * headers that are too long to store (exact limits will vary). |
424 | * Backends that don't support metadata ignore this. (since 1.21) |
425 | * |
426 | * $opts is an associative of boolean flags, including: |
427 | * - force : Operation precondition errors no longer trigger an abort. |
428 | * Any remaining operations are still attempted. Unexpected |
429 | * failures may still cause remaining operations to be aborted. |
430 | * - nonLocking : No locks are acquired for the operations. |
431 | * This can increase performance for non-critical writes. |
432 | * This has no effect unless the 'force' flag is set. |
433 | * - parallelize : Try to do operations in parallel when possible. |
434 | * - bypassReadOnly : Allow writes in read-only mode. (since 1.20) |
435 | * - preserveCache : Don't clear the process cache before checking files. |
436 | * This should only be used if all entries in the process |
437 | * cache were added after the files were already locked. (since 1.20) |
438 | * |
439 | * @note Remarks on locking: |
440 | * File system paths given to operations should refer to files that are |
441 | * already locked or otherwise safe from modification from other processes. |
442 | * Normally these files will be new temp files, which should be adequate. |
443 | * |
444 | * @par Return value: |
445 | * |
446 | * This returns a Status, which contains all warnings and fatals that occurred |
447 | * during the operation. The 'failCount', 'successCount', and 'success' members |
448 | * will reflect each operation attempted. |
449 | * |
450 | * The StatusValue will be "OK" unless: |
451 | * - a) unexpected operation errors occurred (network partitions, disk full...) |
452 | * - b) predicted operation errors occurred and 'force' was not set |
453 | * |
454 | * @param array[] $ops List of operations to execute in order |
455 | * @phan-param array<int,array{ignoreMissingSource?:bool,overwrite?:bool,overwriteSame?:bool,headers?:bool}> $ops |
456 | * @param array $opts Batch operation options |
457 | * @phpcs:ignore Generic.Files.LineLength |
458 | * @phan-param array{force?:bool,nonLocking?:bool,parallelize?:bool,bypassReadOnly?:bool,preserveCache?:bool} $opts |
459 | * @return StatusValue |
460 | */ |
461 | final public function doOperations( array $ops, array $opts = [] ) { |
462 | if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { |
463 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
464 | } |
465 | if ( $ops === [] ) { |
466 | return $this->newStatus(); // nothing to do |
467 | } |
468 | |
469 | $ops = $this->resolveFSFileObjects( $ops ); |
470 | if ( empty( $opts['force'] ) ) { |
471 | unset( $opts['nonLocking'] ); |
472 | } |
473 | |
474 | /** @noinspection PhpUnusedLocalVariableInspection */ |
475 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
476 | |
477 | return $this->doOperationsInternal( $ops, $opts ); |
478 | } |
479 | |
480 | /** |
481 | * @see FileBackend::doOperations() |
482 | * @param array $ops |
483 | * @param array $opts |
484 | * @return StatusValue |
485 | */ |
486 | abstract protected function doOperationsInternal( array $ops, array $opts ); |
487 | |
488 | /** |
489 | * Same as doOperations() except it takes a single operation. |
490 | * If you are doing a batch of operations that should either |
491 | * all succeed or all fail, then use that function instead. |
492 | * |
493 | * @see FileBackend::doOperations() |
494 | * |
495 | * @param array $op Operation |
496 | * @param array $opts Operation options |
497 | * @return StatusValue |
498 | */ |
499 | final public function doOperation( array $op, array $opts = [] ) { |
500 | return $this->doOperations( [ $op ], $opts ); |
501 | } |
502 | |
503 | /** |
504 | * Performs a single create operation. |
505 | * This sets $params['op'] to 'create' and passes it to doOperation(). |
506 | * |
507 | * @see FileBackend::doOperation() |
508 | * |
509 | * @param array $params Operation parameters |
510 | * @param array $opts Operation options |
511 | * @return StatusValue |
512 | */ |
513 | final public function create( array $params, array $opts = [] ) { |
514 | return $this->doOperation( [ 'op' => 'create' ] + $params, $opts ); |
515 | } |
516 | |
517 | /** |
518 | * Performs a single store operation. |
519 | * This sets $params['op'] to 'store' and passes it to doOperation(). |
520 | * |
521 | * @see FileBackend::doOperation() |
522 | * |
523 | * @param array $params Operation parameters |
524 | * @param array $opts Operation options |
525 | * @return StatusValue |
526 | */ |
527 | final public function store( array $params, array $opts = [] ) { |
528 | return $this->doOperation( [ 'op' => 'store' ] + $params, $opts ); |
529 | } |
530 | |
531 | /** |
532 | * Performs a single copy operation. |
533 | * This sets $params['op'] to 'copy' and passes it to doOperation(). |
534 | * |
535 | * @see FileBackend::doOperation() |
536 | * |
537 | * @param array $params Operation parameters |
538 | * @param array $opts Operation options |
539 | * @return StatusValue |
540 | */ |
541 | final public function copy( array $params, array $opts = [] ) { |
542 | return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts ); |
543 | } |
544 | |
545 | /** |
546 | * Performs a single move operation. |
547 | * This sets $params['op'] to 'move' and passes it to doOperation(). |
548 | * |
549 | * @see FileBackend::doOperation() |
550 | * |
551 | * @param array $params Operation parameters |
552 | * @param array $opts Operation options |
553 | * @return StatusValue |
554 | */ |
555 | final public function move( array $params, array $opts = [] ) { |
556 | return $this->doOperation( [ 'op' => 'move' ] + $params, $opts ); |
557 | } |
558 | |
559 | /** |
560 | * Performs a single delete operation. |
561 | * This sets $params['op'] to 'delete' and passes it to doOperation(). |
562 | * |
563 | * @see FileBackend::doOperation() |
564 | * |
565 | * @param array $params Operation parameters |
566 | * @param array $opts Operation options |
567 | * @return StatusValue |
568 | */ |
569 | final public function delete( array $params, array $opts = [] ) { |
570 | return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts ); |
571 | } |
572 | |
573 | /** |
574 | * Performs a single describe operation. |
575 | * This sets $params['op'] to 'describe' and passes it to doOperation(). |
576 | * |
577 | * @see FileBackend::doOperation() |
578 | * |
579 | * @param array $params Operation parameters |
580 | * @param array $opts Operation options |
581 | * @return StatusValue |
582 | * @since 1.21 |
583 | */ |
584 | final public function describe( array $params, array $opts = [] ) { |
585 | return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts ); |
586 | } |
587 | |
588 | /** |
589 | * Perform a set of independent file operations on some files. |
590 | * |
591 | * This does no locking, and possibly no stat calls. |
592 | * Any destination files that already exist will be overwritten. |
593 | * This should *only* be used on non-original files, like cache files. |
594 | * |
595 | * Supported operations and their parameters: |
596 | * - create |
597 | * - store |
598 | * - copy |
599 | * - move |
600 | * - delete |
601 | * - describe (since 1.21) |
602 | * - null |
603 | * |
604 | * FSFile/TempFSFile object support was added in 1.27. |
605 | * |
606 | * a) Create a new file in storage with the contents of a string |
607 | * @code |
608 | * [ |
609 | * 'op' => 'create', |
610 | * 'dst' => <storage path>, |
611 | * 'content' => <string of new file contents>, |
612 | * 'headers' => <HTTP header name/value map> # since 1.21 |
613 | * ] |
614 | * @endcode |
615 | * |
616 | * b) Copy a file system file into storage |
617 | * @code |
618 | * [ |
619 | * 'op' => 'store', |
620 | * 'src' => <file system path, FSFile, or TempFSFile>, |
621 | * 'dst' => <storage path>, |
622 | * 'headers' => <HTTP header name/value map> # since 1.21 |
623 | * ] |
624 | * @endcode |
625 | * |
626 | * c) Copy a file within storage |
627 | * @code |
628 | * [ |
629 | * 'op' => 'copy', |
630 | * 'src' => <storage path>, |
631 | * 'dst' => <storage path>, |
632 | * 'ignoreMissingSource' => <boolean>, # since 1.21 |
633 | * 'headers' => <HTTP header name/value map> # since 1.21 |
634 | * ] |
635 | * @endcode |
636 | * |
637 | * d) Move a file within storage |
638 | * @code |
639 | * [ |
640 | * 'op' => 'move', |
641 | * 'src' => <storage path>, |
642 | * 'dst' => <storage path>, |
643 | * 'ignoreMissingSource' => <boolean>, # since 1.21 |
644 | * 'headers' => <HTTP header name/value map> # since 1.21 |
645 | * ] |
646 | * @endcode |
647 | * |
648 | * e) Delete a file within storage |
649 | * @code |
650 | * [ |
651 | * 'op' => 'delete', |
652 | * 'src' => <storage path>, |
653 | * 'ignoreMissingSource' => <boolean> |
654 | * ] |
655 | * @endcode |
656 | * |
657 | * f) Update metadata for a file within storage |
658 | * @code |
659 | * [ |
660 | * 'op' => 'describe', |
661 | * 'src' => <storage path>, |
662 | * 'headers' => <HTTP header name/value map> |
663 | * ] |
664 | * @endcode |
665 | * |
666 | * g) Do nothing (no-op) |
667 | * @code |
668 | * [ |
669 | * 'op' => 'null', |
670 | * ] |
671 | * @endcode |
672 | * |
673 | * @par Boolean flags for operations (operation-specific): |
674 | * - ignoreMissingSource : The operation will simply succeed and do |
675 | * nothing if the source file does not exist. |
676 | * - headers : If supplied with a header name/value map, the backend will |
677 | * reply with these headers when GETs/HEADs of the destination |
678 | * file are made. Header values should be smaller than 256 bytes. |
679 | * Content-Disposition headers can be longer, though the system |
680 | * might ignore or truncate ones that are too long to store. |
681 | * Existing headers will remain, but these will replace any |
682 | * conflicting previous headers, and headers will be removed |
683 | * if they are set to an empty string. |
684 | * Backends that don't support metadata ignore this. (since 1.21) |
685 | * |
686 | * $opts is an associative of boolean flags, including: |
687 | * - bypassReadOnly : Allow writes in read-only mode (since 1.20) |
688 | * |
689 | * @par Return value: |
690 | * This returns a Status, which contains all warnings and fatals that occurred |
691 | * during the operation. The 'failCount', 'successCount', and 'success' members |
692 | * will reflect each operation attempted for the given files. The StatusValue will be |
693 | * considered "OK" as long as no fatal errors occurred. |
694 | * |
695 | * @param array $ops Set of operations to execute |
696 | * @phan-param list<array{op:?string,src?:string,dst?:string,ignoreMissingSource?:bool,headers?:array}> $ops |
697 | * @param array $opts Batch operation options |
698 | * @phan-param array{bypassReadOnly?:bool} $opts |
699 | * @return StatusValue |
700 | * @since 1.20 |
701 | */ |
702 | final public function doQuickOperations( array $ops, array $opts = [] ) { |
703 | if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { |
704 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
705 | } |
706 | if ( $ops === [] ) { |
707 | return $this->newStatus(); // nothing to do |
708 | } |
709 | |
710 | $ops = $this->resolveFSFileObjects( $ops ); |
711 | foreach ( $ops as &$op ) { |
712 | $op['overwrite'] = true; // avoids RTTs in key/value stores |
713 | } |
714 | |
715 | /** @noinspection PhpUnusedLocalVariableInspection */ |
716 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
717 | |
718 | return $this->doQuickOperationsInternal( $ops, $opts ); |
719 | } |
720 | |
721 | /** |
722 | * @see FileBackend::doQuickOperations() |
723 | * @param array $ops |
724 | * @param array $opts |
725 | * @return StatusValue |
726 | * @since 1.20 |
727 | */ |
728 | abstract protected function doQuickOperationsInternal( array $ops, array $opts ); |
729 | |
730 | /** |
731 | * Same as doQuickOperations() except it takes a single operation. |
732 | * If you are doing a batch of operations, then use that function instead. |
733 | * |
734 | * @see FileBackend::doQuickOperations() |
735 | * |
736 | * @param array $op Operation |
737 | * @param array $opts Batch operation options |
738 | * @return StatusValue |
739 | * @since 1.20 |
740 | */ |
741 | final public function doQuickOperation( array $op, array $opts = [] ) { |
742 | return $this->doQuickOperations( [ $op ], $opts ); |
743 | } |
744 | |
745 | /** |
746 | * Performs a single quick create operation. |
747 | * This sets $params['op'] to 'create' and passes it to doQuickOperation(). |
748 | * |
749 | * @see FileBackend::doQuickOperation() |
750 | * |
751 | * @param array $params Operation parameters |
752 | * @param array $opts Operation options |
753 | * @return StatusValue |
754 | * @since 1.20 |
755 | */ |
756 | final public function quickCreate( array $params, array $opts = [] ) { |
757 | return $this->doQuickOperation( [ 'op' => 'create' ] + $params, $opts ); |
758 | } |
759 | |
760 | /** |
761 | * Performs a single quick store operation. |
762 | * This sets $params['op'] to 'store' and passes it to doQuickOperation(). |
763 | * |
764 | * @see FileBackend::doQuickOperation() |
765 | * |
766 | * @param array $params Operation parameters |
767 | * @param array $opts Operation options |
768 | * @return StatusValue |
769 | * @since 1.20 |
770 | */ |
771 | final public function quickStore( array $params, array $opts = [] ) { |
772 | return $this->doQuickOperation( [ 'op' => 'store' ] + $params, $opts ); |
773 | } |
774 | |
775 | /** |
776 | * Performs a single quick copy operation. |
777 | * This sets $params['op'] to 'copy' and passes it to doQuickOperation(). |
778 | * |
779 | * @see FileBackend::doQuickOperation() |
780 | * |
781 | * @param array $params Operation parameters |
782 | * @param array $opts Operation options |
783 | * @return StatusValue |
784 | * @since 1.20 |
785 | */ |
786 | final public function quickCopy( array $params, array $opts = [] ) { |
787 | return $this->doQuickOperation( [ 'op' => 'copy' ] + $params, $opts ); |
788 | } |
789 | |
790 | /** |
791 | * Performs a single quick move operation. |
792 | * This sets $params['op'] to 'move' and passes it to doQuickOperation(). |
793 | * |
794 | * @see FileBackend::doQuickOperation() |
795 | * |
796 | * @param array $params Operation parameters |
797 | * @param array $opts Operation options |
798 | * @return StatusValue |
799 | * @since 1.20 |
800 | */ |
801 | final public function quickMove( array $params, array $opts = [] ) { |
802 | return $this->doQuickOperation( [ 'op' => 'move' ] + $params, $opts ); |
803 | } |
804 | |
805 | /** |
806 | * Performs a single quick delete operation. |
807 | * This sets $params['op'] to 'delete' and passes it to doQuickOperation(). |
808 | * |
809 | * @see FileBackend::doQuickOperation() |
810 | * |
811 | * @param array $params Operation parameters |
812 | * @param array $opts Operation options |
813 | * @return StatusValue |
814 | * @since 1.20 |
815 | */ |
816 | final public function quickDelete( array $params, array $opts = [] ) { |
817 | return $this->doQuickOperation( [ 'op' => 'delete' ] + $params, $opts ); |
818 | } |
819 | |
820 | /** |
821 | * Performs a single quick describe operation. |
822 | * This sets $params['op'] to 'describe' and passes it to doQuickOperation(). |
823 | * |
824 | * @see FileBackend::doQuickOperation() |
825 | * |
826 | * @param array $params Operation parameters |
827 | * @param array $opts Operation options |
828 | * @return StatusValue |
829 | * @since 1.21 |
830 | */ |
831 | final public function quickDescribe( array $params, array $opts = [] ) { |
832 | return $this->doQuickOperation( [ 'op' => 'describe' ] + $params, $opts ); |
833 | } |
834 | |
835 | /** |
836 | * Concatenate a list of storage files into a single file system file. |
837 | * The target path should refer to a file that is already locked or |
838 | * otherwise safe from modification from other processes. Normally, |
839 | * the file will be a new temp file, which should be adequate. |
840 | * |
841 | * @param array $params Operation parameters, include: |
842 | * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...) |
843 | * - dst : file system path to 0-byte temp file |
844 | * - parallelize : try to do operations in parallel when possible |
845 | * @return StatusValue |
846 | */ |
847 | abstract public function concatenate( array $params ); |
848 | |
849 | /** |
850 | * Prepare a storage directory for usage. |
851 | * This will create any required containers and parent directories. |
852 | * Backends using key/value stores only need to create the container. |
853 | * |
854 | * The 'noAccess' and 'noListing' parameters works the same as in secure(), |
855 | * except they are only applied *if* the directory/container had to be created. |
856 | * These flags should always be set for directories that have private files. |
857 | * However, setting them is not guaranteed to actually do anything. |
858 | * Additional server configuration may be needed to achieve the desired effect. |
859 | * |
860 | * @param array $params Parameters include: |
861 | * - dir : storage directory |
862 | * - noAccess : try to deny file access (since 1.20) |
863 | * - noListing : try to deny file listing (since 1.20) |
864 | * - bypassReadOnly : allow writes in read-only mode (since 1.20) |
865 | * @return StatusValue Good status without value for success, fatal otherwise. |
866 | */ |
867 | final public function prepare( array $params ) { |
868 | if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { |
869 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
870 | } |
871 | /** @noinspection PhpUnusedLocalVariableInspection */ |
872 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
873 | return $this->doPrepare( $params ); |
874 | } |
875 | |
876 | /** |
877 | * @see FileBackend::prepare() |
878 | * @param array $params |
879 | * @return StatusValue Good status without value for success, fatal otherwise. |
880 | */ |
881 | abstract protected function doPrepare( array $params ); |
882 | |
883 | /** |
884 | * Take measures to block web access to a storage directory and |
885 | * the container it belongs to. FS backends might add .htaccess |
886 | * files whereas key/value store backends might revoke container |
887 | * access to the storage user representing end-users in web requests. |
888 | * |
889 | * This is not guaranteed to actually make files or listings publicly hidden. |
890 | * Additional server configuration may be needed to achieve the desired effect. |
891 | * |
892 | * @param array $params Parameters include: |
893 | * - dir : storage directory |
894 | * - noAccess : try to deny file access |
895 | * - noListing : try to deny file listing |
896 | * - bypassReadOnly : allow writes in read-only mode (since 1.20) |
897 | * @return StatusValue |
898 | */ |
899 | final public function secure( array $params ) { |
900 | if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { |
901 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
902 | } |
903 | /** @noinspection PhpUnusedLocalVariableInspection */ |
904 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
905 | return $this->doSecure( $params ); |
906 | } |
907 | |
908 | /** |
909 | * @see FileBackend::secure() |
910 | * @param array $params |
911 | * @return StatusValue |
912 | */ |
913 | abstract protected function doSecure( array $params ); |
914 | |
915 | /** |
916 | * Remove measures to block web access to a storage directory and |
917 | * the container it belongs to. FS backends might remove .htaccess |
918 | * files whereas key/value store backends might grant container |
919 | * access to the storage user representing end-users in web requests. |
920 | * This essentially can undo the result of secure() calls. |
921 | * |
922 | * This is not guaranteed to actually make files or listings publicly viewable. |
923 | * Additional server configuration may be needed to achieve the desired effect. |
924 | * |
925 | * @param array $params Parameters include: |
926 | * - dir : storage directory |
927 | * - access : try to allow file access |
928 | * - listing : try to allow file listing |
929 | * - bypassReadOnly : allow writes in read-only mode (since 1.20) |
930 | * @return StatusValue |
931 | * @since 1.20 |
932 | */ |
933 | final public function publish( array $params ) { |
934 | if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { |
935 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
936 | } |
937 | /** @noinspection PhpUnusedLocalVariableInspection */ |
938 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
939 | return $this->doPublish( $params ); |
940 | } |
941 | |
942 | /** |
943 | * @see FileBackend::publish() |
944 | * @param array $params |
945 | * @return StatusValue |
946 | */ |
947 | abstract protected function doPublish( array $params ); |
948 | |
949 | /** |
950 | * Delete a storage directory if it is empty. |
951 | * Backends using key/value stores may do nothing unless the directory |
952 | * is that of an empty container, in which case it will be deleted. |
953 | * |
954 | * @param array $params Parameters include: |
955 | * - dir : storage directory |
956 | * - recursive : recursively delete empty subdirectories first (since 1.20) |
957 | * - bypassReadOnly : allow writes in read-only mode (since 1.20) |
958 | * @return StatusValue |
959 | */ |
960 | final public function clean( array $params ) { |
961 | if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { |
962 | return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly ); |
963 | } |
964 | /** @noinspection PhpUnusedLocalVariableInspection */ |
965 | $scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts |
966 | return $this->doClean( $params ); |
967 | } |
968 | |
969 | /** |
970 | * @see FileBackend::clean() |
971 | * @param array $params |
972 | * @return StatusValue |
973 | */ |
974 | abstract protected function doClean( array $params ); |
975 | |
976 | /** |
977 | * Check if a file exists at a storage path in the backend. |
978 | * This returns false if only a directory exists at the path. |
979 | * |
980 | * Callers that only care if a file is readily accessible can use non-strict |
981 | * comparisons on the result. If "does not exist" and "existence is unknown" |
982 | * must be distinguished, then strict comparisons to true/null should be used. |
983 | * |
984 | * @see FileBackend::EXISTENCE_ERROR |
985 | * @see FileBackend::directoryExists() |
986 | * |
987 | * @param array $params Parameters include: |
988 | * - src : source storage path |
989 | * - latest : use the latest available data |
990 | * @return bool|null Whether the file exists or null (I/O error) |
991 | */ |
992 | abstract public function fileExists( array $params ); |
993 | |
994 | /** |
995 | * Get the last-modified timestamp of the file at a storage path. |
996 | * |
997 | * @see FileBackend::TIMESTAMP_FAIL |
998 | * |
999 | * @param array $params Parameters include: |
1000 | * - src : source storage path |
1001 | * - latest : use the latest available data |
1002 | * @return string|false TS_MW timestamp or false (missing file or I/O error) |
1003 | */ |
1004 | abstract public function getFileTimestamp( array $params ); |
1005 | |
1006 | /** |
1007 | * Get the contents of a file at a storage path in the backend. |
1008 | * This should be avoided for potentially large files. |
1009 | * |
1010 | * @see FileBackend::CONTENT_FAIL |
1011 | * |
1012 | * @param array $params Parameters include: |
1013 | * - src : source storage path |
1014 | * - latest : use the latest available data |
1015 | * @return string|false Content string or false (missing file or I/O error) |
1016 | */ |
1017 | final public function getFileContents( array $params ) { |
1018 | $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); |
1019 | |
1020 | return $contents[$params['src']]; |
1021 | } |
1022 | |
1023 | /** |
1024 | * Like getFileContents() except it takes an array of storage paths |
1025 | * and returns an order preserved map of storage paths to their content. |
1026 | * |
1027 | * @see FileBackend::getFileContents() |
1028 | * |
1029 | * @param array $params Parameters include: |
1030 | * - srcs : list of source storage paths |
1031 | * - latest : use the latest available data |
1032 | * - parallelize : try to do operations in parallel when possible |
1033 | * @return string[]|false[] Map of (path name => file content or false on failure) |
1034 | * @since 1.20 |
1035 | */ |
1036 | abstract public function getFileContentsMulti( array $params ); |
1037 | |
1038 | /** |
1039 | * Get metadata about a file at a storage path in the backend. |
1040 | * If the file does not exist, then this returns false. |
1041 | * Otherwise, the result is an associative array that includes: |
1042 | * - headers : map of HTTP headers used for GET/HEAD requests (name => value) |
1043 | * - metadata : map of file metadata (name => value) |
1044 | * Metadata keys and headers names will be returned in all lower-case. |
1045 | * Additional values may be included for internal use only. |
1046 | * |
1047 | * Use FileBackend::hasFeatures() to check how well this is supported. |
1048 | * |
1049 | * @see FileBackend::XATTRS_FAIL |
1050 | * |
1051 | * @param array $params |
1052 | * $params include: |
1053 | * - src : source storage path |
1054 | * - latest : use the latest available data |
1055 | * @return array|false File metadata array or false (missing file or I/O error) |
1056 | * @since 1.23 |
1057 | */ |
1058 | abstract public function getFileXAttributes( array $params ); |
1059 | |
1060 | /** |
1061 | * Get the size (bytes) of a file at a storage path in the backend. |
1062 | * |
1063 | * @see FileBackend::SIZE_FAIL |
1064 | * |
1065 | * @param array $params Parameters include: |
1066 | * - src : source storage path |
1067 | * - latest : use the latest available data |
1068 | * @return int|false File size in bytes or false (missing file or I/O error) |
1069 | */ |
1070 | abstract public function getFileSize( array $params ); |
1071 | |
1072 | /** |
1073 | * Get quick information about a file at a storage path in the backend. |
1074 | * If the file does not exist, then this returns false. |
1075 | * Otherwise, the result is an associative array that includes: |
1076 | * - mtime : the last-modified timestamp (TS_MW) |
1077 | * - size : the file size (bytes) |
1078 | * Additional values may be included for internal use only. |
1079 | * |
1080 | * @see FileBackend::STAT_ABSENT |
1081 | * @see FileBackend::STAT_ERROR |
1082 | * |
1083 | * @param array $params Parameters include: |
1084 | * - src : source storage path |
1085 | * - latest : use the latest available data |
1086 | * @return array|false|null Attribute map, false (missing file), or null (I/O error) |
1087 | */ |
1088 | abstract public function getFileStat( array $params ); |
1089 | |
1090 | /** |
1091 | * Get a SHA-1 hash of the content of the file at a storage path in the backend. |
1092 | * |
1093 | * @see FileBackend::SHA1_FAIL |
1094 | * |
1095 | * @param array $params Parameters include: |
1096 | * - src : source storage path |
1097 | * - latest : use the latest available data |
1098 | * @return string|false Hash string or false (missing file or I/O error) |
1099 | */ |
1100 | abstract public function getFileSha1Base36( array $params ); |
1101 | |
1102 | /** |
1103 | * Get the properties of the content of the file at a storage path in the backend. |
1104 | * This gives the result of FSFile::getProps() on a local copy of the file. |
1105 | * |
1106 | * @param array $params Parameters include: |
1107 | * - src : source storage path |
1108 | * - latest : use the latest available data |
1109 | * @return array Properties map; FSFile::placeholderProps() if file missing or on I/O error |
1110 | */ |
1111 | abstract public function getFileProps( array $params ); |
1112 | |
1113 | /** |
1114 | * Stream the content of the file at a storage path in the backend. |
1115 | * |
1116 | * If the file does not exists, an HTTP 404 error will be given. |
1117 | * Appropriate HTTP headers (Status, Content-Type, Content-Length) |
1118 | * will be sent if streaming began, while none will be sent otherwise. |
1119 | * Implementations should flush the output buffer before sending data. |
1120 | * |
1121 | * @param array $params Parameters include: |
1122 | * - src : source storage path |
1123 | * - headers : list of additional HTTP headers to send if the file exists |
1124 | * - options : HTTP request header map with lower case keys (since 1.28). Supports: |
1125 | * range : format is "bytes=(\d*-\d*)" |
1126 | * if-modified-since : format is an HTTP date |
1127 | * - headless : do not send HTTP headers (including those of "headers") (since 1.28) |
1128 | * - latest : use the latest available data |
1129 | * - allowOB : preserve any output buffers (since 1.28) |
1130 | * @return StatusValue |
1131 | */ |
1132 | abstract public function streamFile( array $params ); |
1133 | |
1134 | /** |
1135 | * Returns a file system file, identical in content to the file at a storage path. |
1136 | * The file returned is either: |
1137 | * - a) A TempFSFile local copy of the file at a storage path in the backend. |
1138 | * The temporary copy will have the same extension as the source. |
1139 | * Temporary files may be purged when the file object falls out of scope. |
1140 | * - b) An FSFile pointing to the original file at a storage path in the backend. |
1141 | * This is applicable for backends layered directly on top of file systems. |
1142 | * |
1143 | * Never modify the returned file since it might be the original, it might be shared |
1144 | * among multiple callers of this method, or the backend might internally keep FSFile |
1145 | * references for deferred operations. |
1146 | * |
1147 | * @param array $params Parameters include: |
1148 | * - src : source storage path |
1149 | * - latest : use the latest available data |
1150 | * @return FSFile|null|false Local file copy or false (missing) or null (error) |
1151 | */ |
1152 | final public function getLocalReference( array $params ) { |
1153 | $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); |
1154 | |
1155 | return $fsFiles[$params['src']]; |
1156 | } |
1157 | |
1158 | /** |
1159 | * Like getLocalReference() except it takes an array of storage paths and |
1160 | * yields an order-preserved map of storage paths to temporary local file copies. |
1161 | * |
1162 | * Never modify the returned files since they might be originals, they might be shared |
1163 | * among multiple callers of this method, or the backend might internally keep FSFile |
1164 | * references for deferred operations. |
1165 | * |
1166 | * @see FileBackend::getLocalReference() |
1167 | * |
1168 | * @param array $params Parameters include: |
1169 | * - srcs : list of source storage paths |
1170 | * - latest : use the latest available data |
1171 | * - parallelize : try to do operations in parallel when possible |
1172 | * @return array Map of (path name => FSFile or false (missing) or null (error)) |
1173 | * @since 1.20 |
1174 | */ |
1175 | abstract public function getLocalReferenceMulti( array $params ); |
1176 | |
1177 | /** |
1178 | * Get a local copy on disk of the file at a storage path in the backend. |
1179 | * The temporary copy will have the same file extension as the source. |
1180 | * Temporary files may be purged when the file object falls out of scope. |
1181 | * |
1182 | * Multiple calls to this method for the same path will create new copies. |
1183 | * |
1184 | * @param array $params Parameters include: |
1185 | * - src : source storage path |
1186 | * - latest : use the latest available data |
1187 | * @return TempFSFile|null|false Temporary local file copy or false (missing) or null (error) |
1188 | */ |
1189 | final public function getLocalCopy( array $params ) { |
1190 | $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params ); |
1191 | |
1192 | return $tmpFiles[$params['src']]; |
1193 | } |
1194 | |
1195 | /** |
1196 | * Like getLocalCopy() except it takes an array of storage paths and yields |
1197 | * an order preserved-map of storage paths to temporary local file copies. |
1198 | * |
1199 | * Multiple calls to this method for the same path will create new copies. |
1200 | * |
1201 | * @see FileBackend::getLocalCopy() |
1202 | * |
1203 | * @param array $params Parameters include: |
1204 | * - srcs : list of source storage paths |
1205 | * - latest : use the latest available data |
1206 | * - parallelize : try to do operations in parallel when possible |
1207 | * @return array Map of (path name => TempFSFile or false (missing) or null (error)) |
1208 | * @since 1.20 |
1209 | */ |
1210 | abstract public function getLocalCopyMulti( array $params ); |
1211 | |
1212 | /** |
1213 | * Return an HTTP URL to a given file that requires no authentication to use. |
1214 | * The URL may be pre-authenticated (via some token in the URL) and temporary. |
1215 | * This will return null if the backend cannot make an HTTP URL for the file. |
1216 | * |
1217 | * This is useful for key/value stores when using scripts that seek around |
1218 | * large files and those scripts (and the backend) support HTTP Range headers. |
1219 | * Otherwise, one would need to use getLocalReference(), which involves loading |
1220 | * the entire file on to local disk. |
1221 | * |
1222 | * @see FileBackend::TEMPURL_ERROR |
1223 | * |
1224 | * @param array $params Parameters include: |
1225 | * - src : source storage path |
1226 | * - ttl : lifetime (seconds) if pre-authenticated; default is 1 day |
1227 | * @return string|null URL or null (not supported or I/O error) |
1228 | * @since 1.21 |
1229 | */ |
1230 | abstract public function getFileHttpUrl( array $params ); |
1231 | |
1232 | /** |
1233 | * Check if a directory exists at a given storage path |
1234 | * |
1235 | * For backends using key/value stores, a directory is said to exist whenever |
1236 | * there exist any files with paths using the given directory path as a prefix |
1237 | * followed by a forward slash. For example, if there is a file called |
1238 | * "mwstore://backend/container/dir/path.svg" then directories are said to exist |
1239 | * at "mwstore://backend/container" and "mwstore://backend/container/dir". These |
1240 | * can be thought of as "virtual" directories. |
1241 | * |
1242 | * Backends that directly use a filesystem layer might enumerate empty directories. |
1243 | * The clean() method should always be used when files are deleted or moved if this |
1244 | * is a concern. This is a trade-off to avoid write amplication/contention on file |
1245 | * changes or read amplification when calling this method. |
1246 | * |
1247 | * Storage backends with eventual consistency might return stale data. |
1248 | * |
1249 | * @see FileBackend::EXISTENCE_ERROR |
1250 | * @see FileBackend::clean() |
1251 | * |
1252 | * @param array $params Parameters include: |
1253 | * - dir : storage directory |
1254 | * @return bool|null Whether a directory exists or null (I/O error) |
1255 | * @since 1.20 |
1256 | */ |
1257 | abstract public function directoryExists( array $params ); |
1258 | |
1259 | /** |
1260 | * Get an iterator to list *all* directories under a storage directory |
1261 | * |
1262 | * If the directory is of the form "mwstore://backend/container", |
1263 | * then all directories in the container will be listed. |
1264 | * If the directory is of form "mwstore://backend/container/dir", |
1265 | * then all directories directly under that directory will be listed. |
1266 | * Results will be storage directories relative to the given directory. |
1267 | * |
1268 | * Storage backends with eventual consistency might return stale data. |
1269 | * |
1270 | * Failures during iteration can result in FileBackendError exceptions (since 1.22). |
1271 | * |
1272 | * @see FileBackend::LIST_ERROR |
1273 | * @see FileBackend::directoryExists() |
1274 | * |
1275 | * @param array $params Parameters include: |
1276 | * - dir : storage directory |
1277 | * - topOnly : only return direct child dirs of the directory |
1278 | * @return Traversable|array|null Directory list enumerator or null (initial I/O error) |
1279 | * @since 1.20 |
1280 | */ |
1281 | abstract public function getDirectoryList( array $params ); |
1282 | |
1283 | /** |
1284 | * Same as FileBackend::getDirectoryList() except only lists |
1285 | * directories that are immediately under the given directory. |
1286 | * |
1287 | * Storage backends with eventual consistency might return stale data. |
1288 | * |
1289 | * Failures during iteration can result in FileBackendError exceptions (since 1.22). |
1290 | * |
1291 | * @see FileBackend::LIST_ERROR |
1292 | * @see FileBackend::directoryExists() |
1293 | * |
1294 | * @param array $params Parameters include: |
1295 | * - dir : storage directory |
1296 | * @return Traversable|array|null Directory list enumerator or null (initial I/O error) |
1297 | * @since 1.20 |
1298 | */ |
1299 | final public function getTopDirectoryList( array $params ) { |
1300 | return $this->getDirectoryList( [ 'topOnly' => true ] + $params ); |
1301 | } |
1302 | |
1303 | /** |
1304 | * Get an iterator to list *all* stored files under a storage directory |
1305 | * |
1306 | * If the directory is of the form "mwstore://backend/container", then all |
1307 | * files in the container will be listed. If the directory is of form |
1308 | * "mwstore://backend/container/dir", then all files under that directory will |
1309 | * be listed. Results will be storage paths relative to the given directory. |
1310 | * |
1311 | * Storage backends with eventual consistency might return stale data. |
1312 | * |
1313 | * Failures during iteration can result in FileBackendError exceptions (since 1.22). |
1314 | * |
1315 | * @see FileBackend::LIST_ERROR |
1316 | * |
1317 | * @param array $params Parameters include: |
1318 | * - dir : storage directory |
1319 | * - topOnly : only return direct child files of the directory (since 1.20) |
1320 | * - adviseStat : set to true if stat requests will be made on the files (since 1.22) |
1321 | * - forWrite : true if the list will inform a write operations (since 1.41) |
1322 | * @return Traversable|array|null File list enumerator or null (initial I/O error) |
1323 | */ |
1324 | abstract public function getFileList( array $params ); |
1325 | |
1326 | /** |
1327 | * Same as FileBackend::getFileList() except only lists |
1328 | * files that are immediately under the given directory. |
1329 | * |
1330 | * Storage backends with eventual consistency might return stale data. |
1331 | * |
1332 | * Failures during iteration can result in FileBackendError exceptions (since 1.22). |
1333 | * |
1334 | * @see FileBackend::LIST_ERROR |
1335 | * |
1336 | * @param array $params Parameters include: |
1337 | * - dir : storage directory |
1338 | * - adviseStat : set to true if stat requests will be made on the files (since 1.22) |
1339 | * @return Traversable|array|null File list enumerator or null on failure |
1340 | * @since 1.20 |
1341 | */ |
1342 | final public function getTopFileList( array $params ) { |
1343 | return $this->getFileList( [ 'topOnly' => true ] + $params ); |
1344 | } |
1345 | |
1346 | /** |
1347 | * Preload persistent file stat cache and property cache into in-process cache. |
1348 | * This should be used when stat calls will be made on a known list of a many files. |
1349 | * |
1350 | * @see FileBackend::getFileStat() |
1351 | * |
1352 | * @param array $paths Storage paths |
1353 | */ |
1354 | abstract public function preloadCache( array $paths ); |
1355 | |
1356 | /** |
1357 | * Invalidate any in-process file stat and property cache. |
1358 | * If $paths is given, then only the cache for those files will be cleared. |
1359 | * |
1360 | * @see FileBackend::getFileStat() |
1361 | * |
1362 | * @param array|null $paths Storage paths (optional) |
1363 | */ |
1364 | abstract public function clearCache( array $paths = null ); |
1365 | |
1366 | /** |
1367 | * Preload file stat information (concurrently if possible) into in-process cache. |
1368 | * |
1369 | * This should be used when stat calls will be made on a known list of a many files. |
1370 | * This does not make use of the persistent file stat cache. |
1371 | * |
1372 | * @see FileBackend::getFileStat() |
1373 | * |
1374 | * @param array $params Parameters include: |
1375 | * - srcs : list of source storage paths |
1376 | * - latest : use the latest available data |
1377 | * @return bool Whether all requests proceeded without I/O errors (since 1.24) |
1378 | * @since 1.23 |
1379 | */ |
1380 | abstract public function preloadFileStat( array $params ); |
1381 | |
1382 | /** |
1383 | * Lock the files at the given storage paths in the backend. |
1384 | * This will either lock all the files or none (on failure). |
1385 | * |
1386 | * Callers should consider using getScopedFileLocks() instead. |
1387 | * |
1388 | * @param array $paths Storage paths |
1389 | * @param int $type LockManager::LOCK_* constant |
1390 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24) |
1391 | * @return StatusValue |
1392 | */ |
1393 | final public function lockFiles( array $paths, $type, $timeout = 0 ) { |
1394 | $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths ); |
1395 | |
1396 | return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) ); |
1397 | } |
1398 | |
1399 | /** |
1400 | * Unlock the files at the given storage paths in the backend. |
1401 | * |
1402 | * @param array $paths Storage paths |
1403 | * @param int $type LockManager::LOCK_* constant |
1404 | * @return StatusValue |
1405 | */ |
1406 | final public function unlockFiles( array $paths, $type ) { |
1407 | $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths ); |
1408 | |
1409 | return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) ); |
1410 | } |
1411 | |
1412 | /** |
1413 | * Lock the files at the given storage paths in the backend. |
1414 | * This will either lock all the files or none (on failure). |
1415 | * On failure, the StatusValue object will be updated with errors. |
1416 | * |
1417 | * Once the return value goes out scope, the locks will be released and |
1418 | * the StatusValue updated. Unlock fatals will not change the StatusValue "OK" value. |
1419 | * |
1420 | * @see ScopedLock::factory() |
1421 | * |
1422 | * @param array $paths List of storage paths or map of lock types to path lists |
1423 | * @param int|string $type LockManager::LOCK_* constant or "mixed" |
1424 | * @param StatusValue $status StatusValue to update on lock/unlock |
1425 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24) |
1426 | * @return ScopedLock|null RAII-style self-unlocking lock or null on failure |
1427 | */ |
1428 | final public function getScopedFileLocks( |
1429 | array $paths, $type, StatusValue $status, $timeout = 0 |
1430 | ) { |
1431 | if ( $type === 'mixed' ) { |
1432 | foreach ( $paths as &$typePaths ) { |
1433 | $typePaths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $typePaths ); |
1434 | } |
1435 | } else { |
1436 | $paths = array_map( [ __CLASS__, 'normalizeStoragePath' ], $paths ); |
1437 | } |
1438 | |
1439 | return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout ); |
1440 | } |
1441 | |
1442 | /** |
1443 | * Get an array of scoped locks needed for a batch of file operations. |
1444 | * |
1445 | * Normally, FileBackend::doOperations() handles locking, unless |
1446 | * the 'nonLocking' param is passed in. This function is useful if you |
1447 | * want the files to be locked for a broader scope than just when the |
1448 | * files are changing. For example, if you need to update DB metadata, |
1449 | * you may want to keep the files locked until finished. |
1450 | * |
1451 | * @see FileBackend::doOperations() |
1452 | * |
1453 | * @param array $ops List of file operations to FileBackend::doOperations() |
1454 | * @param StatusValue $status StatusValue to update on lock/unlock |
1455 | * @return ScopedLock|null RAII-style self-unlocking lock or null on failure |
1456 | * @since 1.20 |
1457 | */ |
1458 | abstract public function getScopedLocksForOps( array $ops, StatusValue $status ); |
1459 | |
1460 | /** |
1461 | * Get the root storage path of this backend. |
1462 | * All container paths are "subdirectories" of this path. |
1463 | * |
1464 | * @return string Storage path |
1465 | * @since 1.20 |
1466 | */ |
1467 | final public function getRootStoragePath() { |
1468 | return "mwstore://{$this->name}"; |
1469 | } |
1470 | |
1471 | /** |
1472 | * Get the storage path for the given container for this backend |
1473 | * |
1474 | * @param string $container Container name |
1475 | * @return string Storage path |
1476 | * @since 1.21 |
1477 | */ |
1478 | final public function getContainerStoragePath( $container ) { |
1479 | return $this->getRootStoragePath() . "/{$container}"; |
1480 | } |
1481 | |
1482 | /** |
1483 | * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile) |
1484 | * |
1485 | * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it |
1486 | * around as long it needs (which may vary greatly depending on configuration) |
1487 | * |
1488 | * @param array $ops File operation batch for FileBaclend::doOperations() |
1489 | * @return array File operation batch |
1490 | */ |
1491 | protected function resolveFSFileObjects( array $ops ) { |
1492 | foreach ( $ops as &$op ) { |
1493 | $src = $op['src'] ?? null; |
1494 | if ( $src instanceof FSFile ) { |
1495 | $op['srcRef'] = $src; |
1496 | $op['src'] = $src->getPath(); |
1497 | } |
1498 | } |
1499 | unset( $op ); |
1500 | |
1501 | return $ops; |
1502 | } |
1503 | |
1504 | /** |
1505 | * Check if a given path is a "mwstore://" path. |
1506 | * This does not do any further validation or any existence checks. |
1507 | * |
1508 | * @param string|null $path |
1509 | * @return bool |
1510 | */ |
1511 | final public static function isStoragePath( $path ) { |
1512 | return ( strpos( $path ?? '', 'mwstore://' ) === 0 ); |
1513 | } |
1514 | |
1515 | /** |
1516 | * Split a storage path into a backend name, a container name, |
1517 | * and a relative file path. The relative path may be the empty string. |
1518 | * This does not do any path normalization or traversal checks. |
1519 | * |
1520 | * @param string $storagePath |
1521 | * @return array (backend, container, rel object) or (null, null, null) |
1522 | */ |
1523 | final public static function splitStoragePath( $storagePath ) { |
1524 | if ( self::isStoragePath( $storagePath ) ) { |
1525 | // Remove the "mwstore://" prefix and split the path |
1526 | $parts = explode( '/', substr( $storagePath, 10 ), 3 ); |
1527 | if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { |
1528 | if ( count( $parts ) == 3 ) { |
1529 | return $parts; // e.g. "backend/container/path" |
1530 | } else { |
1531 | return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container" |
1532 | } |
1533 | } |
1534 | } |
1535 | |
1536 | return [ null, null, null ]; |
1537 | } |
1538 | |
1539 | /** |
1540 | * Normalize a storage path by cleaning up directory separators. |
1541 | * Returns null if the path is not of the format of a valid storage path. |
1542 | * |
1543 | * @param string $storagePath |
1544 | * @return string|null Normalized storage path or null on failure |
1545 | */ |
1546 | final public static function normalizeStoragePath( $storagePath ) { |
1547 | [ $backend, $container, $relPath ] = self::splitStoragePath( $storagePath ); |
1548 | if ( $relPath !== null ) { // must be for this backend |
1549 | $relPath = self::normalizeContainerPath( $relPath ); |
1550 | if ( $relPath !== null ) { |
1551 | return ( $relPath != '' ) |
1552 | ? "mwstore://{$backend}/{$container}/{$relPath}" |
1553 | : "mwstore://{$backend}/{$container}"; |
1554 | } |
1555 | } |
1556 | |
1557 | return null; |
1558 | } |
1559 | |
1560 | /** |
1561 | * Get the parent storage directory of a storage path. |
1562 | * This returns a path like "mwstore://backend/container", |
1563 | * "mwstore://backend/container/...", or null if there is no parent. |
1564 | * |
1565 | * @param string $storagePath |
1566 | * @return string|null Parent storage path or null on failure |
1567 | */ |
1568 | final public static function parentStoragePath( $storagePath ) { |
1569 | // XXX dirname() depends on platform and locale! If nothing enforces that the storage path |
1570 | // doesn't contain characters like '\', behavior can vary by platform. We should use |
1571 | // explode() instead. |
1572 | $storagePath = dirname( $storagePath ); |
1573 | [ , , $rel ] = self::splitStoragePath( $storagePath ); |
1574 | |
1575 | return ( $rel === null ) ? null : $storagePath; |
1576 | } |
1577 | |
1578 | /** |
1579 | * Get the final extension from a storage or FS path |
1580 | * |
1581 | * @param string $path |
1582 | * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24) |
1583 | * @return string |
1584 | */ |
1585 | final public static function extensionFromPath( $path, $case = 'lowercase' ) { |
1586 | // This will treat a string starting with . as not having an extension, but store paths have |
1587 | // to start with 'mwstore://', so "garbage in, garbage out". |
1588 | $i = strrpos( $path, '.' ); |
1589 | $ext = $i ? substr( $path, $i + 1 ) : ''; |
1590 | |
1591 | if ( $case === 'lowercase' ) { |
1592 | $ext = strtolower( $ext ); |
1593 | } elseif ( $case === 'uppercase' ) { |
1594 | $ext = strtoupper( $ext ); |
1595 | } |
1596 | |
1597 | return $ext; |
1598 | } |
1599 | |
1600 | /** |
1601 | * Check if a relative path has no directory traversals |
1602 | * |
1603 | * @param string $path |
1604 | * @return bool |
1605 | * @since 1.20 |
1606 | */ |
1607 | final public static function isPathTraversalFree( $path ) { |