('class' => string, 'config' => array, 'instance' => object)) * @phan-var array */ protected $backends = []; /** @var ServiceOptions */ private $options; /** @var BagOStuff */ private $srvCache; /** @var WANObjectCache */ private $wanCache; /** @var MimeAnalyzer */ private $mimeAnalyzer; /** @var LockManagerGroupFactory */ private $lmgFactory; /** @var TempFSFileFactory */ private $tmpFileFactory; /** @var ObjectFactory */ private $objectFactory; /** * @internal */ public const CONSTRUCTOR_OPTIONS = [ 'DirectoryMode', 'FileBackends', 'ForeignFileRepos', 'LocalFileRepo', 'fallbackWikiId', ]; /** * @deprecated since 1.35, inject the service instead * @return FileBackendGroup */ public static function singleton() : FileBackendGroup { return MediaWikiServices::getInstance()->getFileBackendGroup(); } /** * Destroy the singleton instance * * @deprecated since 1.35, test framework should reset services between tests instead */ public static function destroySingleton() { MediaWikiServices::getInstance()->resetServiceForTesting( 'FileBackendGroup' ); } /** * @param ServiceOptions $options * @param ConfiguredReadOnlyMode $configuredReadOnlyMode * @param BagOStuff $srvCache * @param WANObjectCache $wanCache * @param MimeAnalyzer $mimeAnalyzer * @param LockManagerGroupFactory $lmgFactory * @param TempFSFileFactory $tmpFileFactory * @param ObjectFactory $objectFactory */ public function __construct( ServiceOptions $options, ConfiguredReadOnlyMode $configuredReadOnlyMode, BagOStuff $srvCache, WANObjectCache $wanCache, MimeAnalyzer $mimeAnalyzer, LockManagerGroupFactory $lmgFactory, TempFSFileFactory $tmpFileFactory, ObjectFactory $objectFactory ) { $this->options = $options; $this->srvCache = $srvCache; $this->wanCache = $wanCache; $this->mimeAnalyzer = $mimeAnalyzer; $this->lmgFactory = $lmgFactory; $this->tmpFileFactory = $tmpFileFactory; $this->objectFactory = $objectFactory; // Register explicitly defined backends $this->register( $options->get( 'FileBackends' ), $configuredReadOnlyMode->getReason() ); $autoBackends = []; // Automatically create b/c backends for file repos... $repos = array_merge( $options->get( 'ForeignFileRepos' ), [ $options->get( 'LocalFileRepo' ) ] ); foreach ( $repos as $info ) { $backendName = $info['backend']; if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) { continue; // already defined (or set to the object for some reason) } $repoName = $info['name']; // Local vars that used to be FSRepo members... $directory = $info['directory']; $deletedDir = $info['deletedDir'] ?? false; // deletion disabled $thumbDir = $info['thumbDir'] ?? "{$directory}/thumb"; $transcodedDir = $info['transcodedDir'] ?? "{$directory}/transcoded"; // Get the FS backend configuration $autoBackends[] = [ 'name' => $backendName, 'class' => FSFileBackend::class, 'lockManager' => 'fsLockManager', 'containerPaths' => [ "{$repoName}-public" => "{$directory}", "{$repoName}-thumb" => $thumbDir, "{$repoName}-transcoded" => $transcodedDir, "{$repoName}-deleted" => $deletedDir, "{$repoName}-temp" => "{$directory}/temp" ], 'fileMode' => $info['fileMode'] ?? 0644, 'directoryMode' => $options->get( 'DirectoryMode' ), ]; } // Register implicitly defined backends $this->register( $autoBackends, $configuredReadOnlyMode->getReason() ); } /** * Register an array of file backend configurations * * @param array[] $configs * @param string|null $readOnlyReason * @throws InvalidArgumentException */ protected function register( array $configs, $readOnlyReason = null ) { foreach ( $configs as $config ) { if ( !isset( $config['name'] ) ) { throw new InvalidArgumentException( "Cannot register a backend with no name." ); } $name = $config['name']; if ( isset( $this->backends[$name] ) ) { throw new LogicException( "Backend with name '$name' already registered." ); } elseif ( !isset( $config['class'] ) ) { throw new InvalidArgumentException( "Backend with name '$name' has no class." ); } $class = $config['class']; $config['domainId'] = $config['domainId'] ?? $config['wikiId'] ?? $this->options->get( 'fallbackWikiId' ); $config['readOnly'] = $config['readOnly'] ?? $readOnlyReason; unset( $config['class'] ); // backend won't need this $this->backends[$name] = [ 'class' => $class, 'config' => $config, 'instance' => null ]; } } /** * Get the backend object with a given name * * @param string $name * @return FileBackend * @throws InvalidArgumentException */ public function get( $name ) { // Lazy-load the actual backend instance if ( !isset( $this->backends[$name]['instance'] ) ) { $config = $this->config( $name ); $class = $config['class']; if ( $class === FileBackendMultiWrite::class ) { // @todo How can we test this? What's the intended use-case? foreach ( $config['backends'] as $index => $beConfig ) { if ( isset( $beConfig['template'] ) ) { // Config is just a modified version of a registered backend's. // This should only be used when that config is used only by this backend. $config['backends'][$index] += $this->config( $beConfig['template'] ); } } } $this->backends[$name]['instance'] = new $class( $config ); } return $this->backends[$name]['instance']; } /** * Get the config array for a backend object with a given name * * @param string $name * @return array Parameters to FileBackend::__construct() * @throws InvalidArgumentException */ public function config( $name ) { if ( !isset( $this->backends[$name] ) ) { throw new InvalidArgumentException( "No backend defined with the name '$name'." ); } $config = $this->backends[$name]['config']; return array_merge( // Default backend parameters [ 'mimeCallback' => [ $this, 'guessMimeInternal' ], 'obResetFunc' => 'wfResetOutputBuffers', 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], 'tmpFileFactory' => $this->tmpFileFactory, 'statusWrapper' => [ Status::class, 'wrap' ], 'wanCache' => $this->wanCache, 'srvCache' => $this->srvCache, 'logger' => LoggerFactory::getInstance( 'FileOperation' ), 'profiler' => function ( $section ) { return Profiler::instance()->scopedProfileIn( $section ); } ], // Configured backend parameters $config, // Resolved backend parameters [ 'class' => $this->backends[$name]['class'], 'lockManager' => $this->lmgFactory->getLockManagerGroup( $config['domainId'] ) ->get( $config['lockManager'] ), 'fileJournal' => isset( $config['fileJournal'] ) ? $this->objectFactory->createObject( $config['fileJournal'] + [ 'backend' => $name ], [ 'specIsArg' => true, 'assertClass' => FileJournal::class ] ) : new NullFileJournal ] ); } /** * Get an appropriate backend object from a storage path * * @param string $storagePath * @return FileBackend|null Backend or null on failure */ public function backendFromPath( $storagePath ) { list( $backend, , ) = FileBackend::splitStoragePath( $storagePath ); if ( $backend !== null && isset( $this->backends[$backend] ) ) { return $this->get( $backend ); } return null; } /** * @param string $storagePath * @param string|null $content * @param string|null $fsPath * @return string * @since 1.27 */ public function guessMimeInternal( $storagePath, $content, $fsPath ) { // Trust the extension of the storage path (caller must validate) $ext = FileBackend::extensionFromPath( $storagePath ); $type = $this->mimeAnalyzer->getMimeTypeFromExtensionOrNull( $ext ); // For files without a valid extension (or one at all), inspect the contents if ( !$type && $fsPath ) { $type = $this->mimeAnalyzer->guessMimeType( $fsPath, false ); } elseif ( !$type && strlen( $content ) ) { $tmpFile = $this->tmpFileFactory->newTempFSFile( 'mime_', '' ); file_put_contents( $tmpFile->getPath(), $content ); $type = $this->mimeAnalyzer->guessMimeType( $tmpFile->getPath(), false ); } return $type ?: 'unknown/unknown'; } }