diff --git a/src/bundle/Core/Resources/config/default_settings.yml b/src/bundle/Core/Resources/config/default_settings.yml index cf1718dbe7..6cafe7051e 100644 --- a/src/bundle/Core/Resources/config/default_settings.yml +++ b/src/bundle/Core/Resources/config/default_settings.yml @@ -256,8 +256,11 @@ parameters: ## ibexa.orm.entity_mappings: [] + # fallback for existing project configuration, should be overridden + dfs_nfs_path: '%ibexa.io.dir.storage%' + ibexa.io.nfs.adapter.config: - root: './' + root: '%dfs_nfs_path%' path: '$var_dir$/$storage_dir$/' writeFlags: ~ linkHandling: ~ diff --git a/src/bundle/IO/Flysystem/Adapter/SiteAccessAwareLocalAdapter.php b/src/bundle/IO/Flysystem/Adapter/SiteAccessAwareLocalAdapter.php deleted file mode 100644 index 11a44c4bf0..0000000000 --- a/src/bundle/IO/Flysystem/Adapter/SiteAccessAwareLocalAdapter.php +++ /dev/null @@ -1,51 +0,0 @@ -configProcessor = $configProcessor; - - parent::__construct( - $config['root'], - $config['writeFlags'], - $config['linkHandling'], - $config['permissions'] - ); - - $this->path = $config['path']; - } - - public function getPathPrefix(): string - { - $contextPath = $this->configProcessor->processSettingValue($this->path); - - // path prefix is guaranteed to have path separator suffix, see parent::setPathPrefix - return sprintf('%s%s', $this->pathPrefix, $contextPath); - } -} - -class_alias(SiteAccessAwareLocalAdapter::class, 'eZ\Bundle\EzPublishIOBundle\Flysystem\Adapter\SiteAccessAwareLocalAdapter'); diff --git a/src/bundle/IO/Resources/config/io.yml b/src/bundle/IO/Resources/config/io.yml index 9ae5e6ad5c..3a2d242262 100644 --- a/src/bundle/IO/Resources/config/io.yml +++ b/src/bundle/IO/Resources/config/io.yml @@ -97,20 +97,6 @@ services: class: Ibexa\Bundle\IO\ApiLoader\HandlerRegistry # Inject the siteaccess config into a few io services - Ibexa\Core\IO\Adapter\LocalAdapter: - autoconfigure: true - arguments: - - '@Ibexa\Core\IO\IOConfigProvider' - - '@ibexa.config.resolver' - - Ibexa\Bundle\IO\Flysystem\Adapter\SiteAccessAwareLocalAdapter: - arguments: - $configProcessor: '@Ibexa\Contracts\Core\SiteAccess\ConfigProcessor' - $config: '%ibexa.io.nfs.adapter.config%' - - ibexa.io.nfs.adapter.site_access_aware: - alias: Ibexa\Bundle\IO\Flysystem\Adapter\SiteAccessAwareLocalAdapter - Ibexa\Core\IO\UrlDecorator\AbsolutePrefix: class: Ibexa\Core\IO\UrlDecorator\AbsolutePrefix arguments: diff --git a/src/lib/IO/Adapter/LocalAdapter.php b/src/lib/IO/Adapter/LocalAdapter.php deleted file mode 100644 index 03c17358d7..0000000000 --- a/src/lib/IO/Adapter/LocalAdapter.php +++ /dev/null @@ -1,71 +0,0 @@ -ioConfigProvider = $ioConfigProvider; - $this->configResolver = $configResolver; - - $filesPermissions = $this->configResolver->getParameter('io.permissions.files'); - $directoriesPermissions = $this->configResolver->getParameter('io.permissions.directories'); - - parent::__construct( - $this->ioConfigProvider->getRootDir(), - LOCK_EX, - Local::DISALLOW_LINKS, - ['file' => ['public' => $filesPermissions], 'dir' => ['public' => $directoriesPermissions]] - ); - } - - /** - * Reconfigure Adapter due to SiteAccess change which implies - * root dir and permissions could be different for new SiteAccess. - */ - public function onConfigScopeChange(ScopeChangeEvent $event): void - { - $root = $this->ioConfigProvider->getRootDir(); - $root = is_link($root) ? realpath($root) : $root; - $this->ensureDirectory($root); - - if (!is_dir($root) || !is_readable($root)) { - throw new LogicException(sprintf('The root path %s is not readable.', $root)); - } - - $this->setPathPrefix($root); - - $filesPermissions = $this->configResolver->getParameter('io.permissions.files'); - $directoriesPermissions = $this->configResolver->getParameter('io.permissions.directories'); - - $this->permissionMap = array_replace_recursive( - static::$permissions, - ['file' => ['public' => $filesPermissions], 'dir' => ['public' => $directoriesPermissions]] - ); - } -} - -class_alias(LocalAdapter::class, 'eZ\Publish\Core\IO\Adapter\LocalAdapter'); diff --git a/src/lib/IO/Flysystem/Adapter/LocalSiteAccessAwareFilesystemAdapter.php b/src/lib/IO/Flysystem/Adapter/LocalSiteAccessAwareFilesystemAdapter.php new file mode 100644 index 0000000000..48e1666b22 --- /dev/null +++ b/src/lib/IO/Flysystem/Adapter/LocalSiteAccessAwareFilesystemAdapter.php @@ -0,0 +1,396 @@ +prefixer = $prefixer; + $this->visibility = $visibilityConverter; + $this->writeFlags = $writeFlags; + $this->linkHandling = $linkHandling; + $this->ensureDirectoryExists($location, $this->visibility->defaultForDirectories()); + $this->mimeTypeDetector = $mimeTypeDetector; + } + + public function fileExists(string $path): bool + { + $location = $this->prefixer->prefixPath($path); + + return is_file($location); + } + + public function write(string $path, string $contents, Config $config): void + { + $this->writeToFile($path, $contents, $config); + } + + public function writeStream(string $path, $contents, Config $config): void + { + $this->writeToFile($path, $contents, $config); + } + + public function lastModified(string $path): FileAttributes + { + $location = $this->prefixer->prefixPath($path); + error_clear_last(); + $lastModified = @filemtime($location); + + if ($lastModified === false) { + throw UnableToRetrieveMetadata::lastModified($path, error_get_last()['message'] ?? ''); + } + + return new FileAttributes($path, null, null, $lastModified); + } + + public function fileSize(string $path): FileAttributes + { + $location = $this->prefixer->prefixPath($path); + error_clear_last(); + + if (is_file($location) && ($fileSize = @filesize($location)) !== false) { + return new FileAttributes($path, $fileSize); + } + + throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? ''); + } + + public function listContents(string $path, bool $deep): iterable + { + $location = $this->prefixer->prefixPath($path); + + if (!is_dir($location)) { + return; + } + + /** @var \SplFileInfo[] $iterator */ + $iterator = $deep ? $this->listDirectoryRecursively($location) : $this->listDirectory( + $location + ); + + foreach ($iterator as $fileInfo) { + if ($fileInfo->isLink()) { + if ($this->linkHandling & LocalFilesystemAdapter::SKIP_LINKS) { + continue; + } + + throw SymbolicLinkEncountered::atLocation($fileInfo->getPathname()); + } + + $path = $this->prefixer->stripPrefix($fileInfo->getPathname()); + $lastModified = $fileInfo->getMTime(); + $isDirectory = $fileInfo->isDir(); + $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4)); + $visibility = $isDirectory + ? $this->visibility->inverseForDirectory($permissions) + : $this->visibility->inverseForFile($permissions); + + yield $isDirectory ? new DirectoryAttributes( + $path, + $visibility, + $lastModified + ) : new FileAttributes( + str_replace('\\', '/', $path), + $fileInfo->getSize(), + $visibility, + $lastModified + ); + } + } + + public function move(string $source, string $destination, Config $config): void + { + $sourcePath = $this->prefixer->prefixPath($source); + $destinationPath = $this->prefixer->prefixPath($destination); + $this->ensureDirectoryExists( + dirname($destinationPath), + $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) + ); + + if (!@rename($sourcePath, $destinationPath)) { + throw UnableToMoveFile::fromLocationTo($sourcePath, $destinationPath); + } + } + + public function copy(string $source, string $destination, Config $config): void + { + $sourcePath = $this->prefixer->prefixPath($source); + $destinationPath = $this->prefixer->prefixPath($destination); + $this->ensureDirectoryExists( + dirname($destinationPath), + $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) + ); + + if (!@copy($sourcePath, $destinationPath)) { + throw UnableToCopyFile::fromLocationTo($sourcePath, $destinationPath); + } + } + + public function delete(string $path): void + { + $location = $this->prefixer->prefixPath($path); + + if (!file_exists($location)) { + return; + } + + error_clear_last(); + + if (!@unlink($location)) { + throw UnableToDeleteFile::atLocation($location, error_get_last()['message'] ?? ''); + } + } + + public function setVisibility(string $path, string $visibility): void + { + $path = $this->prefixer->prefixPath($path); + $visibilityMask = is_dir($path) + ? $this->visibility->forDirectory($visibility) + : $this->visibility->forFile($visibility); + + $this->setPermissions($path, $visibilityMask); + } + + public function mimeType(string $path): FileAttributes + { + $location = $this->prefixer->prefixPath($path); + error_clear_last(); + $mimeType = $this->mimeTypeDetector->detectMimeTypeFromFile($location); + + if ($mimeType === null) { + throw UnableToRetrieveMetadata::mimeType($path, error_get_last()['message'] ?? ''); + } + + return new FileAttributes($path, null, null, null, $mimeType); + } + + public function read(string $path): string + { + $location = $this->prefixer->prefixPath($path); + error_clear_last(); + $contents = @file_get_contents($location); + + if ($contents === false) { + throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); + } + + return $contents; + } + + public function readStream(string $path) + { + $location = $this->prefixer->prefixPath($path); + error_clear_last(); + $contents = @fopen($location, 'rb'); + + if ($contents === false) { + throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); + } + + return $contents; + } + + public function deleteDirectory(string $path): void + { + $location = $this->prefixer->prefixPath($path); + + if (!is_dir($location)) { + return; + } + + $contents = $this->listDirectoryRecursively($location, RecursiveIteratorIterator::CHILD_FIRST); + + /** @var \SplFileInfo $file */ + foreach ($contents as $file) { + if (!$this->deleteFileInfoObject($file)) { + throw UnableToDeleteDirectory::atLocation($path, 'Unable to delete file at ' . $file->getPathname()); + } + } + + unset($contents); + if (!@rmdir($location)) { + throw UnableToDeleteDirectory::atLocation($path, error_get_last()['message'] ?? ''); + } + } + + public function createDirectory(string $path, Config $config): void + { + $location = $this->prefixer->prefixPath($path); + $visibility = $config->get(Config::OPTION_VISIBILITY, $config->get(Config::OPTION_DIRECTORY_VISIBILITY)); + $permissions = $this->resolveDirectoryVisibility($visibility); + + if (is_dir($location)) { + $this->setPermissions($location, $permissions); + + return; + } + + error_clear_last(); + + if (!mkdir($location, $permissions, true) && !is_dir($location)) { + throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? ''); + } + } + + public function visibility(string $path): FileAttributes + { + $location = $this->prefixer->prefixPath($path); + clearstatcache(false, $location); + error_clear_last(); + $fileperms = @fileperms($location); + + if ($fileperms === false) { + throw UnableToRetrieveMetadata::visibility($path, error_get_last()['message'] ?? ''); + } + + $permissions = $fileperms & 0777; + $visibility = $this->visibility->inverseForFile($permissions); + + return new FileAttributes($path, null, $visibility); + } + + private function deleteFileInfoObject(SplFileInfo $file): bool + { + switch ($file->getType()) { + case 'dir': + return @rmdir((string) $file->getRealPath()); + case 'link': + return @unlink($file->getPathname()); + default: + return @unlink((string) $file->getRealPath()); + } + } + + /** + * @throws \League\Flysystem\FilesystemException + */ + private function writeToFile(string $path, $contents, Config $config): void + { + $prefixedLocation = $this->prefixer->prefixPath($path); + $this->ensureDirectoryExists( + dirname($prefixedLocation), + $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) + ); + error_clear_last(); + + if (@file_put_contents($prefixedLocation, $contents, $this->writeFlags) === false) { + throw UnableToWriteFile::atLocation($path, error_get_last()['message'] ?? ''); + } + + if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { + $this->setVisibility($path, (string)$visibility); + } + } + + private function resolveDirectoryVisibility(?string $visibility): int + { + return $visibility === null + ? $this->visibility->defaultForDirectories() + : $this->visibility->forDirectory($visibility); + } + + private function listDirectoryRecursively(string $path): Generator + { + yield from new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + } + + private function listDirectory(string $location): Generator + { + $iterator = new DirectoryIterator($location); + + foreach ($iterator as $item) { + if ($item->isDot()) { + continue; + } + + yield $item; + } + } + + private function setPermissions(string $location, int $visibility): void + { + error_clear_last(); + if (!@chmod($location, $visibility)) { + $extraMessage = error_get_last()['message'] ?? ''; + throw UnableToSetVisibility::atLocation( + $this->prefixer->stripPrefix($location), + $extraMessage + ); + } + } + + private function ensureDirectoryExists(string $dirname, int $visibility): void + { + if (is_dir($dirname)) { + return; + } + + error_clear_last(); + if (!mkdir($dirname, $visibility, true) && !is_dir($dirname)) { + $mkdirError = error_get_last(); + } + + clearstatcache(true, $dirname); + if (!is_dir($dirname)) { + $errorMessage = $mkdirError['message'] ?? ''; + + throw UnableToCreateDirectory::atLocation($dirname, $errorMessage); + } + } +} diff --git a/src/lib/IO/Flysystem/PathPrefixer/BaseSiteAccessAwarePathPrefixer.php b/src/lib/IO/Flysystem/PathPrefixer/BaseSiteAccessAwarePathPrefixer.php new file mode 100644 index 0000000000..9c1ff11f35 --- /dev/null +++ b/src/lib/IO/Flysystem/PathPrefixer/BaseSiteAccessAwarePathPrefixer.php @@ -0,0 +1,57 @@ +separator = $separator; + } + + abstract protected function getSiteAccessAwarePathPrefix(): string; + + public function prefixPath(string $path): string + { + $siteAccessAwarePathPrefix = $this->getSiteAccessAwarePathPrefix(); + $prefix = rtrim($siteAccessAwarePathPrefix, '\\/'); + if ($prefix !== '' || $siteAccessAwarePathPrefix === $this->separator) { + $prefix .= $this->separator; + } + + return $prefix . ltrim($path, '\\/'); + } + + public function stripPrefix(string $path): string + { + /* @var string */ + return substr($path, strlen($this->getSiteAccessAwarePathPrefix())); + } + + public function stripDirectoryPrefix(string $path): string + { + return rtrim($this->stripPrefix($path), '\\/'); + } + + public function prefixDirectoryPath(string $path): string + { + $prefixedPath = $this->prefixPath(rtrim($path, '\\/')); + + if ($prefixedPath === '' || (substr($prefixedPath, -1) === $this->separator)) { + return $prefixedPath; + } + + return $prefixedPath . $this->separator; + } +} diff --git a/src/lib/IO/Flysystem/PathPrefixer/DFSSiteAccessAwarePathPrefixer.php b/src/lib/IO/Flysystem/PathPrefixer/DFSSiteAccessAwarePathPrefixer.php new file mode 100644 index 0000000000..863173e67a --- /dev/null +++ b/src/lib/IO/Flysystem/PathPrefixer/DFSSiteAccessAwarePathPrefixer.php @@ -0,0 +1,47 @@ +rootDir = $rootDir; + $this->path = $path; + $this->configProcessor = $configProcessor; + } + + protected function getSiteAccessAwarePathPrefix(): string + { + return sprintf( + '%s%s%s', + rtrim($this->rootDir, $this->separator), + $this->separator, + $this->configProcessor->processSettingValue($this->path) + ); + } +} diff --git a/src/lib/IO/Flysystem/PathPrefixer/LocalSiteAccessAwarePathPrefixer.php b/src/lib/IO/Flysystem/PathPrefixer/LocalSiteAccessAwarePathPrefixer.php new file mode 100644 index 0000000000..e024a0bf49 --- /dev/null +++ b/src/lib/IO/Flysystem/PathPrefixer/LocalSiteAccessAwarePathPrefixer.php @@ -0,0 +1,32 @@ +ioConfigProvider = $ioConfigProvider; + } + + protected function getSiteAccessAwarePathPrefix(): string + { + return $this->ioConfigProvider->getRootDir(); + } +} diff --git a/src/lib/IO/Flysystem/PathPrefixer/PathPrefixerInterface.php b/src/lib/IO/Flysystem/PathPrefixer/PathPrefixerInterface.php new file mode 100644 index 0000000000..bb3e2ee778 --- /dev/null +++ b/src/lib/IO/Flysystem/PathPrefixer/PathPrefixerInterface.php @@ -0,0 +1,23 @@ +nativeVisibilityConverter = $nativeVisibilityConverter; + } + + abstract protected function getPublicFilePermissions(): int; + + abstract protected function getPublicDirectoryPermissions(): int; + + public function forFile(string $visibility): int + { + PortableVisibilityGuard::guardAgainstInvalidInput($visibility); + + return $visibility === Visibility::PUBLIC + ? $this->getPublicFilePermissions() + : $this->nativeVisibilityConverter->forFile($visibility); + } + + public function forDirectory(string $visibility): int + { + PortableVisibilityGuard::guardAgainstInvalidInput($visibility); + + return $visibility === Visibility::PUBLIC + ? $this->getPublicDirectoryPermissions() + : $this->nativeVisibilityConverter->forDirectory($visibility); + } + + public function inverseForFile(int $visibility): string + { + if ($visibility === $this->getPublicFilePermissions()) { + return Visibility::PUBLIC; + } + + if ($visibility === $this->nativeVisibilityConverter->forFile(Visibility::PRIVATE)) { + return Visibility::PRIVATE; + } + + return Visibility::PUBLIC; // default + } + + public function inverseForDirectory(int $visibility): string + { + if ($visibility === $this->getPublicDirectoryPermissions()) { + return Visibility::PUBLIC; + } + + if ($visibility === $this->nativeVisibilityConverter->forDirectory(Visibility::PRIVATE)) { + return Visibility::PRIVATE; + } + + return Visibility::PUBLIC; // default + } + + public function defaultForDirectories(): int + { + return $this->nativeVisibilityConverter->defaultForDirectories(); + } +} diff --git a/src/lib/IO/Flysystem/VisibilityConverter/DFSVisibilityConverter.php b/src/lib/IO/Flysystem/VisibilityConverter/DFSVisibilityConverter.php new file mode 100644 index 0000000000..522733d70a --- /dev/null +++ b/src/lib/IO/Flysystem/VisibilityConverter/DFSVisibilityConverter.php @@ -0,0 +1,54 @@ +permissions = $permissions; + } + + protected function getPublicFilePermissions(): int + { + return $this->permissions['files'] ?? $this->nativeVisibilityConverter->forFile( + Visibility::PUBLIC + ); + } + + protected function getPublicDirectoryPermissions(): int + { + return $this->permissions['directories'] ?? $this->nativeVisibilityConverter->forDirectory( + Visibility::PUBLIC + ); + } +} diff --git a/src/lib/IO/Flysystem/VisibilityConverter/SiteAccessAwareVisibilityConverter.php b/src/lib/IO/Flysystem/VisibilityConverter/SiteAccessAwareVisibilityConverter.php new file mode 100644 index 0000000000..7c94201627 --- /dev/null +++ b/src/lib/IO/Flysystem/VisibilityConverter/SiteAccessAwareVisibilityConverter.php @@ -0,0 +1,51 @@ +configResolver = $configResolver; + } + + protected function getPublicFilePermissions(): int + { + return $this->configResolver->getParameter( + self::SITE_CONFIG_IO_FILE_PERMISSIONS_PARAM_NAME + ); + } + + protected function getPublicDirectoryPermissions(): int + { + return $this->configResolver->getParameter(self::SITE_CONFIG_IO_DIR_PERMISSIONS_PARAM_NAME); + } +} diff --git a/src/lib/Resources/settings/io.yml b/src/lib/Resources/settings/io.yml index 2cf410ec02..75bc701c59 100644 --- a/src/lib/Resources/settings/io.yml +++ b/src/lib/Resources/settings/io.yml @@ -41,12 +41,59 @@ services: ibexa.core.io.flysystem.default_filesystem: parent: ibexa.core.io.flysystem.base_filesystem arguments: - - '@Ibexa\Core\IO\Adapter\LocalAdapter' + $adapter: '@Ibexa\Core\IO\Flysystem\Adapter\LocalSiteAccessAwareFilesystemAdapter' - Ibexa\Core\IO\Adapter\LocalAdapter: - class: League\Flysystem\Adapter\Local + Ibexa\Core\IO\Flysystem\Adapter\LocalSiteAccessAwareFilesystemAdapter: arguments: - - '%ibexa.io.dir.root%' + $location: '%ibexa.io.dir.storage%' + $visibilityConverter: '@Ibexa\Core\IO\Flysystem\VisibilityConverter\SiteAccessAwareVisibilityConverter' + $prefixer: '@Ibexa\Core\IO\Flysystem\PathPrefixer\LocalSiteAccessAwarePathPrefixer' + $mimeTypeDetector: '@ibexa.core.io.flysystem.mime_type_detector' + + ibexa.io.nfs.adapter.site_access_aware: + class: Ibexa\Core\IO\Flysystem\Adapter\LocalSiteAccessAwareFilesystemAdapter + arguments: + $location: "@=parameter('ibexa.io.nfs.adapter.config')['root']" # used this expression due to BC for parameter name + $visibilityConverter: '@Ibexa\Core\IO\Flysystem\VisibilityConverter\SiteAccessAwareVisibilityConverter' + $prefixer: '@Ibexa\Core\IO\Flysystem\PathPrefixer\DFSSiteAccessAwarePathPrefixer' + $mimeTypeDetector: '@ibexa.core.io.flysystem.mime_type_detector' + + ibexa.core.io.flysystem.mime_type_detector: + alias: ibexa.core.io.flysystem.mime_type_detector.f_info + + ibexa.core.io.flysystem.mime_type_detector.f_info: + class: League\MimeTypeDetection\FinfoMimeTypeDetector + + ibexa.core.io.flysystem.visibility.portable_visibility.converter: + class: League\Flysystem\UnixVisibility\PortableVisibilityConverter + + Ibexa\Core\IO\Flysystem\VisibilityConverter\BaseVisibilityConverter: + abstract: true + arguments: + $nativeVisibilityConverter: '@ibexa.core.io.flysystem.visibility.portable_visibility.converter' + + Ibexa\Core\IO\Flysystem\VisibilityConverter\SiteAccessAwareVisibilityConverter: + parent: Ibexa\Core\IO\Flysystem\VisibilityConverter\BaseVisibilityConverter + arguments: + $configResolver: '@Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface' + + Ibexa\Core\IO\Flysystem\VisibilityConverter\DFSVisibilityConverter: + parent: Ibexa\Core\IO\Flysystem\VisibilityConverter\BaseVisibilityConverter + arguments: + $permissions: "@=parameter('ibexa.io.nfs.adapter.config')['permissions']" + + Ibexa\Core\IO\Flysystem\PathPrefixer\BaseSiteAccessAwarePathPrefixer: + abstract: true + + Ibexa\Core\IO\Flysystem\PathPrefixer\LocalSiteAccessAwarePathPrefixer: + arguments: + $ioConfigProvider: '@Ibexa\Core\IO\IOConfigProvider' + + Ibexa\Core\IO\Flysystem\PathPrefixer\DFSSiteAccessAwarePathPrefixer: + arguments: + $configProcessor: '@Ibexa\Contracts\Core\SiteAccess\ConfigProcessor' + $rootDir: "@=parameter('ibexa.io.nfs.adapter.config')['root']" + $path: "@=parameter('ibexa.io.nfs.adapter.config')['path']" ibexa.core.io.default_url_decorator: alias: Ibexa\Core\IO\UrlDecorator\AbsolutePrefix