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