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 use MediaWiki\Logger\LoggerFactory
;
25 use MediaWiki\MediaWikiServices
;
28 * Class to handle file backend registration
30 * @ingroup FileBackend
33 class FileBackendGroup
{
34 /** @var FileBackendGroup */
35 protected static $instance = null;
37 /** @var array (name => ('class' => string, 'config' => array, 'instance' => object)) */
38 protected $backends = [];
40 protected function __construct() {
44 * @return FileBackendGroup
46 public static function singleton() {
47 if ( self
::$instance == null ) {
48 self
::$instance = new self();
49 self
::$instance->initFromGlobals();
52 return self
::$instance;
56 * Destroy the singleton instance
58 public static function destroySingleton() {
59 self
::$instance = null;
63 * Register file backends from the global variables
65 protected function initFromGlobals() {
66 global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends, $wgDirectoryMode;
68 // Register explicitly defined backends
69 $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
72 // Automatically create b/c backends for file repos...
73 $repos = array_merge( $wgForeignFileRepos, [ $wgLocalFileRepo ] );
74 foreach ( $repos as $info ) {
75 $backendName = $info['backend'];
76 if ( is_object( $backendName ) ||
isset( $this->backends
[$backendName] ) ) {
77 continue; // already defined (or set to the object for some reason)
79 $repoName = $info['name'];
80 // Local vars that used to be FSRepo members...
81 $directory = $info['directory'];
82 $deletedDir = $info['deletedDir'] ??
false; // deletion disabled
83 $thumbDir = $info['thumbDir'] ??
"{$directory}/thumb";
84 $transcodedDir = $info['transcodedDir'] ??
"{$directory}/transcoded";
85 // Get the FS backend configuration
87 'name' => $backendName,
88 'class' => FSFileBackend
::class,
89 'lockManager' => 'fsLockManager',
91 "{$repoName}-public" => "{$directory}",
92 "{$repoName}-thumb" => $thumbDir,
93 "{$repoName}-transcoded" => $transcodedDir,
94 "{$repoName}-deleted" => $deletedDir,
95 "{$repoName}-temp" => "{$directory}/temp"
97 'fileMode' => $info['fileMode'] ??
0644,
98 'directoryMode' => $wgDirectoryMode,
102 // Register implicitly defined backends
103 $this->register( $autoBackends, wfConfiguredReadOnlyReason() );
107 * Register an array of file backend configurations
109 * @param array[] $configs
110 * @param string|null $readOnlyReason
111 * @throws InvalidArgumentException
113 protected function register( array $configs, $readOnlyReason = null ) {
114 foreach ( $configs as $config ) {
115 if ( !isset( $config['name'] ) ) {
116 throw new InvalidArgumentException( "Cannot register a backend with no name." );
118 $name = $config['name'];
119 if ( isset( $this->backends
[$name] ) ) {
120 throw new LogicException( "Backend with name `{$name}` already registered." );
121 } elseif ( !isset( $config['class'] ) ) {
122 throw new InvalidArgumentException( "Backend with name `{$name}` has no class." );
124 $class = $config['class'];
126 $config['readOnly'] = $config['readOnly'] ??
$readOnlyReason;
128 unset( $config['class'] ); // backend won't need this
129 $this->backends
[$name] = [
138 * Get the backend object with a given name
140 * @param string $name
141 * @return FileBackend
142 * @throws InvalidArgumentException
144 public function get( $name ) {
145 // Lazy-load the actual backend instance
146 if ( !isset( $this->backends
[$name]['instance'] ) ) {
147 $config = $this->config( $name );
149 $class = $config['class'];
150 if ( $class === FileBackendMultiWrite
::class ) {
151 foreach ( $config['backends'] as $index => $beConfig ) {
152 if ( isset( $beConfig['template'] ) ) {
153 // Config is just a modified version of a registered backend's.
154 // This should only be used when that config is used only by this backend.
155 $config['backends'][$index] +
= $this->config( $beConfig['template'] );
160 $this->backends
[$name]['instance'] = new $class( $config );
163 return $this->backends
[$name]['instance'];
167 * Get the config array for a backend object with a given name
169 * @param string $name
170 * @return array Parameters to FileBackend::__construct()
171 * @throws InvalidArgumentException
173 public function config( $name ) {
174 if ( !isset( $this->backends
[$name] ) ) {
175 throw new InvalidArgumentException( "No backend defined with the name `$name`." );
177 $class = $this->backends
[$name]['class'];
179 $config = $this->backends
[$name]['config'];
180 $config['class'] = $class;
181 $config +
= [ // set defaults
182 // @FIXME: this does not include the domain for b/c but it ideally should
183 'wikiId' => wfWikiID(), // e.g. "my_wiki-en_"
184 'mimeCallback' => [ $this, 'guessMimeInternal' ],
185 'obResetFunc' => 'wfResetOutputBuffers',
186 'streamMimeFunc' => [ StreamFile
::class, 'contentTypeFromPath' ],
187 'tmpDirectory' => wfTempDir(),
188 'statusWrapper' => [ Status
::class, 'wrap' ],
189 'wanCache' => MediaWikiServices
::getInstance()->getMainWANObjectCache(),
190 'srvCache' => ObjectCache
::getLocalServerInstance( 'hash' ),
191 'logger' => LoggerFactory
::getInstance( 'FileOperation' ),
192 'profiler' => function ( $section ) {
193 return Profiler
::instance()->scopedProfileIn( $section );
196 $config['lockManager'] =
197 LockManagerGroup
::singleton( $config['wikiId'] )->get( $config['lockManager'] );
198 $config['fileJournal'] = isset( $config['fileJournal'] )
199 ? FileJournal
::factory( $config['fileJournal'], $name )
200 : FileJournal
::factory( [ 'class' => NullFileJournal
::class ], $name );
206 * Get an appropriate backend object from a storage path
208 * @param string $storagePath
209 * @return FileBackend|null Backend or null on failure
211 public function backendFromPath( $storagePath ) {
212 list( $backend, , ) = FileBackend
::splitStoragePath( $storagePath );
213 if ( $backend !== null && isset( $this->backends
[$backend] ) ) {
214 return $this->get( $backend );
221 * @param string $storagePath
222 * @param string|null $content
223 * @param string|null $fsPath
227 public function guessMimeInternal( $storagePath, $content, $fsPath ) {
228 $magic = MediaWiki\MediaWikiServices
::getInstance()->getMimeAnalyzer();
229 // Trust the extension of the storage path (caller must validate)
230 $ext = FileBackend
::extensionFromPath( $storagePath );
231 $type = $magic->guessTypesForExtension( $ext );
232 // For files without a valid extension (or one at all), inspect the contents
233 if ( !$type && $fsPath ) {
234 $type = $magic->guessMimeType( $fsPath, false );
235 } elseif ( !$type && strlen( $content ) ) {
236 $tmpFile = TempFSFile
::factory( 'mime_', '', wfTempDir() );
237 file_put_contents( $tmpFile->getPath(), $content );
238 $type = $magic->guessMimeType( $tmpFile->getPath(), false );
240 return $type ?
: 'unknown/unknown';