Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.85% |
498 / 713 |
|
48.05% |
37 / 77 |
CRAP | |
0.00% |
0 / 1 |
FileBackendStore | |
69.85% |
498 / 713 |
|
48.05% |
37 / 77 |
2316.49 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
maxFileSizeInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPathUsableInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
createInternal | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
doCreateInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
storeInternal | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
doStoreInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
copyInternal | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
doCopyInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
deleteInternal | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
doDeleteInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
moveInternal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
doMoveInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
describeInternal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
doDescribeInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
nullInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
concatenate | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
doConcatenate | |
56.10% |
23 / 41 |
|
0.00% |
0 / 1 |
24.19 | |||
doPrepare | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
4.47 | |||
doPrepareInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doSecure | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
5.57 | |||
doSecureInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doPublish | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
5.57 | |||
doPublishInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doClean | |
69.23% |
18 / 26 |
|
0.00% |
0 / 1 |
11.36 | |||
doCleanInternal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fileExists | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getFileTimestamp | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getFileSize | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getFileStat | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
17 | |||
ingestFreshFileStats | |
75.56% |
34 / 45 |
|
0.00% |
0 / 1 |
7.72 | |||
doGetFileStat | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getFileContentsMulti | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
doGetFileContentsMulti | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
getFileXAttributes | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
56 | |||
doGetFileXAttributes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFileSha1Base36 | |
66.67% |
16 / 24 |
|
0.00% |
0 / 1 |
8.81 | |||
doGetFileSha1Base36 | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
getFileProps | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getLocalReferenceMulti | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
8 | |||
doGetLocalReferenceMulti | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLocalCopyMulti | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
doGetLocalCopyMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getFileHttpUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
streamFile | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
4.43 | |||
doStreamFile | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
directoryExists | |
25.00% |
4 / 16 |
|
0.00% |
0 / 1 |
21.19 | |||
doDirectoryExists | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getDirectoryList | |
44.44% |
4 / 9 |
|
0.00% |
0 / 1 |
4.54 | |||
getDirectoryListInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getFileList | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
3.79 | |||
getFileListInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getOperationsInternal | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
3.00 | |||
getPathsToLockForOpsInternal | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getScopedLocksForOps | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
doOperationsInternal | |
76.67% |
23 / 30 |
|
0.00% |
0 / 1 |
7.62 | |||
doQuickOperationsInternal | |
75.76% |
25 / 33 |
|
0.00% |
0 / 1 |
11.42 | |||
executeOpHandlesInternal | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
8.12 | |||
doExecuteOpHandlesInternal | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
sanitizeOpHeaders | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
preloadCache | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
clearCache | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
doClearCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
preloadFileStat | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
doGetFileStatMulti | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
directoriesAreVirtual | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
isValidShortContainerName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
isValidContainerName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resolveStoragePath | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
resolveStoragePathReal | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getContainerShard | |
23.08% |
3 / 13 |
|
0.00% |
0 / 1 |
29.30 | |||
isSingleShardPathInternal | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getContainerHashLevels | |
25.00% |
2 / 8 |
|
0.00% |
0 / 1 |
21.19 | |||
getContainerSuffixes | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
fullContainerName | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
resolveContainerName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resolveContainerPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
containerCacheKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContainerCache | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
deleteContainerCache | |
25.00% |
1 / 4 |
|
0.00% |
0 / 1 |
3.69 | |||
primeContainerCache | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
7.01 | |||
doPrimeContainerCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fileCacheKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFileCache | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
3.58 | |||
deleteFileCache | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
4.68 | |||
primeFileCache | |
46.67% |
14 / 30 |
|
0.00% |
0 / 1 |
29.36 | |||
normalizeXAttributes | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
setConcurrencyFlags | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
getContentType | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * Base class for all backends using particular storage medium. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup FileBackend |
22 | */ |
23 | |
24 | use MediaWiki\Json\FormatJson; |
25 | use Wikimedia\AtEase\AtEase; |
26 | use Wikimedia\FileBackend\FileBackend; |
27 | use Wikimedia\ObjectCache\BagOStuff; |
28 | use Wikimedia\ObjectCache\EmptyBagOStuff; |
29 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
30 | |
31 | /** |
32 | * @brief Base class for all backends using particular storage medium. |
33 | * |
34 | * This class defines the methods as abstract that subclasses must implement. |
35 | * Outside callers should *not* use functions with "Internal" in the name. |
36 | * |
37 | * The FileBackend operations are implemented using basic functions |
38 | * such as storeInternal(), copyInternal(), deleteInternal() and the like. |
39 | * This class is also responsible for path resolution and sanitization. |
40 | * |
41 | * @stable to extend |
42 | * @ingroup FileBackend |
43 | * @since 1.19 |
44 | */ |
45 | abstract class FileBackendStore extends FileBackend { |
46 | /** @var WANObjectCache */ |
47 | protected $memCache; |
48 | /** @var BagOStuff */ |
49 | protected $srvCache; |
50 | /** @var MapCacheLRU Map of paths to small (RAM/disk) cache items */ |
51 | protected $cheapCache; |
52 | /** @var MapCacheLRU Map of paths to large (RAM/disk) cache items */ |
53 | protected $expensiveCache; |
54 | |
55 | /** @var array<string,array> Map of container names to sharding config */ |
56 | protected $shardViaHashLevels = []; |
57 | |
58 | /** @var callable|null Method to get the MIME type of files */ |
59 | protected $mimeCallback; |
60 | |
61 | protected $maxFileSize = 32 * 1024 * 1024 * 1024; // integer bytes (32GiB) |
62 | |
63 | protected const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries |
64 | protected const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache" |
65 | protected const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache" |
66 | |
67 | /** @var false Idiom for "no result due to missing file" (since 1.34) */ |
68 | protected const RES_ABSENT = false; |
69 | /** @var null Idiom for "no result due to I/O errors" (since 1.34) */ |
70 | protected const RES_ERROR = null; |
71 | |
72 | /** @var string File does not exist according to a normal stat query */ |
73 | protected const ABSENT_NORMAL = 'FNE-N'; |
74 | /** @var string File does not exist according to a "latest"-mode stat query */ |
75 | protected const ABSENT_LATEST = 'FNE-L'; |
76 | |
77 | /** |
78 | * @see FileBackend::__construct() |
79 | * Additional $config params include: |
80 | * - srvCache : BagOStuff cache to APC or the like. |
81 | * - wanCache : WANObjectCache object to use for persistent caching. |
82 | * - mimeCallback : Callback that takes (storage path, content, file system path) and |
83 | * returns the MIME type of the file or 'unknown/unknown'. The file |
84 | * system path parameter should be used if the content one is null. |
85 | * |
86 | * @stable to call |
87 | * |
88 | * @param array $config |
89 | */ |
90 | public function __construct( array $config ) { |
91 | parent::__construct( $config ); |
92 | $this->mimeCallback = $config['mimeCallback'] ?? null; |
93 | $this->srvCache = new EmptyBagOStuff(); // disabled by default |
94 | $this->memCache = WANObjectCache::newEmpty(); // disabled by default |
95 | $this->cheapCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE ); |
96 | $this->expensiveCache = new MapCacheLRU( self::CACHE_EXPENSIVE_SIZE ); |
97 | } |
98 | |
99 | /** |
100 | * Get the maximum allowable file size given backend |
101 | * medium restrictions and basic performance constraints. |
102 | * Do not call this function from places outside FileBackend and FileOp. |
103 | * |
104 | * @return int Bytes |
105 | */ |
106 | final public function maxFileSizeInternal() { |
107 | return min( $this->maxFileSize, PHP_INT_MAX ); |
108 | } |
109 | |
110 | /** |
111 | * Check if a file can be created or changed at a given storage path in the backend |
112 | * |
113 | * FS backends should check that the parent directory exists, files can be written |
114 | * under it, and that any file already there is both readable and writable. |
115 | * Backends using key/value stores should check if the container exists. |
116 | * |
117 | * @param string $storagePath |
118 | * @return bool |
119 | */ |
120 | abstract public function isPathUsableInternal( $storagePath ); |
121 | |
122 | /** |
123 | * Create a file in the backend with the given contents. |
124 | * This will overwrite any file that exists at the destination. |
125 | * Do not call this function from places outside FileBackend and FileOp. |
126 | * |
127 | * $params include: |
128 | * - content : the raw file contents |
129 | * - dst : destination storage path |
130 | * - headers : HTTP header name/value map |
131 | * - async : StatusValue will be returned immediately if supported. |
132 | * If the StatusValue is OK, then its value field will be |
133 | * set to a FileBackendStoreOpHandle object. |
134 | * - dstExists : Whether a file exists at the destination (optimization). |
135 | * Callers can use "false" if no existing file is being changed. |
136 | * |
137 | * @param array $params |
138 | * @return StatusValue |
139 | */ |
140 | final public function createInternal( array $params ) { |
141 | /** @noinspection PhpUnusedLocalVariableInspection */ |
142 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
143 | |
144 | if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) { |
145 | $status = $this->newStatus( 'backend-fail-maxsize', |
146 | $params['dst'], $this->maxFileSizeInternal() ); |
147 | } else { |
148 | $status = $this->doCreateInternal( $params ); |
149 | $this->clearCache( [ $params['dst'] ] ); |
150 | if ( $params['dstExists'] ?? true ) { |
151 | $this->deleteFileCache( $params['dst'] ); // persistent cache |
152 | } |
153 | } |
154 | |
155 | return $status; |
156 | } |
157 | |
158 | /** |
159 | * @see FileBackendStore::createInternal() |
160 | * @param array $params |
161 | * @return StatusValue |
162 | */ |
163 | abstract protected function doCreateInternal( array $params ); |
164 | |
165 | /** |
166 | * Store a file into the backend from a file on disk. |
167 | * This will overwrite any file that exists at the destination. |
168 | * Do not call this function from places outside FileBackend and FileOp. |
169 | * |
170 | * $params include: |
171 | * - src : source path on disk |
172 | * - dst : destination storage path |
173 | * - headers : HTTP header name/value map |
174 | * - async : StatusValue will be returned immediately if supported. |
175 | * If the StatusValue is OK, then its value field will be |
176 | * set to a FileBackendStoreOpHandle object. |
177 | * - dstExists : Whether a file exists at the destination (optimization). |
178 | * Callers can use "false" if no existing file is being changed. |
179 | * |
180 | * @param array $params |
181 | * @return StatusValue |
182 | */ |
183 | final public function storeInternal( array $params ) { |
184 | /** @noinspection PhpUnusedLocalVariableInspection */ |
185 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
186 | |
187 | if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) { |
188 | $status = $this->newStatus( 'backend-fail-maxsize', |
189 | $params['dst'], $this->maxFileSizeInternal() ); |
190 | } else { |
191 | $status = $this->doStoreInternal( $params ); |
192 | $this->clearCache( [ $params['dst'] ] ); |
193 | if ( $params['dstExists'] ?? true ) { |
194 | $this->deleteFileCache( $params['dst'] ); // persistent cache |
195 | } |
196 | } |
197 | |
198 | return $status; |
199 | } |
200 | |
201 | /** |
202 | * @see FileBackendStore::storeInternal() |
203 | * @param array $params |
204 | * @return StatusValue |
205 | */ |
206 | abstract protected function doStoreInternal( array $params ); |
207 | |
208 | /** |
209 | * Copy a file from one storage path to another in the backend. |
210 | * This will overwrite any file that exists at the destination. |
211 | * Do not call this function from places outside FileBackend and FileOp. |
212 | * |
213 | * $params include: |
214 | * - src : source storage path |
215 | * - dst : destination storage path |
216 | * - ignoreMissingSource : do nothing if the source file does not exist |
217 | * - headers : HTTP header name/value map |
218 | * - async : StatusValue will be returned immediately if supported. |
219 | * If the StatusValue is OK, then its value field will be |
220 | * set to a FileBackendStoreOpHandle object. |
221 | * - dstExists : Whether a file exists at the destination (optimization). |
222 | * Callers can use "false" if no existing file is being changed. |
223 | * |
224 | * @param array $params |
225 | * @return StatusValue |
226 | */ |
227 | final public function copyInternal( array $params ) { |
228 | /** @noinspection PhpUnusedLocalVariableInspection */ |
229 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
230 | |
231 | $status = $this->doCopyInternal( $params ); |
232 | $this->clearCache( [ $params['dst'] ] ); |
233 | if ( $params['dstExists'] ?? true ) { |
234 | $this->deleteFileCache( $params['dst'] ); // persistent cache |
235 | } |
236 | |
237 | return $status; |
238 | } |
239 | |
240 | /** |
241 | * @see FileBackendStore::copyInternal() |
242 | * @param array $params |
243 | * @return StatusValue |
244 | */ |
245 | abstract protected function doCopyInternal( array $params ); |
246 | |
247 | /** |
248 | * Delete a file at the storage path. |
249 | * Do not call this function from places outside FileBackend and FileOp. |
250 | * |
251 | * $params include: |
252 | * - src : source storage path |
253 | * - ignoreMissingSource : do nothing if the source file does not exist |
254 | * - async : StatusValue will be returned immediately if supported. |
255 | * If the StatusValue is OK, then its value field will be |
256 | * set to a FileBackendStoreOpHandle object. |
257 | * |
258 | * @param array $params |
259 | * @return StatusValue |
260 | */ |
261 | final public function deleteInternal( array $params ) { |
262 | /** @noinspection PhpUnusedLocalVariableInspection */ |
263 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
264 | |
265 | $status = $this->doDeleteInternal( $params ); |
266 | $this->clearCache( [ $params['src'] ] ); |
267 | $this->deleteFileCache( $params['src'] ); // persistent cache |
268 | return $status; |
269 | } |
270 | |
271 | /** |
272 | * @see FileBackendStore::deleteInternal() |
273 | * @param array $params |
274 | * @return StatusValue |
275 | */ |
276 | abstract protected function doDeleteInternal( array $params ); |
277 | |
278 | /** |
279 | * Move a file from one storage path to another in the backend. |
280 | * This will overwrite any file that exists at the destination. |
281 | * Do not call this function from places outside FileBackend and FileOp. |
282 | * |
283 | * $params include: |
284 | * - src : source storage path |
285 | * - dst : destination storage path |
286 | * - ignoreMissingSource : do nothing if the source file does not exist |
287 | * - headers : HTTP header name/value map |
288 | * - async : StatusValue will be returned immediately if supported. |
289 | * If the StatusValue is OK, then its value field will be |
290 | * set to a FileBackendStoreOpHandle object. |
291 | * - dstExists : Whether a file exists at the destination (optimization). |
292 | * Callers can use "false" if no existing file is being changed. |
293 | * |
294 | * @param array $params |
295 | * @return StatusValue |
296 | */ |
297 | final public function moveInternal( array $params ) { |
298 | /** @noinspection PhpUnusedLocalVariableInspection */ |
299 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
300 | |
301 | $status = $this->doMoveInternal( $params ); |
302 | $this->clearCache( [ $params['src'], $params['dst'] ] ); |
303 | $this->deleteFileCache( $params['src'] ); // persistent cache |
304 | if ( $params['dstExists'] ?? true ) { |
305 | $this->deleteFileCache( $params['dst'] ); // persistent cache |
306 | } |
307 | |
308 | return $status; |
309 | } |
310 | |
311 | /** |
312 | * @see FileBackendStore::moveInternal() |
313 | * @param array $params |
314 | * @return StatusValue |
315 | */ |
316 | abstract protected function doMoveInternal( array $params ); |
317 | |
318 | /** |
319 | * Alter metadata for a file at the storage path. |
320 | * Do not call this function from places outside FileBackend and FileOp. |
321 | * |
322 | * $params include: |
323 | * - src : source storage path |
324 | * - headers : HTTP header name/value map |
325 | * - async : StatusValue will be returned immediately if supported. |
326 | * If the StatusValue is OK, then its value field will be |
327 | * set to a FileBackendStoreOpHandle object. |
328 | * |
329 | * @param array $params |
330 | * @return StatusValue |
331 | */ |
332 | final public function describeInternal( array $params ) { |
333 | /** @noinspection PhpUnusedLocalVariableInspection */ |
334 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
335 | |
336 | if ( count( $params['headers'] ) ) { |
337 | $status = $this->doDescribeInternal( $params ); |
338 | $this->clearCache( [ $params['src'] ] ); |
339 | $this->deleteFileCache( $params['src'] ); // persistent cache |
340 | } else { |
341 | $status = $this->newStatus(); // nothing to do |
342 | } |
343 | |
344 | return $status; |
345 | } |
346 | |
347 | /** |
348 | * @see FileBackendStore::describeInternal() |
349 | * @stable to override |
350 | * @param array $params |
351 | * @return StatusValue |
352 | */ |
353 | protected function doDescribeInternal( array $params ) { |
354 | return $this->newStatus(); |
355 | } |
356 | |
357 | /** |
358 | * No-op file operation that does nothing. |
359 | * Do not call this function from places outside FileBackend and FileOp. |
360 | * |
361 | * @param array $params |
362 | * @return StatusValue |
363 | */ |
364 | final public function nullInternal( array $params ) { |
365 | return $this->newStatus(); |
366 | } |
367 | |
368 | final public function concatenate( array $params ) { |
369 | /** @noinspection PhpUnusedLocalVariableInspection */ |
370 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
371 | $status = $this->newStatus(); |
372 | |
373 | // Try to lock the source files for the scope of this function |
374 | /** @noinspection PhpUnusedLocalVariableInspection */ |
375 | $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status ); |
376 | if ( $status->isOK() ) { |
377 | // Actually do the file concatenation... |
378 | $start_time = microtime( true ); |
379 | $status->merge( $this->doConcatenate( $params ) ); |
380 | $sec = microtime( true ) - $start_time; |
381 | if ( !$status->isOK() ) { |
382 | $this->logger->error( static::class . "-{$this->name}" . |
383 | " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" ); |
384 | } |
385 | } |
386 | |
387 | return $status; |
388 | } |
389 | |
390 | /** |
391 | * @see FileBackendStore::concatenate() |
392 | * @stable to override |
393 | * @param array $params |
394 | * @return StatusValue |
395 | */ |
396 | protected function doConcatenate( array $params ) { |
397 | $status = $this->newStatus(); |
398 | $tmpPath = $params['dst']; |
399 | unset( $params['latest'] ); |
400 | |
401 | // Check that the specified temp file is valid... |
402 | AtEase::suppressWarnings(); |
403 | $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 ); |
404 | AtEase::restoreWarnings(); |
405 | if ( !$ok ) { // not present or not empty |
406 | $status->fatal( 'backend-fail-opentemp', $tmpPath ); |
407 | |
408 | return $status; |
409 | } |
410 | |
411 | // Get local FS versions of the chunks needed for the concatenation... |
412 | $fsFiles = $this->getLocalReferenceMulti( $params ); |
413 | foreach ( $fsFiles as $path => &$fsFile ) { |
414 | if ( !$fsFile ) { // chunk failed to download? |
415 | $fsFile = $this->getLocalReference( [ 'src' => $path ] ); |
416 | if ( !$fsFile ) { // retry failed? |
417 | $status->fatal( |
418 | $fsFile === self::RES_ERROR ? 'backend-fail-read' : 'backend-fail-notexists', |
419 | $path |
420 | ); |
421 | |
422 | return $status; |
423 | } |
424 | } |
425 | } |
426 | unset( $fsFile ); // unset reference so we can reuse $fsFile |
427 | |
428 | // Get a handle for the destination temp file |
429 | $tmpHandle = fopen( $tmpPath, 'ab' ); |
430 | if ( $tmpHandle === false ) { |
431 | $status->fatal( 'backend-fail-opentemp', $tmpPath ); |
432 | |
433 | return $status; |
434 | } |
435 | |
436 | // Build up the temp file using the source chunks (in order)... |
437 | foreach ( $fsFiles as $virtualSource => $fsFile ) { |
438 | // Get a handle to the local FS version |
439 | $sourceHandle = fopen( $fsFile->getPath(), 'rb' ); |
440 | if ( $sourceHandle === false ) { |
441 | fclose( $tmpHandle ); |
442 | $status->fatal( 'backend-fail-read', $virtualSource ); |
443 | |
444 | return $status; |
445 | } |
446 | // Append chunk to file (pass chunk size to avoid magic quotes) |
447 | if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) { |
448 | fclose( $sourceHandle ); |
449 | fclose( $tmpHandle ); |
450 | $status->fatal( 'backend-fail-writetemp', $tmpPath ); |
451 | |
452 | return $status; |
453 | } |
454 | fclose( $sourceHandle ); |
455 | } |
456 | if ( !fclose( $tmpHandle ) ) { |
457 | $status->fatal( 'backend-fail-closetemp', $tmpPath ); |
458 | |
459 | return $status; |
460 | } |
461 | |
462 | clearstatcache(); // temp file changed |
463 | |
464 | return $status; |
465 | } |
466 | |
467 | /** |
468 | * @inheritDoc |
469 | */ |
470 | final protected function doPrepare( array $params ) { |
471 | /** @noinspection PhpUnusedLocalVariableInspection */ |
472 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
473 | $status = $this->newStatus(); |
474 | |
475 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
476 | if ( $dir === null ) { |
477 | $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); |
478 | |
479 | return $status; // invalid storage path |
480 | } |
481 | |
482 | if ( $shard !== null ) { // confined to a single container/shard |
483 | $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) ); |
484 | } else { // directory is on several shards |
485 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
486 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
487 | foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { |
488 | $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) ); |
489 | } |
490 | } |
491 | |
492 | return $status; |
493 | } |
494 | |
495 | /** |
496 | * @see FileBackendStore::doPrepare() |
497 | * @stable to override |
498 | * @param string $container |
499 | * @param string $dir |
500 | * @param array $params |
501 | * @return StatusValue Good status without value for success, fatal otherwise. |
502 | */ |
503 | protected function doPrepareInternal( $container, $dir, array $params ) { |
504 | return $this->newStatus(); |
505 | } |
506 | |
507 | final protected function doSecure( array $params ) { |
508 | /** @noinspection PhpUnusedLocalVariableInspection */ |
509 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
510 | $status = $this->newStatus(); |
511 | |
512 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
513 | if ( $dir === null ) { |
514 | $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); |
515 | |
516 | return $status; // invalid storage path |
517 | } |
518 | |
519 | if ( $shard !== null ) { // confined to a single container/shard |
520 | $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); |
521 | } else { // directory is on several shards |
522 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
523 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
524 | foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { |
525 | $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) ); |
526 | } |
527 | } |
528 | |
529 | return $status; |
530 | } |
531 | |
532 | /** |
533 | * @see FileBackendStore::doSecure() |
534 | * @stable to override |
535 | * @param string $container |
536 | * @param string $dir |
537 | * @param array $params |
538 | * @return StatusValue Good status without value for success, fatal otherwise. |
539 | */ |
540 | protected function doSecureInternal( $container, $dir, array $params ) { |
541 | return $this->newStatus(); |
542 | } |
543 | |
544 | final protected function doPublish( array $params ) { |
545 | /** @noinspection PhpUnusedLocalVariableInspection */ |
546 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
547 | $status = $this->newStatus(); |
548 | |
549 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
550 | if ( $dir === null ) { |
551 | $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); |
552 | |
553 | return $status; // invalid storage path |
554 | } |
555 | |
556 | if ( $shard !== null ) { // confined to a single container/shard |
557 | $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) ); |
558 | } else { // directory is on several shards |
559 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
560 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
561 | foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { |
562 | $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) ); |
563 | } |
564 | } |
565 | |
566 | return $status; |
567 | } |
568 | |
569 | /** |
570 | * @see FileBackendStore::doPublish() |
571 | * @stable to override |
572 | * @param string $container |
573 | * @param string $dir |
574 | * @param array $params |
575 | * @return StatusValue |
576 | */ |
577 | protected function doPublishInternal( $container, $dir, array $params ) { |
578 | return $this->newStatus(); |
579 | } |
580 | |
581 | final protected function doClean( array $params ) { |
582 | /** @noinspection PhpUnusedLocalVariableInspection */ |
583 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
584 | $status = $this->newStatus(); |
585 | |
586 | // Recursive: first delete all empty subdirs recursively |
587 | if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) { |
588 | $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] ); |
589 | if ( $subDirsRel !== null ) { // no errors |
590 | foreach ( $subDirsRel as $subDirRel ) { |
591 | $subDir = $params['dir'] . "/{$subDirRel}"; // full path |
592 | $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) ); |
593 | } |
594 | unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends) |
595 | } |
596 | } |
597 | |
598 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
599 | if ( $dir === null ) { |
600 | $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); |
601 | |
602 | return $status; // invalid storage path |
603 | } |
604 | |
605 | // Attempt to lock this directory... |
606 | $filesLockEx = [ $params['dir'] ]; |
607 | /** @noinspection PhpUnusedLocalVariableInspection */ |
608 | $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); |
609 | if ( !$status->isOK() ) { |
610 | return $status; // abort |
611 | } |
612 | |
613 | if ( $shard !== null ) { // confined to a single container/shard |
614 | $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) ); |
615 | $this->deleteContainerCache( $fullCont ); // purge cache |
616 | } else { // directory is on several shards |
617 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
618 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
619 | foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { |
620 | $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) ); |
621 | $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache |
622 | } |
623 | } |
624 | |
625 | return $status; |
626 | } |
627 | |
628 | /** |
629 | * @see FileBackendStore::doClean() |
630 | * @stable to override |
631 | * @param string $container |
632 | * @param string $dir |
633 | * @param array $params |
634 | * @return StatusValue |
635 | */ |
636 | protected function doCleanInternal( $container, $dir, array $params ) { |
637 | return $this->newStatus(); |
638 | } |
639 | |
640 | final public function fileExists( array $params ) { |
641 | /** @noinspection PhpUnusedLocalVariableInspection */ |
642 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
643 | |
644 | $stat = $this->getFileStat( $params ); |
645 | if ( is_array( $stat ) ) { |
646 | return true; |
647 | } |
648 | |
649 | return $stat === self::RES_ABSENT ? false : self::EXISTENCE_ERROR; |
650 | } |
651 | |
652 | final public function getFileTimestamp( array $params ) { |
653 | /** @noinspection PhpUnusedLocalVariableInspection */ |
654 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
655 | |
656 | $stat = $this->getFileStat( $params ); |
657 | if ( is_array( $stat ) ) { |
658 | return $stat['mtime']; |
659 | } |
660 | |
661 | return self::TIMESTAMP_FAIL; // all failure cases |
662 | } |
663 | |
664 | final public function getFileSize( array $params ) { |
665 | /** @noinspection PhpUnusedLocalVariableInspection */ |
666 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
667 | |
668 | $stat = $this->getFileStat( $params ); |
669 | if ( is_array( $stat ) ) { |
670 | return $stat['size']; |
671 | } |
672 | |
673 | return self::SIZE_FAIL; // all failure cases |
674 | } |
675 | |
676 | final public function getFileStat( array $params ) { |
677 | /** @noinspection PhpUnusedLocalVariableInspection */ |
678 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
679 | |
680 | $path = self::normalizeStoragePath( $params['src'] ); |
681 | if ( $path === null ) { |
682 | return self::STAT_ERROR; // invalid storage path |
683 | } |
684 | |
685 | // Whether to bypass cache except for process cache entries loaded directly from |
686 | // high consistency backend queries (caller handles any cache flushing and locking) |
687 | $latest = !empty( $params['latest'] ); |
688 | // Whether to ignore cache entries missing the SHA-1 field for existing files |
689 | $requireSHA1 = !empty( $params['requireSHA1'] ); |
690 | |
691 | $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); |
692 | // Load the persistent stat cache into process cache if needed |
693 | if ( !$latest ) { |
694 | if ( |
695 | // File stat is not in process cache |
696 | $stat === null || |
697 | // Key/value store backends might opportunistically set file stat process |
698 | // cache entries from object listings that do not include the SHA-1. In that |
699 | // case, loading the persistent stat cache will likely yield the SHA-1. |
700 | ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) ) |
701 | ) { |
702 | $this->primeFileCache( [ $path ] ); |
703 | // Get any newly process-cached entry |
704 | $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL ); |
705 | } |
706 | } |
707 | |
708 | if ( is_array( $stat ) ) { |
709 | if ( |
710 | ( !$latest || !empty( $stat['latest'] ) ) && |
711 | ( !$requireSHA1 || isset( $stat['sha1'] ) ) |
712 | ) { |
713 | return $stat; |
714 | } |
715 | } elseif ( $stat === self::ABSENT_LATEST ) { |
716 | return self::STAT_ABSENT; |
717 | } elseif ( $stat === self::ABSENT_NORMAL ) { |
718 | if ( !$latest ) { |
719 | return self::STAT_ABSENT; |
720 | } |
721 | } |
722 | |
723 | // Load the file stat from the backend and update caches |
724 | $stat = $this->doGetFileStat( $params ); |
725 | $this->ingestFreshFileStats( [ $path => $stat ], $latest ); |
726 | |
727 | if ( is_array( $stat ) ) { |
728 | return $stat; |
729 | } |
730 | |
731 | return $stat === self::RES_ERROR ? self::STAT_ERROR : self::STAT_ABSENT; |
732 | } |
733 | |
734 | /** |
735 | * Ingest file stat entries that just came from querying the backend (not cache) |
736 | * |
737 | * @param array<string,array|false|null> $stats Map of storage path => {@see doGetFileStat} result |
738 | * @param bool $latest Whether doGetFileStat()/doGetFileStatMulti() had the 'latest' flag |
739 | * @return bool Whether all files have non-error stat replies |
740 | */ |
741 | final protected function ingestFreshFileStats( array $stats, $latest ) { |
742 | $success = true; |
743 | |
744 | foreach ( $stats as $path => $stat ) { |
745 | if ( is_array( $stat ) ) { |
746 | // Strongly consistent backends might automatically set this flag |
747 | $stat['latest'] ??= $latest; |
748 | |
749 | $this->cheapCache->setField( $path, 'stat', $stat ); |
750 | if ( isset( $stat['sha1'] ) ) { |
751 | // Some backends store the SHA-1 hash as metadata |
752 | $this->cheapCache->setField( |
753 | $path, |
754 | 'sha1', |
755 | [ 'hash' => $stat['sha1'], 'latest' => $latest ] |
756 | ); |
757 | } |
758 | if ( isset( $stat['xattr'] ) ) { |
759 | // Some backends store custom headers/metadata |
760 | $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); |
761 | $this->cheapCache->setField( |
762 | $path, |
763 | 'xattr', |
764 | [ 'map' => $stat['xattr'], 'latest' => $latest ] |
765 | ); |
766 | } |
767 | // Update persistent cache (@TODO: set all entries in one batch) |
768 | $this->setFileCache( $path, $stat ); |
769 | } elseif ( $stat === self::RES_ABSENT ) { |
770 | $this->cheapCache->setField( |
771 | $path, |
772 | 'stat', |
773 | $latest ? self::ABSENT_LATEST : self::ABSENT_NORMAL |
774 | ); |
775 | $this->cheapCache->setField( |
776 | $path, |
777 | 'xattr', |
778 | [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ] |
779 | ); |
780 | $this->cheapCache->setField( |
781 | $path, |
782 | 'sha1', |
783 | [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ] |
784 | ); |
785 | $this->logger->debug( |
786 | __METHOD__ . ': File {path} does not exist', |
787 | [ 'path' => $path ] |
788 | ); |
789 | } else { |
790 | $success = false; |
791 | $this->logger->error( |
792 | __METHOD__ . ': Could not stat file {path}', |
793 | [ 'path' => $path ] |
794 | ); |
795 | } |
796 | } |
797 | |
798 | return $success; |
799 | } |
800 | |
801 | /** |
802 | * @see FileBackendStore::getFileStat() |
803 | * @param array $params |
804 | * @return array|false|null |
805 | */ |
806 | abstract protected function doGetFileStat( array $params ); |
807 | |
808 | public function getFileContentsMulti( array $params ) { |
809 | /** @noinspection PhpUnusedLocalVariableInspection */ |
810 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
811 | |
812 | $params = $this->setConcurrencyFlags( $params ); |
813 | $contents = $this->doGetFileContentsMulti( $params ); |
814 | foreach ( $contents as $path => $content ) { |
815 | if ( !is_string( $content ) ) { |
816 | $contents[$path] = self::CONTENT_FAIL; // used for all failure cases |
817 | } |
818 | } |
819 | |
820 | return $contents; |
821 | } |
822 | |
823 | /** |
824 | * @see FileBackendStore::getFileContentsMulti() |
825 | * @stable to override |
826 | * @param array $params |
827 | * @return string[]|bool[]|null[] Map of (path => string, false (missing), or null (error)) |
828 | */ |
829 | protected function doGetFileContentsMulti( array $params ) { |
830 | $contents = []; |
831 | foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) { |
832 | if ( $fsFile instanceof FSFile ) { |
833 | AtEase::suppressWarnings(); |
834 | $content = file_get_contents( $fsFile->getPath() ); |
835 | AtEase::restoreWarnings(); |
836 | $contents[$path] = is_string( $content ) ? $content : self::RES_ERROR; |
837 | } else { |
838 | // self::RES_ERROR or self::RES_ABSENT |
839 | $contents[$path] = $fsFile; |
840 | } |
841 | } |
842 | |
843 | return $contents; |
844 | } |
845 | |
846 | final public function getFileXAttributes( array $params ) { |
847 | /** @noinspection PhpUnusedLocalVariableInspection */ |
848 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
849 | |
850 | $path = self::normalizeStoragePath( $params['src'] ); |
851 | if ( $path === null ) { |
852 | return self::XATTRS_FAIL; // invalid storage path |
853 | } |
854 | $latest = !empty( $params['latest'] ); // use latest data? |
855 | if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) { |
856 | $stat = $this->cheapCache->getField( $path, 'xattr' ); |
857 | // If we want the latest data, check that this cached |
858 | // value was in fact fetched with the latest available data. |
859 | if ( !$latest || $stat['latest'] ) { |
860 | return $stat['map']; |
861 | } |
862 | } |
863 | $fields = $this->doGetFileXAttributes( $params ); |
864 | if ( is_array( $fields ) ) { |
865 | $fields = self::normalizeXAttributes( $fields ); |
866 | $this->cheapCache->setField( |
867 | $path, |
868 | 'xattr', |
869 | [ 'map' => $fields, 'latest' => $latest ] |
870 | ); |
871 | } elseif ( $fields === self::RES_ABSENT ) { |
872 | $this->cheapCache->setField( |
873 | $path, |
874 | 'xattr', |
875 | [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ] |
876 | ); |
877 | } else { |
878 | $fields = self::XATTRS_FAIL; // used for all failure cases |
879 | } |
880 | |
881 | return $fields; |
882 | } |
883 | |
884 | /** |
885 | * @see FileBackendStore::getFileXAttributes() |
886 | * @stable to override |
887 | * @param array $params |
888 | * @return array[][]|false|null Attributes, false (missing file), or null (error) |
889 | */ |
890 | protected function doGetFileXAttributes( array $params ) { |
891 | return [ 'headers' => [], 'metadata' => [] ]; // not supported |
892 | } |
893 | |
894 | final public function getFileSha1Base36( array $params ) { |
895 | /** @noinspection PhpUnusedLocalVariableInspection */ |
896 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
897 | |
898 | $path = self::normalizeStoragePath( $params['src'] ); |
899 | if ( $path === null ) { |
900 | return self::SHA1_FAIL; // invalid storage path |
901 | } |
902 | $latest = !empty( $params['latest'] ); // use latest data? |
903 | if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) { |
904 | $stat = $this->cheapCache->getField( $path, 'sha1' ); |
905 | // If we want the latest data, check that this cached |
906 | // value was in fact fetched with the latest available data. |
907 | if ( !$latest || $stat['latest'] ) { |
908 | return $stat['hash']; |
909 | } |
910 | } |
911 | $sha1 = $this->doGetFileSha1Base36( $params ); |
912 | if ( is_string( $sha1 ) ) { |
913 | $this->cheapCache->setField( |
914 | $path, |
915 | 'sha1', |
916 | [ 'hash' => $sha1, 'latest' => $latest ] |
917 | ); |
918 | } elseif ( $sha1 === self::RES_ABSENT ) { |
919 | $this->cheapCache->setField( |
920 | $path, |
921 | 'sha1', |
922 | [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ] |
923 | ); |
924 | } else { |
925 | $sha1 = self::SHA1_FAIL; // used for all failure cases |
926 | } |
927 | |
928 | return $sha1; |
929 | } |
930 | |
931 | /** |
932 | * @see FileBackendStore::getFileSha1Base36() |
933 | * @stable to override |
934 | * @param array $params |
935 | * @return bool|string|null SHA1, false (missing file), or null (error) |
936 | */ |
937 | protected function doGetFileSha1Base36( array $params ) { |
938 | $fsFile = $this->getLocalReference( $params ); |
939 | if ( $fsFile instanceof FSFile ) { |
940 | $sha1 = $fsFile->getSha1Base36(); |
941 | |
942 | return is_string( $sha1 ) ? $sha1 : self::RES_ERROR; |
943 | } |
944 | |
945 | return $fsFile === self::RES_ERROR ? self::RES_ERROR : self::RES_ABSENT; |
946 | } |
947 | |
948 | final public function getFileProps( array $params ) { |
949 | /** @noinspection PhpUnusedLocalVariableInspection */ |
950 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
951 | |
952 | $fsFile = $this->getLocalReference( $params ); |
953 | |
954 | return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); |
955 | } |
956 | |
957 | final public function getLocalReferenceMulti( array $params ) { |
958 | /** @noinspection PhpUnusedLocalVariableInspection */ |
959 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
960 | |
961 | $params = $this->setConcurrencyFlags( $params ); |
962 | |
963 | $fsFiles = []; // (path => FSFile) |
964 | $latest = !empty( $params['latest'] ); // use latest data? |
965 | // Reuse any files already in process cache... |
966 | foreach ( $params['srcs'] as $src ) { |
967 | $path = self::normalizeStoragePath( $src ); |
968 | if ( $path === null ) { |
969 | $fsFiles[$src] = self::RES_ERROR; // invalid storage path |
970 | } elseif ( $this->expensiveCache->hasField( $path, 'localRef' ) ) { |
971 | $val = $this->expensiveCache->getField( $path, 'localRef' ); |
972 | // If we want the latest data, check that this cached |
973 | // value was in fact fetched with the latest available data. |
974 | if ( !$latest || $val['latest'] ) { |
975 | $fsFiles[$src] = $val['object']; |
976 | } |
977 | } |
978 | } |
979 | // Fetch local references of any remaining files... |
980 | $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) ); |
981 | foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) { |
982 | if ( $fsFile instanceof FSFile ) { |
983 | $fsFiles[$path] = $fsFile; |
984 | $this->expensiveCache->setField( |
985 | $path, |
986 | 'localRef', |
987 | [ 'object' => $fsFile, 'latest' => $latest ] |
988 | ); |
989 | } else { |
990 | // self::RES_ERROR or self::RES_ABSENT |
991 | $fsFiles[$path] = $fsFile; |
992 | } |
993 | } |
994 | |
995 | return $fsFiles; |
996 | } |
997 | |
998 | /** |
999 | * @see FileBackendStore::getLocalReferenceMulti() |
1000 | * @stable to override |
1001 | * @param array $params |
1002 | * @return string[]|bool[]|null[] Map of (path => FSFile, false (missing), or null (error)) |
1003 | */ |
1004 | protected function doGetLocalReferenceMulti( array $params ) { |
1005 | return $this->doGetLocalCopyMulti( $params ); |
1006 | } |
1007 | |
1008 | final public function getLocalCopyMulti( array $params ) { |
1009 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1010 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1011 | |
1012 | $params = $this->setConcurrencyFlags( $params ); |
1013 | |
1014 | return $this->doGetLocalCopyMulti( $params ); |
1015 | } |
1016 | |
1017 | /** |
1018 | * @see FileBackendStore::getLocalCopyMulti() |
1019 | * @param array $params |
1020 | * @return string[]|bool[]|null[] Map of (path => TempFSFile, false (missing), or null (error)) |
1021 | */ |
1022 | abstract protected function doGetLocalCopyMulti( array $params ); |
1023 | |
1024 | /** |
1025 | * @see FileBackend::getFileHttpUrl() |
1026 | * @stable to override |
1027 | * @param array $params |
1028 | * @return string|null |
1029 | */ |
1030 | public function getFileHttpUrl( array $params ) { |
1031 | return self::TEMPURL_ERROR; // not supported |
1032 | } |
1033 | |
1034 | final public function streamFile( array $params ) { |
1035 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1036 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1037 | $status = $this->newStatus(); |
1038 | |
1039 | // Always set some fields for subclass convenience |
1040 | $params['options'] ??= []; |
1041 | $params['headers'] ??= []; |
1042 | |
1043 | // Don't stream it out as text/html if there was a PHP error |
1044 | if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) { |
1045 | print "Headers already sent, terminating.\n"; |
1046 | $status->fatal( 'backend-fail-stream', $params['src'] ); |
1047 | return $status; |
1048 | } |
1049 | |
1050 | $status->merge( $this->doStreamFile( $params ) ); |
1051 | |
1052 | return $status; |
1053 | } |
1054 | |
1055 | /** |
1056 | * @see FileBackendStore::streamFile() |
1057 | * @stable to override |
1058 | * @param array $params |
1059 | * @return StatusValue |
1060 | */ |
1061 | protected function doStreamFile( array $params ) { |
1062 | $status = $this->newStatus(); |
1063 | |
1064 | $flags = 0; |
1065 | $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0; |
1066 | $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0; |
1067 | |
1068 | $fsFile = $this->getLocalReference( $params ); |
1069 | if ( $fsFile ) { |
1070 | $streamer = new HTTPFileStreamer( |
1071 | $fsFile->getPath(), |
1072 | $this->getStreamerOptions() |
1073 | ); |
1074 | $res = $streamer->stream( $params['headers'], true, $params['options'], $flags ); |
1075 | } else { |
1076 | $res = false; |
1077 | HTTPFileStreamer::send404Message( $params['src'], $flags ); |
1078 | } |
1079 | |
1080 | if ( !$res ) { |
1081 | $status->fatal( 'backend-fail-stream', $params['src'] ); |
1082 | } |
1083 | |
1084 | return $status; |
1085 | } |
1086 | |
1087 | final public function directoryExists( array $params ) { |
1088 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
1089 | if ( $dir === null ) { |
1090 | return self::EXISTENCE_ERROR; // invalid storage path |
1091 | } |
1092 | if ( $shard !== null ) { // confined to a single container/shard |
1093 | return $this->doDirectoryExists( $fullCont, $dir, $params ); |
1094 | } else { // directory is on several shards |
1095 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
1096 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
1097 | $res = false; // response |
1098 | foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { |
1099 | $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params ); |
1100 | if ( $exists === true ) { |
1101 | $res = true; |
1102 | break; // found one! |
1103 | } elseif ( $exists === self::RES_ERROR ) { |
1104 | $res = self::EXISTENCE_ERROR; |
1105 | } |
1106 | } |
1107 | |
1108 | return $res; |
1109 | } |
1110 | } |
1111 | |
1112 | /** |
1113 | * @see FileBackendStore::directoryExists() |
1114 | * |
1115 | * @param string $container Resolved container name |
1116 | * @param string $dir Resolved path relative to container |
1117 | * @param array $params |
1118 | * @return bool|null |
1119 | */ |
1120 | abstract protected function doDirectoryExists( $container, $dir, array $params ); |
1121 | |
1122 | final public function getDirectoryList( array $params ) { |
1123 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
1124 | if ( $dir === null ) { |
1125 | return self::EXISTENCE_ERROR; // invalid storage path |
1126 | } |
1127 | if ( $shard !== null ) { |
1128 | // File listing is confined to a single container/shard |
1129 | return $this->getDirectoryListInternal( $fullCont, $dir, $params ); |
1130 | } else { |
1131 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
1132 | // File listing spans multiple containers/shards |
1133 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
1134 | |
1135 | return new FileBackendStoreShardDirIterator( $this, |
1136 | $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); |
1137 | } |
1138 | } |
1139 | |
1140 | /** |
1141 | * Do not call this function from places outside FileBackend |
1142 | * |
1143 | * @see FileBackendStore::getDirectoryList() |
1144 | * |
1145 | * @param string $container Resolved container name |
1146 | * @param string $dir Resolved path relative to container |
1147 | * @param array $params |
1148 | * @return Traversable|array|null Iterable list or null (error) |
1149 | */ |
1150 | abstract public function getDirectoryListInternal( $container, $dir, array $params ); |
1151 | |
1152 | final public function getFileList( array $params ) { |
1153 | [ $fullCont, $dir, $shard ] = $this->resolveStoragePath( $params['dir'] ); |
1154 | if ( $dir === null ) { |
1155 | return self::LIST_ERROR; // invalid storage path |
1156 | } |
1157 | if ( $shard !== null ) { |
1158 | // File listing is confined to a single container/shard |
1159 | return $this->getFileListInternal( $fullCont, $dir, $params ); |
1160 | } else { |
1161 | $this->logger->debug( __METHOD__ . ": iterating over all container shards." ); |
1162 | // File listing spans multiple containers/shards |
1163 | [ , $shortCont, ] = self::splitStoragePath( $params['dir'] ); |
1164 | |
1165 | return new FileBackendStoreShardFileIterator( $this, |
1166 | $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); |
1167 | } |
1168 | } |
1169 | |
1170 | /** |
1171 | * Do not call this function from places outside FileBackend |
1172 | * |
1173 | * @see FileBackendStore::getFileList() |
1174 | * |
1175 | * @param string $container Resolved container name |
1176 | * @param string $dir Resolved path relative to container |
1177 | * @param array $params |
1178 | * @return Traversable|string[]|null Iterable list or null (error) |
1179 | */ |
1180 | abstract public function getFileListInternal( $container, $dir, array $params ); |
1181 | |
1182 | /** |
1183 | * Return a list of FileOp objects from a list of operations. |
1184 | * Do not call this function from places outside FileBackend. |
1185 | * |
1186 | * The result must have the same number of items as the input. |
1187 | * An exception is thrown if an unsupported operation is requested. |
1188 | * |
1189 | * @param array[] $ops Same format as doOperations() |
1190 | * @return FileOp[] |
1191 | * @throws FileBackendError |
1192 | */ |
1193 | final public function getOperationsInternal( array $ops ) { |
1194 | $supportedOps = [ |
1195 | 'store' => StoreFileOp::class, |
1196 | 'copy' => CopyFileOp::class, |
1197 | 'move' => MoveFileOp::class, |
1198 | 'delete' => DeleteFileOp::class, |
1199 | 'create' => CreateFileOp::class, |
1200 | 'describe' => DescribeFileOp::class, |
1201 | 'null' => NullFileOp::class |
1202 | ]; |
1203 | |
1204 | $performOps = []; // array of FileOp objects |
1205 | // Build up ordered array of FileOps... |
1206 | foreach ( $ops as $operation ) { |
1207 | $opName = $operation['op']; |
1208 | if ( isset( $supportedOps[$opName] ) ) { |
1209 | $class = $supportedOps[$opName]; |
1210 | // Get params for this operation |
1211 | $params = $operation; |
1212 | // Append the FileOp class |
1213 | $performOps[] = new $class( $this, $params, $this->logger ); |
1214 | } else { |
1215 | throw new FileBackendError( "Operation '$opName' is not supported." ); |
1216 | } |
1217 | } |
1218 | |
1219 | return $performOps; |
1220 | } |
1221 | |
1222 | /** |
1223 | * Get a list of storage paths to lock for a list of operations |
1224 | * Returns an array with LockManager::LOCK_UW (shared locks) and |
1225 | * LockManager::LOCK_EX (exclusive locks) keys, each corresponding |
1226 | * to a list of storage paths to be locked. All returned paths are |
1227 | * normalized. |
1228 | * |
1229 | * @param FileOp[] $performOps List of FileOp objects |
1230 | * @return string[][] (LockManager::LOCK_UW => path list, LockManager::LOCK_EX => path list) |
1231 | */ |
1232 | final public function getPathsToLockForOpsInternal( array $performOps ) { |
1233 | // Build up a list of files to lock... |
1234 | $paths = [ 'sh' => [], 'ex' => [] ]; |
1235 | foreach ( $performOps as $fileOp ) { |
1236 | $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() ); |
1237 | $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() ); |
1238 | } |
1239 | // Optimization: if doing an EX lock anyway, don't also set an SH one |
1240 | $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] ); |
1241 | // Get a shared lock on the parent directory of each path changed |
1242 | $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) ); |
1243 | |
1244 | return [ |
1245 | LockManager::LOCK_UW => $paths['sh'], |
1246 | LockManager::LOCK_EX => $paths['ex'] |
1247 | ]; |
1248 | } |
1249 | |
1250 | public function getScopedLocksForOps( array $ops, StatusValue $status ) { |
1251 | $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) ); |
1252 | |
1253 | return $this->getScopedFileLocks( $paths, 'mixed', $status ); |
1254 | } |
1255 | |
1256 | final protected function doOperationsInternal( array $ops, array $opts ) { |
1257 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1258 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1259 | $status = $this->newStatus(); |
1260 | |
1261 | // Fix up custom header name/value pairs |
1262 | $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops ); |
1263 | // Build up a list of FileOps and involved paths |
1264 | $fileOps = $this->getOperationsInternal( $ops ); |
1265 | $pathsUsed = []; |
1266 | foreach ( $fileOps as $fileOp ) { |
1267 | $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() ); |
1268 | } |
1269 | |
1270 | // Acquire any locks as needed for the scope of this function |
1271 | if ( empty( $opts['nonLocking'] ) ) { |
1272 | $pathsByLockType = $this->getPathsToLockForOpsInternal( $fileOps ); |
1273 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1274 | $scopeLock = $this->getScopedFileLocks( $pathsByLockType, 'mixed', $status ); |
1275 | if ( !$status->isOK() ) { |
1276 | return $status; // abort |
1277 | } |
1278 | } |
1279 | |
1280 | // Clear any file cache entries (after locks acquired) |
1281 | if ( empty( $opts['preserveCache'] ) ) { |
1282 | $this->clearCache( $pathsUsed ); |
1283 | } |
1284 | |
1285 | // Enlarge the cache to fit the stat entries of these files |
1286 | $this->cheapCache->setMaxSize( max( 2 * count( $pathsUsed ), self::CACHE_CHEAP_SIZE ) ); |
1287 | |
1288 | // Load from the persistent container caches |
1289 | $this->primeContainerCache( $pathsUsed ); |
1290 | // Get the latest stat info for all the files (having locked them) |
1291 | $ok = $this->preloadFileStat( [ 'srcs' => $pathsUsed, 'latest' => true ] ); |
1292 | |
1293 | if ( $ok ) { |
1294 | // Actually attempt the operation batch... |
1295 | $opts = $this->setConcurrencyFlags( $opts ); |
1296 | $subStatus = FileOpBatch::attempt( $fileOps, $opts ); |
1297 | } else { |
1298 | // If we could not even stat some files, then bail out |
1299 | $subStatus = $this->newStatus( 'backend-fail-internal', $this->name ); |
1300 | foreach ( $ops as $i => $op ) { // mark each op as failed |
1301 | $subStatus->success[$i] = false; |
1302 | ++$subStatus->failCount; |
1303 | } |
1304 | $this->logger->error( static::class . "-{$this->name} " . |
1305 | " stat failure; aborted operations: " . FormatJson::encode( $ops ) ); |
1306 | } |
1307 | |
1308 | // Merge errors into StatusValue fields |
1309 | $status->merge( $subStatus ); |
1310 | $status->success = $subStatus->success; // not done in merge() |
1311 | |
1312 | // Shrink the stat cache back to normal size |
1313 | $this->cheapCache->setMaxSize( self::CACHE_CHEAP_SIZE ); |
1314 | |
1315 | return $status; |
1316 | } |
1317 | |
1318 | final protected function doQuickOperationsInternal( array $ops, array $opts ) { |
1319 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1320 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1321 | $status = $this->newStatus(); |
1322 | |
1323 | // Fix up custom header name/value pairs |
1324 | $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops ); |
1325 | // Build up a list of FileOps and involved paths |
1326 | $fileOps = $this->getOperationsInternal( $ops ); |
1327 | $pathsUsed = []; |
1328 | foreach ( $fileOps as $fileOp ) { |
1329 | $pathsUsed = array_merge( $pathsUsed, $fileOp->storagePathsReadOrChanged() ); |
1330 | } |
1331 | |
1332 | // Clear any file cache entries for involved paths |
1333 | $this->clearCache( $pathsUsed ); |
1334 | |
1335 | // Parallel ops may be disabled in config due to dependencies (e.g. needing popen()) |
1336 | $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 ); |
1337 | $maxConcurrency = $this->concurrency; // throttle |
1338 | /** @var StatusValue[] $statuses */ |
1339 | $statuses = []; // array of (index => StatusValue) |
1340 | /** @var FileBackendStoreOpHandle[] $batch */ |
1341 | $batch = []; |
1342 | foreach ( $fileOps as $index => $fileOp ) { |
1343 | $subStatus = $async |
1344 | ? $fileOp->attemptAsyncQuick() |
1345 | : $fileOp->attemptQuick(); |
1346 | if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async |
1347 | if ( count( $batch ) >= $maxConcurrency ) { |
1348 | // Execute this batch. Don't queue any more ops since they contain |
1349 | // open filehandles which are a limited resource (T230245). |
1350 | $statuses += $this->executeOpHandlesInternal( $batch ); |
1351 | $batch = []; |
1352 | } |
1353 | $batch[$index] = $subStatus->value; // keep index |
1354 | } else { // error or completed |
1355 | $statuses[$index] = $subStatus; // keep index |
1356 | } |
1357 | } |
1358 | if ( count( $batch ) ) { |
1359 | $statuses += $this->executeOpHandlesInternal( $batch ); |
1360 | } |
1361 | // Marshall and merge all the responses... |
1362 | foreach ( $statuses as $index => $subStatus ) { |
1363 | $status->merge( $subStatus ); |
1364 | if ( $subStatus->isOK() ) { |
1365 | $status->success[$index] = true; |
1366 | ++$status->successCount; |
1367 | } else { |
1368 | $status->success[$index] = false; |
1369 | ++$status->failCount; |
1370 | } |
1371 | } |
1372 | |
1373 | $this->clearCache( $pathsUsed ); |
1374 | |
1375 | return $status; |
1376 | } |
1377 | |
1378 | /** |
1379 | * Execute a list of FileBackendStoreOpHandle handles in parallel. |
1380 | * The resulting StatusValue object fields will correspond |
1381 | * to the order in which the handles where given. |
1382 | * |
1383 | * @param FileBackendStoreOpHandle[] $fileOpHandles |
1384 | * @return StatusValue[] Map of StatusValue objects |
1385 | * @throws FileBackendError |
1386 | */ |
1387 | final public function executeOpHandlesInternal( array $fileOpHandles ) { |
1388 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1389 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1390 | |
1391 | foreach ( $fileOpHandles as $fileOpHandle ) { |
1392 | if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) { |
1393 | throw new InvalidArgumentException( "Expected FileBackendStoreOpHandle object." ); |
1394 | } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) { |
1395 | throw new InvalidArgumentException( "Expected handle for this file backend." ); |
1396 | } |
1397 | } |
1398 | |
1399 | $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles ); |
1400 | foreach ( $fileOpHandles as $fileOpHandle ) { |
1401 | $fileOpHandle->closeResources(); |
1402 | } |
1403 | |
1404 | return $statuses; |
1405 | } |
1406 | |
1407 | /** |
1408 | * @see FileBackendStore::executeOpHandlesInternal() |
1409 | * @stable to override |
1410 | * |
1411 | * @param FileBackendStoreOpHandle[] $fileOpHandles |
1412 | * |
1413 | * @throws FileBackendError |
1414 | * @return StatusValue[] List of corresponding StatusValue objects |
1415 | */ |
1416 | protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { |
1417 | if ( count( $fileOpHandles ) ) { |
1418 | throw new FileBackendError( "Backend does not support asynchronous operations." ); |
1419 | } |
1420 | |
1421 | return []; |
1422 | } |
1423 | |
1424 | /** |
1425 | * Normalize and filter HTTP headers from a file operation |
1426 | * |
1427 | * This normalizes and strips long HTTP headers from a file operation. |
1428 | * Most headers are just numbers, but some are allowed to be long. |
1429 | * This function is useful for cleaning up headers and avoiding backend |
1430 | * specific errors, especially in the middle of batch file operations. |
1431 | * |
1432 | * @param array $op Same format as doOperation() |
1433 | * @return array |
1434 | */ |
1435 | protected function sanitizeOpHeaders( array $op ) { |
1436 | static $longs = [ 'content-disposition' ]; |
1437 | |
1438 | if ( isset( $op['headers'] ) ) { // op sets HTTP headers |
1439 | $newHeaders = []; |
1440 | foreach ( $op['headers'] as $name => $value ) { |
1441 | $name = strtolower( $name ); |
1442 | $maxHVLen = in_array( $name, $longs ) ? INF : 255; |
1443 | if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) { |
1444 | $this->logger->error( "Header '{header}' is too long.", [ |
1445 | 'filebackend' => $this->name, |
1446 | 'header' => "$name: $value", |
1447 | ] ); |
1448 | } else { |
1449 | $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => "" |
1450 | } |
1451 | } |
1452 | $op['headers'] = $newHeaders; |
1453 | } |
1454 | |
1455 | return $op; |
1456 | } |
1457 | |
1458 | final public function preloadCache( array $paths ) { |
1459 | $fullConts = []; // full container names |
1460 | foreach ( $paths as $path ) { |
1461 | [ $fullCont, , ] = $this->resolveStoragePath( $path ); |
1462 | $fullConts[] = $fullCont; |
1463 | } |
1464 | // Load from the persistent file and container caches |
1465 | $this->primeContainerCache( $fullConts ); |
1466 | $this->primeFileCache( $paths ); |
1467 | } |
1468 | |
1469 | final public function clearCache( array $paths = null ) { |
1470 | if ( is_array( $paths ) ) { |
1471 | $paths = array_map( [ FileBackend::class, 'normalizeStoragePath' ], $paths ); |
1472 | $paths = array_filter( $paths, 'strlen' ); // remove nulls |
1473 | } |
1474 | if ( $paths === null ) { |
1475 | $this->cheapCache->clear(); |
1476 | $this->expensiveCache->clear(); |
1477 | } else { |
1478 | foreach ( $paths as $path ) { |
1479 | $this->cheapCache->clear( $path ); |
1480 | $this->expensiveCache->clear( $path ); |
1481 | } |
1482 | } |
1483 | $this->doClearCache( $paths ); |
1484 | } |
1485 | |
1486 | /** |
1487 | * Clears any additional stat caches for storage paths |
1488 | * @stable to override |
1489 | * |
1490 | * @see FileBackend::clearCache() |
1491 | * |
1492 | * @param string[]|null $paths Storage paths (optional) |
1493 | */ |
1494 | protected function doClearCache( array $paths = null ) { |
1495 | } |
1496 | |
1497 | final public function preloadFileStat( array $params ) { |
1498 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1499 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1500 | |
1501 | $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1; |
1502 | $stats = $this->doGetFileStatMulti( $params ); |
1503 | if ( $stats === null ) { |
1504 | return true; // not supported |
1505 | } |
1506 | |
1507 | // Whether this queried the backend in high consistency mode |
1508 | $latest = !empty( $params['latest'] ); |
1509 | |
1510 | return $this->ingestFreshFileStats( $stats, $latest ); |
1511 | } |
1512 | |
1513 | /** |
1514 | * Get file stat information (concurrently if possible) for several files |
1515 | * @stable to override |
1516 | * |
1517 | * @see FileBackend::getFileStat() |
1518 | * |
1519 | * @param array $params Parameters include: |
1520 | * - srcs : list of source storage paths |
1521 | * - latest : use the latest available data |
1522 | * @return array<string,array|false|null>|null Null if not supported. Otherwise a map of storage |
1523 | * path to attribute map, false (missing file), or null (I/O error). |
1524 | * @since 1.23 |
1525 | */ |
1526 | protected function doGetFileStatMulti( array $params ) { |
1527 | return null; // not supported |
1528 | } |
1529 | |
1530 | /** |
1531 | * Is this a key/value store where directories are just virtual? |
1532 | * Virtual directories exists in so much as files exists that are |
1533 | * prefixed with the directory path followed by a forward slash. |
1534 | * |
1535 | * @return bool |
1536 | */ |
1537 | abstract protected function directoriesAreVirtual(); |
1538 | |
1539 | /** |
1540 | * Check if a short container name is valid |
1541 | * |
1542 | * This checks for length and illegal characters. |
1543 | * This may disallow certain characters that can appear |
1544 | * in the prefix used to make the full container name. |
1545 | * |
1546 | * @param string $container |
1547 | * @return bool |
1548 | */ |
1549 | final protected static function isValidShortContainerName( $container ) { |
1550 | // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments) |
1551 | // might be used by subclasses. Reserve the dot character. |
1552 | // The only way dots end up in containers (e.g. resolveStoragePath) |
1553 | // is due to the wikiId container prefix or the above suffixes. |
1554 | return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container ); |
1555 | } |
1556 | |
1557 | /** |
1558 | * Check if a full container name is valid |
1559 | * |
1560 | * This checks for length and illegal characters. |
1561 | * Limiting the characters makes migrations to other stores easier. |
1562 | * |
1563 | * @param string $container |
1564 | * @return bool |
1565 | */ |
1566 | final protected static function isValidContainerName( $container ) { |
1567 | // This accounts for NTFS, Swift, and Ceph restrictions |
1568 | // and disallows directory separators or traversal characters. |
1569 | // Note that matching strings URL encode to the same string; |
1570 | // in Swift/Ceph, the length restriction is *after* URL encoding. |
1571 | return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container ); |
1572 | } |
1573 | |
1574 | /** |
1575 | * Splits a storage path into an internal container name, |
1576 | * an internal relative file name, and a container shard suffix. |
1577 | * Any shard suffix is already appended to the internal container name. |
1578 | * This also checks that the storage path is valid and within this backend. |
1579 | * |
1580 | * If the container is sharded but a suffix could not be determined, |
1581 | * this means that the path can only refer to a directory and can only |
1582 | * be scanned by looking in all the container shards. |
1583 | * |
1584 | * @param string $storagePath |
1585 | * @return array (container, path, container suffix) or (null, null, null) if invalid |
1586 | */ |
1587 | final protected function resolveStoragePath( $storagePath ) { |
1588 | [ $backend, $shortCont, $relPath ] = self::splitStoragePath( $storagePath ); |
1589 | if ( $backend === $this->name ) { // must be for this backend |
1590 | $relPath = self::normalizeContainerPath( $relPath ); |
1591 | if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) { |
1592 | // Get shard for the normalized path if this container is sharded |
1593 | $cShard = $this->getContainerShard( $shortCont, $relPath ); |
1594 | // Validate and sanitize the relative path (backend-specific) |
1595 | $relPath = $this->resolveContainerPath( $shortCont, $relPath ); |
1596 | if ( $relPath !== null ) { |
1597 | // Prepend any domain ID prefix to the container name |
1598 | $container = $this->fullContainerName( $shortCont ); |
1599 | if ( self::isValidContainerName( $container ) ) { |
1600 | // Validate and sanitize the container name (backend-specific) |
1601 | $container = $this->resolveContainerName( "{$container}{$cShard}" ); |
1602 | if ( $container !== null ) { |
1603 | return [ $container, $relPath, $cShard ]; |
1604 | } |
1605 | } |
1606 | } |
1607 | } |
1608 | } |
1609 | |
1610 | return [ null, null, null ]; |
1611 | } |
1612 | |
1613 | /** |
1614 | * Like resolveStoragePath() except null values are returned if |
1615 | * the container is sharded and the shard could not be determined |
1616 | * or if the path ends with '/'. The latter case is illegal for FS |
1617 | * backends and can confuse listings for object store backends. |
1618 | * |
1619 | * This function is used when resolving paths that must be valid |
1620 | * locations for files. Directory and listing functions should |
1621 | * generally just use resolveStoragePath() instead. |
1622 | * |
1623 | * @see FileBackendStore::resolveStoragePath() |
1624 | * |
1625 | * @param string $storagePath |
1626 | * @return array (container, path) or (null, null) if invalid |
1627 | */ |
1628 | final protected function resolveStoragePathReal( $storagePath ) { |
1629 | [ $container, $relPath, $cShard ] = $this->resolveStoragePath( $storagePath ); |
1630 | if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) { |
1631 | return [ $container, $relPath ]; |
1632 | } |
1633 | |
1634 | return [ null, null ]; |
1635 | } |
1636 | |
1637 | /** |
1638 | * Get the container name shard suffix for a given path. |
1639 | * Any empty suffix means the container is not sharded. |
1640 | * |
1641 | * @param string $container Container name |
1642 | * @param string $relPath Storage path relative to the container |
1643 | * @return string|null Returns null if shard could not be determined |
1644 | */ |
1645 | final protected function getContainerShard( $container, $relPath ) { |
1646 | [ $levels, $base, $repeat ] = $this->getContainerHashLevels( $container ); |
1647 | if ( $levels == 1 || $levels == 2 ) { |
1648 | // Hash characters are either base 16 or 36 |
1649 | $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]'; |
1650 | // Get a regex that represents the shard portion of paths. |
1651 | // The concatenation of the captures gives us the shard. |
1652 | if ( $levels === 1 ) { // 16 or 36 shards per container |
1653 | $hashDirRegex = '(' . $char . ')'; |
1654 | } else { // 256 or 1296 shards per container |
1655 | if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc") |
1656 | $hashDirRegex = $char . '/(' . $char . '{2})'; |
1657 | } else { // short hash dir format (e.g. "a/b/c") |
1658 | $hashDirRegex = '(' . $char . ')/(' . $char . ')'; |
1659 | } |
1660 | } |
1661 | // Allow certain directories to be above the hash dirs so as |
1662 | // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab"). |
1663 | // They must be 2+ chars to avoid any hash directory ambiguity. |
1664 | $m = []; |
1665 | if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) { |
1666 | return '.' . implode( '', array_slice( $m, 1 ) ); |
1667 | } |
1668 | |
1669 | return null; // failed to match |
1670 | } |
1671 | |
1672 | return ''; // no sharding |
1673 | } |
1674 | |
1675 | /** |
1676 | * Check if a storage path maps to a single shard. |
1677 | * Container dirs like "a", where the container shards on "x/xy", |
1678 | * can reside on several shards. Such paths are tricky to handle. |
1679 | * |
1680 | * @param string $storagePath |
1681 | * @return bool |
1682 | */ |
1683 | final public function isSingleShardPathInternal( $storagePath ) { |
1684 | [ , , $shard ] = $this->resolveStoragePath( $storagePath ); |
1685 | |
1686 | return ( $shard !== null ); |
1687 | } |
1688 | |
1689 | /** |
1690 | * Get the sharding config for a container. |
1691 | * If greater than 0, then all file storage paths within |
1692 | * the container are required to be hashed accordingly. |
1693 | * |
1694 | * @param string $container |
1695 | * @return array (integer levels, integer base, repeat flag) or (0, 0, false) |
1696 | */ |
1697 | final protected function getContainerHashLevels( $container ) { |
1698 | if ( isset( $this->shardViaHashLevels[$container] ) ) { |
1699 | $config = $this->shardViaHashLevels[$container]; |
1700 | $hashLevels = (int)$config['levels']; |
1701 | if ( $hashLevels == 1 || $hashLevels == 2 ) { |
1702 | $hashBase = (int)$config['base']; |
1703 | if ( $hashBase == 16 || $hashBase == 36 ) { |
1704 | return [ $hashLevels, $hashBase, $config['repeat'] ]; |
1705 | } |
1706 | } |
1707 | } |
1708 | |
1709 | return [ 0, 0, false ]; // no sharding |
1710 | } |
1711 | |
1712 | /** |
1713 | * Get a list of full container shard suffixes for a container |
1714 | * |
1715 | * @param string $container |
1716 | * @return array |
1717 | */ |
1718 | final protected function getContainerSuffixes( $container ) { |
1719 | $shards = []; |
1720 | [ $digits, $base ] = $this->getContainerHashLevels( $container ); |
1721 | if ( $digits > 0 ) { |
1722 | $numShards = $base ** $digits; |
1723 | for ( $index = 0; $index < $numShards; $index++ ) { |
1724 | $shards[] = '.' . Wikimedia\base_convert( (string)$index, 10, $base, $digits ); |
1725 | } |
1726 | } |
1727 | |
1728 | return $shards; |
1729 | } |
1730 | |
1731 | /** |
1732 | * Get the full container name, including the domain ID prefix |
1733 | * |
1734 | * @param string $container |
1735 | * @return string |
1736 | */ |
1737 | final protected function fullContainerName( $container ) { |
1738 | if ( $this->domainId != '' ) { |
1739 | return "{$this->domainId}-$container"; |
1740 | } else { |
1741 | return $container; |
1742 | } |
1743 | } |
1744 | |
1745 | /** |
1746 | * Resolve a container name, checking if it's allowed by the backend. |
1747 | * This is intended for internal use, such as encoding illegal chars. |
1748 | * Subclasses can override this to be more restrictive. |
1749 | * @stable to override |
1750 | * |
1751 | * @param string $container |
1752 | * @return string|null |
1753 | */ |
1754 | protected function resolveContainerName( $container ) { |
1755 | return $container; |
1756 | } |
1757 | |
1758 | /** |
1759 | * Resolve a relative storage path, checking if it's allowed by the backend. |
1760 | * This is intended for internal use, such as encoding illegal chars or perhaps |
1761 | * getting absolute paths (e.g. FS based backends). Note that the relative path |
1762 | * may be the empty string (e.g. the path is simply to the container). |
1763 | * @stable to override |
1764 | * |
1765 | * @param string $container Container name |
1766 | * @param string $relStoragePath Storage path relative to the container |
1767 | * @return string|null Path or null if not valid |
1768 | */ |
1769 | protected function resolveContainerPath( $container, $relStoragePath ) { |
1770 | return $relStoragePath; |
1771 | } |
1772 | |
1773 | /** |
1774 | * Get the cache key for a container |
1775 | * |
1776 | * @param string $container Resolved container name |
1777 | * @return string |
1778 | */ |
1779 | private function containerCacheKey( $container ) { |
1780 | return "filebackend:{$this->name}:{$this->domainId}:container:{$container}"; |
1781 | } |
1782 | |
1783 | /** |
1784 | * Set the cached info for a container |
1785 | * |
1786 | * @param string $container Resolved container name |
1787 | * @param array $val Information to cache |
1788 | */ |
1789 | final protected function setContainerCache( $container, array $val ) { |
1790 | if ( !$this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 ) ) { |
1791 | $this->logger->warning( "Unable to set stat cache for container {container}.", |
1792 | [ 'filebackend' => $this->name, 'container' => $container ] |
1793 | ); |
1794 | } |
1795 | } |
1796 | |
1797 | /** |
1798 | * Delete the cached info for a container. |
1799 | * The cache key is salted for a while to prevent race conditions. |
1800 | * |
1801 | * @param string $container Resolved container name |
1802 | */ |
1803 | final protected function deleteContainerCache( $container ) { |
1804 | if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) { |
1805 | $this->logger->warning( "Unable to delete stat cache for container {container}.", |
1806 | [ 'filebackend' => $this->name, 'container' => $container ] |
1807 | ); |
1808 | } |
1809 | } |
1810 | |
1811 | /** |
1812 | * Do a batch lookup from cache for container stats for all containers |
1813 | * used in a list of container names or storage paths objects. |
1814 | * This loads the persistent cache values into the process cache. |
1815 | * |
1816 | * @param array $items |
1817 | */ |
1818 | final protected function primeContainerCache( array $items ) { |
1819 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1820 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1821 | |
1822 | $paths = []; // list of storage paths |
1823 | $contNames = []; // (cache key => resolved container name) |
1824 | // Get all the paths/containers from the items... |
1825 | foreach ( $items as $item ) { |
1826 | if ( self::isStoragePath( $item ) ) { |
1827 | $paths[] = $item; |
1828 | } elseif ( is_string( $item ) ) { // full container name |
1829 | $contNames[$this->containerCacheKey( $item )] = $item; |
1830 | } |
1831 | } |
1832 | // Get all the corresponding cache keys for paths... |
1833 | foreach ( $paths as $path ) { |
1834 | [ $fullCont, , ] = $this->resolveStoragePath( $path ); |
1835 | if ( $fullCont !== null ) { // valid path for this backend |
1836 | $contNames[$this->containerCacheKey( $fullCont )] = $fullCont; |
1837 | } |
1838 | } |
1839 | |
1840 | $contInfo = []; // (resolved container name => cache value) |
1841 | // Get all cache entries for these container cache keys... |
1842 | $values = $this->memCache->getMulti( array_keys( $contNames ) ); |
1843 | foreach ( $values as $cacheKey => $val ) { |
1844 | $contInfo[$contNames[$cacheKey]] = $val; |
1845 | } |
1846 | |
1847 | // Populate the container process cache for the backend... |
1848 | $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) ); |
1849 | } |
1850 | |
1851 | /** |
1852 | * Fill the backend-specific process cache given an array of |
1853 | * resolved container names and their corresponding cached info. |
1854 | * Only containers that actually exist should appear in the map. |
1855 | * @stable to override |
1856 | * |
1857 | * @param array $containerInfo Map of resolved container names to cached info |
1858 | */ |
1859 | protected function doPrimeContainerCache( array $containerInfo ) { |
1860 | } |
1861 | |
1862 | /** |
1863 | * Get the cache key for a file path |
1864 | * |
1865 | * @param string $path Normalized storage path |
1866 | * @return string |
1867 | */ |
1868 | private function fileCacheKey( $path ) { |
1869 | return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path ); |
1870 | } |
1871 | |
1872 | /** |
1873 | * Set the cached stat info for a file path. |
1874 | * Negatives (404s) are not cached. By not caching negatives, we can skip cache |
1875 | * salting for the case when a file is created at a path were there was none before. |
1876 | * |
1877 | * @param string $path Storage path |
1878 | * @param array $val Stat information to cache |
1879 | */ |
1880 | final protected function setFileCache( $path, array $val ) { |
1881 | $path = FileBackend::normalizeStoragePath( $path ); |
1882 | if ( $path === null ) { |
1883 | return; // invalid storage path |
1884 | } |
1885 | $mtime = (int)ConvertibleTimestamp::convert( TS_UNIX, $val['mtime'] ); |
1886 | $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 ); |
1887 | $key = $this->fileCacheKey( $path ); |
1888 | // Set the cache unless it is currently salted. |
1889 | if ( !$this->memCache->set( $key, $val, $ttl ) ) { |
1890 | $this->logger->warning( "Unable to set stat cache for file {path}.", |
1891 | [ 'filebackend' => $this->name, 'path' => $path ] |
1892 | ); |
1893 | } |
1894 | } |
1895 | |
1896 | /** |
1897 | * Delete the cached stat info for a file path. |
1898 | * The cache key is salted for a while to prevent race conditions. |
1899 | * Since negatives (404s) are not cached, this does not need to be called when |
1900 | * a file is created at a path were there was none before. |
1901 | * |
1902 | * @param string $path Storage path |
1903 | */ |
1904 | final protected function deleteFileCache( $path ) { |
1905 | $path = FileBackend::normalizeStoragePath( $path ); |
1906 | if ( $path === null ) { |
1907 | return; // invalid storage path |
1908 | } |
1909 | if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) { |
1910 | $this->logger->warning( "Unable to delete stat cache for file {path}.", |
1911 | [ 'filebackend' => $this->name, 'path' => $path ] |
1912 | ); |
1913 | } |
1914 | } |
1915 | |
1916 | /** |
1917 | * Do a batch lookup from cache for file stats for all paths |
1918 | * used in a list of storage paths or FileOp objects. |
1919 | * This loads the persistent cache values into the process cache. |
1920 | * |
1921 | * @param array $items List of storage paths |
1922 | */ |
1923 | final protected function primeFileCache( array $items ) { |
1924 | /** @noinspection PhpUnusedLocalVariableInspection */ |
1925 | $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" ); |
1926 | |
1927 | $paths = []; // list of storage paths |
1928 | $pathNames = []; // (cache key => storage path) |
1929 | // Get all the paths/containers from the items... |
1930 | foreach ( $items as $item ) { |
1931 | if ( self::isStoragePath( $item ) ) { |
1932 | $path = FileBackend::normalizeStoragePath( $item ); |
1933 | if ( $path !== null ) { |
1934 | $paths[] = $path; |
1935 | } |
1936 | } |
1937 | } |
1938 | // Get all the corresponding cache keys for paths... |
1939 | foreach ( $paths as $path ) { |
1940 | [ , $rel, ] = $this->resolveStoragePath( $path ); |
1941 | if ( $rel !== null ) { // valid path for this backend |
1942 | $pathNames[$this->fileCacheKey( $path )] = $path; |
1943 | } |
1944 | } |
1945 | // Get all cache entries for these file cache keys. |
1946 | // Note that negatives are not cached by getFileStat()/preloadFileStat(). |
1947 | $values = $this->memCache->getMulti( array_keys( $pathNames ) ); |
1948 | // Load all of the results into process cache... |
1949 | foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) { |
1950 | $path = $pathNames[$cacheKey]; |
1951 | // This flag only applies to stat info loaded directly |
1952 | // from a high consistency backend query to the process cache |
1953 | unset( $stat['latest'] ); |
1954 | |
1955 | $this->cheapCache->setField( $path, 'stat', $stat ); |
1956 | if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) { |
1957 | // Some backends store SHA-1 as metadata |
1958 | $this->cheapCache->setField( |
1959 | $path, |
1960 | 'sha1', |
1961 | [ 'hash' => $stat['sha1'], 'latest' => false ] |
1962 | ); |
1963 | } |
1964 | if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) { |
1965 | // Some backends store custom headers/metadata |
1966 | $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] ); |
1967 | $this->cheapCache->setField( |
1968 | $path, |
1969 | 'xattr', |
1970 | [ 'map' => $stat['xattr'], 'latest' => false ] |
1971 | ); |
1972 | } |
1973 | } |
1974 | } |
1975 | |
1976 | /** |
1977 | * Normalize file headers/metadata to the FileBackend::getFileXAttributes() format |
1978 | * |
1979 | * @param array $xattr |
1980 | * @return array |
1981 | * @since 1.22 |
1982 | */ |
1983 | final protected static function normalizeXAttributes( array $xattr ) { |
1984 | $newXAttr = [ 'headers' => [], 'metadata' => [] ]; |
1985 | |
1986 | foreach ( $xattr['headers'] as $name => $value ) { |
1987 | $newXAttr['headers'][strtolower( $name )] = $value; |
1988 | } |
1989 | |
1990 | foreach ( $xattr['metadata'] as $name => $value ) { |
1991 | $newXAttr['metadata'][strtolower( $name )] = $value; |
1992 | } |
1993 | |
1994 | return $newXAttr; |
1995 | } |
1996 | |
1997 | /** |
1998 | * Set the 'concurrency' option from a list of operation options |
1999 | * |
2000 | * @param array $opts Map of operation options |
2001 | * @return array |
2002 | */ |
2003 | final protected function setConcurrencyFlags( array $opts ) { |
2004 | $opts['concurrency'] = 1; // off |
2005 | if ( $this->parallelize === 'implicit' ) { |
2006 | if ( $opts['parallelize'] ?? true ) { |
2007 | $opts['concurrency'] = $this->concurrency; |
2008 | } |
2009 | } elseif ( $this->parallelize === 'explicit' ) { |
2010 | if ( !empty( $opts['parallelize'] ) ) { |
2011 | $opts['concurrency'] = $this->concurrency; |
2012 | } |
2013 | } |
2014 | |
2015 | return $opts; |
2016 | } |
2017 | |
2018 | /** |
2019 | * Get the content type to use in HEAD/GET requests for a file |
2020 | * @stable to override |
2021 | * |
2022 | * @param string $storagePath |
2023 | * @param string|null $content File data |
2024 | * @param string|null $fsPath File system path |
2025 | * @return string MIME type |
2026 | */ |
2027 | protected function getContentType( $storagePath, $content, $fsPath ) { |
2028 | if ( $this->mimeCallback ) { |
2029 | return call_user_func_array( $this->mimeCallback, func_get_args() ); |
2030 | } |
2031 | |
2032 | $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false; |
2033 | return $mime ?: 'unknown/unknown'; |
2034 | } |
2035 | } |