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