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