3 * File backend registration handling.
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.
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.
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
21 * @ingroup FileBackend
24 namespace MediaWiki\FileBackend
;
26 use InvalidArgumentException
;
28 use MediaWiki\Config\ServiceOptions
;
29 use MediaWiki\Deferred\DeferredUpdates
;
30 use MediaWiki\FileBackend\FSFile\TempFSFileFactory
;
31 use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory
;
32 use MediaWiki\Logger\LoggerFactory
;
33 use MediaWiki\MainConfigNames
;
34 use MediaWiki\Output\StreamFile
;
35 use MediaWiki\Status\Status
;
37 use Wikimedia\FileBackend\FileBackend
;
38 use Wikimedia\FileBackend\FileBackendMultiWrite
;
39 use Wikimedia\FileBackend\FSFileBackend
;
40 use Wikimedia\Mime\MimeAnalyzer
;
41 use Wikimedia\ObjectCache\BagOStuff
;
42 use Wikimedia\ObjectCache\WANObjectCache
;
43 use Wikimedia\ObjectFactory\ObjectFactory
;
44 use Wikimedia\Rdbms\ReadOnlyMode
;
47 * Class to handle file backend registration
49 * @ingroup FileBackend
52 class FileBackendGroup
{
54 * @var array[] (name => ('class' => string, 'config' => array, 'instance' => object))
55 * @phan-var array<string,array{class:class-string,config:array,instance:object}>
57 protected $backends = [];
59 /** @var ServiceOptions */
65 /** @var WANObjectCache */
68 /** @var MimeAnalyzer */
69 private $mimeAnalyzer;
71 /** @var LockManagerGroupFactory */
74 /** @var TempFSFileFactory */
75 private $tmpFileFactory;
77 /** @var ObjectFactory */
78 private $objectFactory;
81 * @internal For use by ServiceWiring
83 public const CONSTRUCTOR_OPTIONS
= [
84 MainConfigNames
::DirectoryMode
,
85 MainConfigNames
::FileBackends
,
86 MainConfigNames
::ForeignFileRepos
,
87 MainConfigNames
::LocalFileRepo
,
92 * @param ServiceOptions $options
93 * @param ReadOnlyMode $readOnlyMode
94 * @param BagOStuff $srvCache
95 * @param WANObjectCache $wanCache
96 * @param MimeAnalyzer $mimeAnalyzer
97 * @param LockManagerGroupFactory $lmgFactory
98 * @param TempFSFileFactory $tmpFileFactory
99 * @param ObjectFactory $objectFactory
101 public function __construct(
102 ServiceOptions
$options,
103 ReadOnlyMode
$readOnlyMode,
105 WANObjectCache
$wanCache,
106 MimeAnalyzer
$mimeAnalyzer,
107 LockManagerGroupFactory
$lmgFactory,
108 TempFSFileFactory
$tmpFileFactory,
109 ObjectFactory
$objectFactory
111 $this->options
= $options;
112 $this->srvCache
= $srvCache;
113 $this->wanCache
= $wanCache;
114 $this->mimeAnalyzer
= $mimeAnalyzer;
115 $this->lmgFactory
= $lmgFactory;
116 $this->tmpFileFactory
= $tmpFileFactory;
117 $this->objectFactory
= $objectFactory;
119 // Register explicitly defined backends
120 $this->register( $options->get( MainConfigNames
::FileBackends
), $readOnlyMode->getConfiguredReason() );
123 // Automatically create b/c backends for file repos...
124 $repos = array_merge(
125 $options->get( MainConfigNames
::ForeignFileRepos
), [ $options->get( MainConfigNames
::LocalFileRepo
) ] );
126 foreach ( $repos as $info ) {
127 $backendName = $info['backend'];
128 if ( is_object( $backendName ) ||
isset( $this->backends
[$backendName] ) ) {
129 continue; // already defined (or set to the object for some reason)
131 $repoName = $info['name'];
132 // Local vars that used to be FSRepo members...
133 $directory = $info['directory'];
134 $deletedDir = $info['deletedDir'] ??
false; // deletion disabled
135 $thumbDir = $info['thumbDir'] ??
"{$directory}/thumb";
136 $transcodedDir = $info['transcodedDir'] ??
"{$directory}/transcoded";
137 $lockManager = $info['lockManager'] ??
'fsLockManager';
138 // Get the FS backend configuration
140 'name' => $backendName,
141 'class' => FSFileBackend
::class,
142 'lockManager' => $lockManager,
143 'containerPaths' => [
144 "{$repoName}-public" => "{$directory}",
145 "{$repoName}-thumb" => $thumbDir,
146 "{$repoName}-transcoded" => $transcodedDir,
147 "{$repoName}-deleted" => $deletedDir,
148 "{$repoName}-temp" => "{$directory}/temp"
150 'fileMode' => $info['fileMode'] ??
0644,
151 'directoryMode' => $options->get( MainConfigNames
::DirectoryMode
),
155 // Register implicitly defined backends
156 $this->register( $autoBackends, $readOnlyMode->getConfiguredReason() );
160 * Register an array of file backend configurations
162 * @param array[] $configs
163 * @param string|null $readOnlyReason
165 protected function register( array $configs, $readOnlyReason = null ) {
166 foreach ( $configs as $config ) {
167 if ( !isset( $config['name'] ) ) {
168 throw new InvalidArgumentException( "Cannot register a backend with no name." );
170 $name = $config['name'];
171 if ( isset( $this->backends
[$name] ) ) {
172 throw new LogicException( "Backend with name '$name' already registered." );
173 } elseif ( !isset( $config['class'] ) ) {
174 throw new InvalidArgumentException( "Backend with name '$name' has no class." );
176 $class = $config['class'];
178 $config['domainId'] ??
= $config['wikiId'] ??
$this->options
->get( 'fallbackWikiId' );
179 $config['readOnly'] ??
= $readOnlyReason;
181 unset( $config['class'] ); // backend won't need this
182 $this->backends
[$name] = [
191 * Get the backend object with a given name
193 * @param string $name
194 * @return FileBackend
196 public function get( $name ) {
197 // Lazy-load the actual backend instance
198 if ( !isset( $this->backends
[$name]['instance'] ) ) {
199 $config = $this->config( $name );
201 $class = $config['class'];
202 // Checking old alias for compatibility with unchanged config
203 if ( $class === FileBackendMultiWrite
::class ||
$class === \FileBackendMultiWrite
::class ) {
204 // @todo How can we test this? What's the intended use-case?
205 foreach ( $config['backends'] as $index => $beConfig ) {
206 if ( isset( $beConfig['template'] ) ) {
207 // Config is just a modified version of a registered backend's.
208 // This should only be used when that config is used only by this backend.
209 $config['backends'][$index] +
= $this->config( $beConfig['template'] );
214 $this->backends
[$name]['instance'] = new $class( $config );
217 return $this->backends
[$name]['instance'];
221 * Get the config array for a backend object with a given name
223 * @param string $name
224 * @return array Parameters to FileBackend::__construct()
226 public function config( $name ) {
227 if ( !isset( $this->backends
[$name] ) ) {
228 throw new InvalidArgumentException( "No backend defined with the name '$name'." );
231 $config = $this->backends
[$name]['config'];
234 // Default backend parameters
236 'mimeCallback' => [ $this, 'guessMimeInternal' ],
237 'obResetFunc' => 'wfResetOutputBuffers',
238 'asyncHandler' => [ DeferredUpdates
::class, 'addCallableUpdate' ],
239 'streamMimeFunc' => [ StreamFile
::class, 'contentTypeFromPath' ],
240 'tmpFileFactory' => $this->tmpFileFactory
,
241 'statusWrapper' => [ Status
::class, 'wrap' ],
242 'wanCache' => $this->wanCache
,
243 'srvCache' => $this->srvCache
,
244 'logger' => LoggerFactory
::getInstance( 'FileOperation' ),
245 'profiler' => static function ( $section ) {
246 return Profiler
::instance()->scopedProfileIn( $section );
249 // Configured backend parameters
251 // Resolved backend parameters
253 'class' => $this->backends
[$name]['class'],
255 $this->lmgFactory
->getLockManagerGroup( $config['domainId'] )
256 ->get( $config['lockManager'] ),
262 * Get an appropriate backend object from a storage path
264 * @param string $storagePath
265 * @return FileBackend|null Backend or null on failure
267 public function backendFromPath( $storagePath ) {
268 [ $backend, , ] = FileBackend
::splitStoragePath( $storagePath );
269 if ( $backend !== null && isset( $this->backends
[$backend] ) ) {
270 return $this->get( $backend );
277 * @param string $storagePath
278 * @param string|null $content
279 * @param string|null $fsPath
283 public function guessMimeInternal( $storagePath, $content, $fsPath ) {
284 // Trust the extension of the storage path (caller must validate)
285 $ext = FileBackend
::extensionFromPath( $storagePath );
286 $type = $this->mimeAnalyzer
->getMimeTypeFromExtensionOrNull( $ext );
287 // For files without a valid extension (or one at all), inspect the contents
288 if ( !$type && $fsPath ) {
289 $type = $this->mimeAnalyzer
->guessMimeType( $fsPath, false );
290 } elseif ( !$type && $content !== null && $content !== '' ) {
291 $tmpFile = $this->tmpFileFactory
->newTempFSFile( 'mime_', '' );
292 file_put_contents( $tmpFile->getPath(), $content );
293 $type = $this->mimeAnalyzer
->guessMimeType( $tmpFile->getPath(), false );
295 return $type ?
: 'unknown/unknown';
298 /** @deprecated class alias since 1.43 */
299 class_alias( FileBackendGroup
::class, 'FileBackendGroup' );