Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.04% |
97 / 101 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
FileBackendGroup | |
97.00% |
97 / 100 |
|
83.33% |
5 / 6 |
27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
4 | |||
register | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
get | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
7.33 | |||
config | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
backendFromPath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
guessMimeInternal | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * File backend registration handling. |
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 MediaWiki\FileBackend; |
25 | |
26 | use InvalidArgumentException; |
27 | use LogicException; |
28 | use MediaWiki\Config\ServiceOptions; |
29 | use MediaWiki\FileBackend\FSFile\TempFSFileFactory; |
30 | use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory; |
31 | use MediaWiki\Logger\LoggerFactory; |
32 | use MediaWiki\MainConfigNames; |
33 | use MediaWiki\Output\StreamFile; |
34 | use MediaWiki\Status\Status; |
35 | use Profiler; |
36 | use Wikimedia\FileBackend\FileBackend; |
37 | use Wikimedia\FileBackend\FileBackendMultiWrite; |
38 | use Wikimedia\FileBackend\FSFileBackend; |
39 | use Wikimedia\Mime\MimeAnalyzer; |
40 | use Wikimedia\ObjectCache\BagOStuff; |
41 | use Wikimedia\ObjectCache\WANObjectCache; |
42 | use Wikimedia\ObjectFactory\ObjectFactory; |
43 | use Wikimedia\Rdbms\ReadOnlyMode; |
44 | |
45 | /** |
46 | * Class to handle file backend registration |
47 | * |
48 | * @ingroup FileBackend |
49 | * @since 1.19 |
50 | */ |
51 | class FileBackendGroup { |
52 | /** |
53 | * @var array[] (name => ('class' => string, 'config' => array, 'instance' => object)) |
54 | * @phan-var array<string,array{class:class-string,config:array,instance:object}> |
55 | */ |
56 | protected $backends = []; |
57 | |
58 | /** @var ServiceOptions */ |
59 | private $options; |
60 | |
61 | /** @var BagOStuff */ |
62 | private $srvCache; |
63 | |
64 | /** @var WANObjectCache */ |
65 | private $wanCache; |
66 | |
67 | /** @var MimeAnalyzer */ |
68 | private $mimeAnalyzer; |
69 | |
70 | /** @var LockManagerGroupFactory */ |
71 | private $lmgFactory; |
72 | |
73 | /** @var TempFSFileFactory */ |
74 | private $tmpFileFactory; |
75 | |
76 | /** @var ObjectFactory */ |
77 | private $objectFactory; |
78 | |
79 | /** |
80 | * @internal For use by ServiceWiring |
81 | */ |
82 | public const CONSTRUCTOR_OPTIONS = [ |
83 | MainConfigNames::DirectoryMode, |
84 | MainConfigNames::FileBackends, |
85 | MainConfigNames::ForeignFileRepos, |
86 | MainConfigNames::LocalFileRepo, |
87 | 'fallbackWikiId', |
88 | ]; |
89 | |
90 | /** |
91 | * @param ServiceOptions $options |
92 | * @param ReadOnlyMode $readOnlyMode |
93 | * @param BagOStuff $srvCache |
94 | * @param WANObjectCache $wanCache |
95 | * @param MimeAnalyzer $mimeAnalyzer |
96 | * @param LockManagerGroupFactory $lmgFactory |
97 | * @param TempFSFileFactory $tmpFileFactory |
98 | * @param ObjectFactory $objectFactory |
99 | */ |
100 | public function __construct( |
101 | ServiceOptions $options, |
102 | ReadOnlyMode $readOnlyMode, |
103 | BagOStuff $srvCache, |
104 | WANObjectCache $wanCache, |
105 | MimeAnalyzer $mimeAnalyzer, |
106 | LockManagerGroupFactory $lmgFactory, |
107 | TempFSFileFactory $tmpFileFactory, |
108 | ObjectFactory $objectFactory |
109 | ) { |
110 | $this->options = $options; |
111 | $this->srvCache = $srvCache; |
112 | $this->wanCache = $wanCache; |
113 | $this->mimeAnalyzer = $mimeAnalyzer; |
114 | $this->lmgFactory = $lmgFactory; |
115 | $this->tmpFileFactory = $tmpFileFactory; |
116 | $this->objectFactory = $objectFactory; |
117 | |
118 | // Register explicitly defined backends |
119 | $this->register( $options->get( MainConfigNames::FileBackends ), $readOnlyMode->getConfiguredReason() ); |
120 | |
121 | $autoBackends = []; |
122 | // Automatically create b/c backends for file repos... |
123 | $repos = array_merge( |
124 | $options->get( MainConfigNames::ForeignFileRepos ), [ $options->get( MainConfigNames::LocalFileRepo ) ] ); |
125 | foreach ( $repos as $info ) { |
126 | $backendName = $info['backend']; |
127 | if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) { |
128 | continue; // already defined (or set to the object for some reason) |
129 | } |
130 | $repoName = $info['name']; |
131 | // Local vars that used to be FSRepo members... |
132 | $directory = $info['directory']; |
133 | $deletedDir = $info['deletedDir'] ?? false; // deletion disabled |
134 | $thumbDir = $info['thumbDir'] ?? "{$directory}/thumb"; |
135 | $transcodedDir = $info['transcodedDir'] ?? "{$directory}/transcoded"; |
136 | $lockManager = $info['lockManager'] ?? 'fsLockManager'; |
137 | // Get the FS backend configuration |
138 | $autoBackends[] = [ |
139 | 'name' => $backendName, |
140 | 'class' => FSFileBackend::class, |
141 | 'lockManager' => $lockManager, |
142 | 'containerPaths' => [ |
143 | "{$repoName}-public" => "{$directory}", |
144 | "{$repoName}-thumb" => $thumbDir, |
145 | "{$repoName}-transcoded" => $transcodedDir, |
146 | "{$repoName}-deleted" => $deletedDir, |
147 | "{$repoName}-temp" => "{$directory}/temp" |
148 | ], |
149 | 'fileMode' => $info['fileMode'] ?? 0644, |
150 | 'directoryMode' => $options->get( MainConfigNames::DirectoryMode ), |
151 | ]; |
152 | } |
153 | |
154 | // Register implicitly defined backends |
155 | $this->register( $autoBackends, $readOnlyMode->getConfiguredReason() ); |
156 | } |
157 | |
158 | /** |
159 | * Register an array of file backend configurations |
160 | * |
161 | * @param array[] $configs |
162 | * @param string|null $readOnlyReason |
163 | */ |
164 | protected function register( array $configs, $readOnlyReason = null ) { |
165 | foreach ( $configs as $config ) { |
166 | if ( !isset( $config['name'] ) ) { |
167 | throw new InvalidArgumentException( "Cannot register a backend with no name." ); |
168 | } |
169 | $name = $config['name']; |
170 | if ( isset( $this->backends[$name] ) ) { |
171 | throw new LogicException( "Backend with name '$name' already registered." ); |
172 | } elseif ( !isset( $config['class'] ) ) { |
173 | throw new InvalidArgumentException( "Backend with name '$name' has no class." ); |
174 | } |
175 | $class = $config['class']; |
176 | |
177 | $config['domainId'] ??= $config['wikiId'] ?? $this->options->get( 'fallbackWikiId' ); |
178 | $config['readOnly'] ??= $readOnlyReason; |
179 | |
180 | unset( $config['class'] ); // backend won't need this |
181 | $this->backends[$name] = [ |
182 | 'class' => $class, |
183 | 'config' => $config, |
184 | 'instance' => null |
185 | ]; |
186 | } |
187 | } |
188 | |
189 | /** |
190 | * Get the backend object with a given name |
191 | * |
192 | * @param string $name |
193 | * @return FileBackend |
194 | */ |
195 | public function get( $name ) { |
196 | // Lazy-load the actual backend instance |
197 | if ( !isset( $this->backends[$name]['instance'] ) ) { |
198 | $config = $this->config( $name ); |
199 | |
200 | $class = $config['class']; |
201 | // Checking old alias for compatibility with unchanged config |
202 | if ( $class === FileBackendMultiWrite::class || $class === \FileBackendMultiWrite::class ) { |
203 | // @todo How can we test this? What's the intended use-case? |
204 | foreach ( $config['backends'] as $index => $beConfig ) { |
205 | if ( isset( $beConfig['template'] ) ) { |
206 | // Config is just a modified version of a registered backend's. |
207 | // This should only be used when that config is used only by this backend. |
208 | $config['backends'][$index] += $this->config( $beConfig['template'] ); |
209 | } |
210 | } |
211 | } |
212 | |
213 | $this->backends[$name]['instance'] = new $class( $config ); |
214 | } |
215 | |
216 | return $this->backends[$name]['instance']; |
217 | } |
218 | |
219 | /** |
220 | * Get the config array for a backend object with a given name |
221 | * |
222 | * @param string $name |
223 | * @return array Parameters to FileBackend::__construct() |
224 | */ |
225 | public function config( $name ) { |
226 | if ( !isset( $this->backends[$name] ) ) { |
227 | throw new InvalidArgumentException( "No backend defined with the name '$name'." ); |
228 | } |
229 | |
230 | $config = $this->backends[$name]['config']; |
231 | |
232 | return array_merge( |
233 | // Default backend parameters |
234 | [ |
235 | 'mimeCallback' => [ $this, 'guessMimeInternal' ], |
236 | 'obResetFunc' => 'wfResetOutputBuffers', |
237 | 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], |
238 | 'tmpFileFactory' => $this->tmpFileFactory, |
239 | 'statusWrapper' => [ Status::class, 'wrap' ], |
240 | 'wanCache' => $this->wanCache, |
241 | 'srvCache' => $this->srvCache, |
242 | 'logger' => LoggerFactory::getInstance( 'FileOperation' ), |
243 | 'profiler' => static function ( $section ) { |
244 | return Profiler::instance()->scopedProfileIn( $section ); |
245 | } |
246 | ], |
247 | // Configured backend parameters |
248 | $config, |
249 | // Resolved backend parameters |
250 | [ |
251 | 'class' => $this->backends[$name]['class'], |
252 | 'lockManager' => |
253 | $this->lmgFactory->getLockManagerGroup( $config['domainId'] ) |
254 | ->get( $config['lockManager'] ), |
255 | ] |
256 | ); |
257 | } |
258 | |
259 | /** |
260 | * Get an appropriate backend object from a storage path |
261 | * |
262 | * @param string $storagePath |
263 | * @return FileBackend|null Backend or null on failure |
264 | */ |
265 | public function backendFromPath( $storagePath ) { |
266 | [ $backend, , ] = FileBackend::splitStoragePath( $storagePath ); |
267 | if ( $backend !== null && isset( $this->backends[$backend] ) ) { |
268 | return $this->get( $backend ); |
269 | } |
270 | |
271 | return null; |
272 | } |
273 | |
274 | /** |
275 | * @param string $storagePath |
276 | * @param string|null $content |
277 | * @param string|null $fsPath |
278 | * @return string |
279 | * @since 1.27 |
280 | */ |
281 | public function guessMimeInternal( $storagePath, $content, $fsPath ) { |
282 | // Trust the extension of the storage path (caller must validate) |
283 | $ext = FileBackend::extensionFromPath( $storagePath ); |
284 | $type = $this->mimeAnalyzer->getMimeTypeFromExtensionOrNull( $ext ); |
285 | // For files without a valid extension (or one at all), inspect the contents |
286 | if ( !$type && $fsPath ) { |
287 | $type = $this->mimeAnalyzer->guessMimeType( $fsPath, false ); |
288 | } elseif ( !$type && $content !== null && $content !== '' ) { |
289 | $tmpFile = $this->tmpFileFactory->newTempFSFile( 'mime_', '' ); |
290 | file_put_contents( $tmpFile->getPath(), $content ); |
291 | $type = $this->mimeAnalyzer->guessMimeType( $tmpFile->getPath(), false ); |
292 | } |
293 | return $type ?: 'unknown/unknown'; |
294 | } |
295 | } |
296 | /** @deprecated class alias since 1.43 */ |
297 | class_alias( FileBackendGroup::class, 'FileBackendGroup' ); |