MediaWiki master
FileBackendGroup.php
Go to the documentation of this file.
1<?php
31use Wikimedia\ObjectFactory\ObjectFactory;
33
45 protected $backends = [];
46
48 private $options;
49
51 private $srvCache;
52
54 private $wanCache;
55
57 private $mimeAnalyzer;
58
60 private $lmgFactory;
61
63 private $tmpFileFactory;
64
66 private $objectFactory;
67
71 public const CONSTRUCTOR_OPTIONS = [
72 MainConfigNames::DirectoryMode,
73 MainConfigNames::FileBackends,
74 MainConfigNames::ForeignFileRepos,
75 MainConfigNames::LocalFileRepo,
76 'fallbackWikiId',
77 ];
78
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
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
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
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
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
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}
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
Class to handle file backend registration.
__construct(ServiceOptions $options, ReadOnlyMode $readOnlyMode, BagOStuff $srvCache, WANObjectCache $wanCache, MimeAnalyzer $mimeAnalyzer, LockManagerGroupFactory $lmgFactory, TempFSFileFactory $tmpFileFactory, ObjectFactory $objectFactory)
config( $name)
Get the config array for a backend object with a given name.
guessMimeInternal( $storagePath, $content, $fsPath)
backendFromPath( $storagePath)
Get an appropriate backend object from a storage path.
array[] $backends
(name => ('class' => string, 'config' => array, 'instance' => object))
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
A class for passing options to services.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Functions related to the output of file content.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
static instance()
Definition Profiler.php:105
Multi-datacenter aware caching interface.
Determine whether a site is currently in read-only mode.