From d14a03222044078f639cef8f59a526f8c7dffda1 Mon Sep 17 00:00:00 2001 From: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:28:27 +0200 Subject: [PATCH] feat: implement support for authoritative mount providers Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> --- build/psalm-baseline.xml | 6 - build/stubs/php-polyfill.php | 3 + .../Files/Config/MountProviderCollection.php | 70 +++++-- lib/private/Files/SetupManager.php | 175 +++++++++++++++--- lib/private/Files/SetupManagerFactory.php | 3 + 5 files changed, 214 insertions(+), 43 deletions(-) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 75beecd1639..712dc3bc3bb 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -3641,12 +3641,6 @@ - - - - - - diff --git a/build/stubs/php-polyfill.php b/build/stubs/php-polyfill.php index 606a21f8dfe..2f85987b06c 100644 --- a/build/stubs/php-polyfill.php +++ b/build/stubs/php-polyfill.php @@ -7,3 +7,6 @@ // PHP 8.4 function array_find(array $array, callable $callback) {} +// PHP 8.5 +function array_any(array $array, callable $callback): bool {} + diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index aa445a0879e..24d2bf54a09 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -9,16 +9,21 @@ namespace OC\Files\Config; use OC\Hooks\Emitter; use OC\Hooks\EmitterTrait; +use OCA\Files_Sharing\MountProvider; use OCP\Diagnostics\IEventLogger; use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IMountProvider; +use OCP\Files\Config\IMountProviderArgs; use OCP\Files\Config\IMountProviderCollection; +use OCP\Files\Config\IPartialMountProvider; use OCP\Files\Config\IRootMountProvider; use OCP\Files\Config\IUserMountCache; use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorageFactory; use OCP\IUser; +use function get_class; +use function in_array; class MountProviderCollection implements IMountProviderCollection, Emitter { use EmitterTrait; @@ -29,7 +34,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { private array $homeProviders = []; /** - * @var list + * @var array, IMountProvider> */ private array $providers = []; @@ -67,7 +72,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { $mounts = array_map(function (IMountProvider $provider) use ($user, $loader) { return $this->getMountsFromProvider($provider, $user, $loader); }, $providers); - $mounts = array_merge(...array_values($mounts)); + $mounts = array_merge(...$mounts); return $this->filterMounts($user, $mounts); } @@ -75,21 +80,53 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { * @return list */ public function getMountsForUser(IUser $user): array { - return $this->getUserMountsForProviders($user, $this->providers); + return $this->getUserMountsForProviders($user, array_values($this->providers)); + } + + /** + * @param IMountProviderArgs[] $mountProviderArgs + * @return array IMountPoint array indexed by mount + * point. + */ + public function getUserMountsFromProviderByPath( + string $providerClass, + string $path, + array $mountProviderArgs, + ): array { + $provider = $this->providers[$providerClass] ?? null; + if ($provider === null) { + return []; + } + + if (!is_a($providerClass, IPartialMountProvider::class, true)) { + throw new \LogicException( + 'Mount provider does not support partial mounts' + ); + } + + /** @var IPartialMountProvider $provider */ + return $provider->getMountsForPath( + $path, + $mountProviderArgs, + $this->loader, + ); } /** * Returns the mounts for the user from the specified provider classes. * Providers not registered in the MountProviderCollection will be skipped. * + * @inheritdoc + * * @return list */ public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array { $providers = array_filter( $this->providers, - fn (IMountProvider $mountProvider) => (in_array(get_class($mountProvider), $mountProviderClasses)) + fn (string $providerClass) => in_array($providerClass, $mountProviderClasses), + ARRAY_FILTER_USE_KEY ); - return $this->getUserMountsForProviders($user, $providers); + return $this->getUserMountsForProviders($user, array_values($providers)); } /** @@ -100,16 +137,21 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { // to check for name collisions $firstMounts = []; if ($providerFilter) { - $providers = array_filter($this->providers, $providerFilter); + $providers = array_filter($this->providers, $providerFilter, ARRAY_FILTER_USE_KEY); } else { $providers = $this->providers; } - $firstProviders = array_filter($providers, function (IMountProvider $provider) { - return (get_class($provider) !== 'OCA\Files_Sharing\MountProvider'); - }); - $lastProviders = array_filter($providers, function (IMountProvider $provider) { - return (get_class($provider) === 'OCA\Files_Sharing\MountProvider'); - }); + $firstProviders + = array_filter( + $providers, + fn (string $providerClass) => ($providerClass !== MountProvider::class), + ARRAY_FILTER_USE_KEY + ); + $lastProviders = array_filter( + $providers, + fn (string $providerClass) => $providerClass === MountProvider::class, + ARRAY_FILTER_USE_KEY + ); foreach ($firstProviders as $provider) { $mounts = $this->getMountsFromProvider($provider, $user, $this->loader); $firstMounts = array_merge($firstMounts, $mounts); @@ -151,7 +193,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { * Add a provider for mount points */ public function registerProvider(IMountProvider $provider): void { - $this->providers[] = $provider; + $this->providers[get_class($provider)] = $provider; $this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]); } @@ -229,7 +271,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { * @return list */ public function getProviders(): array { - return $this->providers; + return array_values($this->providers); } /** diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 1911316ad36..3c0cdfe2123 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -8,6 +8,7 @@ declare(strict_types=1); namespace OC\Files; +use OC\Files\Cache\FileAccess; use OC\Files\Config\MountProviderCollection; use OC\Files\Mount\HomeMountPoint; use OC\Files\Mount\MountPoint; @@ -33,6 +34,8 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IMountProvider; +use OCP\Files\Config\IMountProviderArgs; +use OCP\Files\Config\IPartialMountProvider; use OCP\Files\Config\IRootMountProvider; use OCP\Files\Config\IUserMountCache; use OCP\Files\Events\BeforeFileSystemSetupEvent; @@ -53,6 +56,10 @@ use OCP\IUserSession; use OCP\Lockdown\ILockdownManager; use OCP\Share\Events\ShareCreatedEvent; use Psr\Log\LoggerInterface; +use function array_key_exists; +use function count; +use function dirname; +use function in_array; class SetupManager { private bool $rootSetup = false; @@ -66,11 +73,19 @@ class SetupManager { * @var array[]> */ private array $setupUserMountProviders = []; + /** + * An array of paths that have already been set up + * + * @var array + */ + private array $setupMountProviderPaths = []; private ICache $cache; private bool $listeningForProviders; private array $fullSetupRequired = []; private bool $setupBuiltinWrappersDone = false; private bool $forceFullSetup = false; + private const SETUP_WITH_CHILDREN = 1; + private const SETUP_WITHOUT_CHILDREN = 0; public function __construct( private IEventLogger $eventLogger, @@ -86,6 +101,7 @@ class SetupManager { private IConfig $config, private ShareDisableChecker $shareDisableChecker, private IAppManager $appManager, + private FileAccess $fileAccess, ) { $this->cache = $cacheFactory->createDistributed('setupmanager::'); $this->listeningForProviders = false; @@ -102,6 +118,27 @@ class SetupManager { return in_array($user->getUID(), $this->setupUsersComplete, true); } + /** + * Checks if a path has been cached either directly or through a full setup + * of one of its parents. + */ + private function isPathSetup(string $path): bool { + // if the exact path was already setup with or without children + if (array_key_exists($path, $this->setupMountProviderPaths)) { + return true; + } + + // or if any of the ancestors was fully setup + while (($path = dirname($path)) !== '/') { + $setupPath = $this->setupMountProviderPaths[$path] ?? null; + if ($setupPath === self::SETUP_WITH_CHILDREN) { + return true; + } + } + + return false; + } + private function setupBuiltinWrappers() { if ($this->setupBuiltinWrappersDone) { return; @@ -204,9 +241,9 @@ class SetupManager { $this->setupForUserWith($user, function () use ($user) { $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function ( - IMountProvider $provider, + string $providerClass, ) use ($user) { - return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]); + return !in_array($providerClass, $this->setupUserMountProviders[$user->getUID()]); }); }); $this->afterUserFullySetup($user, $previouslySetupProviders); @@ -379,7 +416,8 @@ class SetupManager { } /** - * Set up the filesystem for the specified path + * Set up the filesystem for the specified path, optionally including all + * children mounts. */ public function setupForPath(string $path, bool $includeChildren = false): void { $user = $this->getUserForPath($path); @@ -421,51 +459,141 @@ class SetupManager { $this->eventLogger->start('fs:setup:user:path', "Setup $path filesystem for user"); $this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path"); - $mounts = []; - if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { - $currentProviders[] = $cachedMount->getMountProvider(); - if ($cachedMount->getMountProvider()) { - $setupProviders[] = $cachedMount->getMountProvider(); - $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]); - } else { + $fullProviderMounts = []; + $authoritativeMounts = []; + + $mountProvider = $cachedMount->getMountProvider(); + $mountPoint = $cachedMount->getMountPoint(); + $isMountProviderSetup = in_array($mountProvider, $setupProviders); + $isPathSetupAsAuthoritative + = $this->isPathSetup($mountPoint); + if (!$isMountProviderSetup && !$isPathSetupAsAuthoritative) { + if ($mountProvider === '') { $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup'); $this->eventLogger->end('fs:setup:user:path:find'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); return; } + + if (is_a($mountProvider, IPartialMountProvider::class, true)) { + $rootId = $cachedMount->getRootId(); + $rootMetadata = $this->fileAccess->getByFileId($rootId); + $providerArgs = new IMountProviderArgs($cachedMount, $rootMetadata); + // mark the path as cached (without children for now...) + $cacheKey = rtrim($mountPoint, '/'); + $this->setupMountProviderPaths[$cacheKey] = self::SETUP_WITHOUT_CHILDREN; + $authoritativeMounts[] = array_values( + $this->mountProviderCollection->getUserMountsFromProviderByPath( + $mountProvider, + $path, + [$providerArgs] + ) + ); + } else { + $currentProviders[] = $mountProvider; + $setupProviders[] = $mountProvider; + $fullProviderMounts[] + = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$mountProvider]); + } } if ($includeChildren) { $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path); $this->eventLogger->end('fs:setup:user:path:find'); - $needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) { - return $needsFullSetup || $cachedMountInfo->getMountProvider() === ''; - }, false); + $needsFullSetup + = array_any( + $subCachedMounts, + fn (ICachedMountInfo $info) => $info->getMountProvider() === '' + ); if ($needsFullSetup) { $this->logger->debug('mount has no provider set, performing full setup'); $this->setupForUser($user); $this->eventLogger->end('fs:setup:user:path'); return; - } else { - foreach ($subCachedMounts as $cachedMount) { - if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { - $currentProviders[] = $cachedMount->getMountProvider(); - $setupProviders[] = $cachedMount->getMountProvider(); - $mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()])); + } + + /** @var array, ICachedMountInfo[]> $authoritativeCachedMounts */ + $authoritativeCachedMounts = []; + foreach ($subCachedMounts as $cachedMount) { + /** @var class-string $mountProvider */ + $mountProvider = $cachedMount->getMountProvider(); + + // skip setup for already set up providers + if (in_array($mountProvider, $setupProviders)) { + continue; + } + + if (is_a($mountProvider, IPartialMountProvider::class, true)) { + // skip setup if path was set up as authoritative before + if ($this->isPathSetup($cachedMount->getMountPoint())) { + continue; } + // collect cached mount points for authoritative providers + $authoritativeCachedMounts[$mountProvider] ??= []; + $authoritativeCachedMounts[$mountProvider][] = $cachedMount; + continue; + } + + $currentProviders[] = $mountProvider; + $setupProviders[] = $mountProvider; + $fullProviderMounts[] + = $this->mountProviderCollection->getUserMountsForProviderClasses( + $user, + [$mountProvider] + ); + } + + if (!empty($authoritativeCachedMounts)) { + $rootIds = array_map( + fn (ICachedMountInfo $mount) => $mount->getRootId(), + array_merge(...array_values($authoritativeCachedMounts)), + ); + + $rootsMetadata = []; + foreach (array_chunk($rootIds, 1000) as $chunk) { + foreach ($this->fileAccess->getByFileIds($chunk) as $id => $fileMetadata) { + $rootsMetadata[$id] = $fileMetadata; + } + } + $cacheKey = rtrim($mountPoint, '/'); + $this->setupMountProviderPaths[$cacheKey] = self::SETUP_WITH_CHILDREN; + foreach ($authoritativeCachedMounts as $providerClass => $cachedMounts) { + $providerArgs = array_filter(array_map( + static function (ICachedMountInfo $info) use ($rootsMetadata) { + $rootMetadata = $rootsMetadata[$info->getRootId()] ?? null; + + return $rootMetadata + ? new IMountProviderArgs($info, $rootMetadata) + : null; + }, + $cachedMounts + )); + $authoritativeMounts[] + = $this->mountProviderCollection->getUserMountsFromProviderByPath( + $providerClass, + $path, + $providerArgs, + ); } } } else { $this->eventLogger->end('fs:setup:user:path:find'); } - if (count($mounts)) { - $this->registerMounts($user, $mounts, $currentProviders); - $this->setupForUserWith($user, function () use ($mounts) { - array_walk($mounts, [$this->mountManager, 'addMount']); + $fullProviderMounts = array_merge(...$fullProviderMounts); + $authoritativeMounts = array_merge(...$authoritativeMounts); + + if (count($fullProviderMounts) || count($authoritativeMounts)) { + if (count($fullProviderMounts)) { + $this->registerMounts($user, $fullProviderMounts, $currentProviders); + } + + $this->setupForUserWith($user, function () use ($fullProviderMounts, $authoritativeMounts) { + $allMounts = [...$fullProviderMounts, ...$authoritativeMounts]; + array_walk($allMounts, $this->mountManager->addMount(...)); }); } elseif (!$this->isSetupStarted($user)) { $this->oneTimeUserSetup($user); @@ -545,6 +673,7 @@ class SetupManager { $this->setupUsers = []; $this->setupUsersComplete = []; $this->setupUserMountProviders = []; + $this->setupMountProviderPaths = []; $this->fullSetupRequired = []; $this->rootSetup = false; $this->mountManager->clear(); diff --git a/lib/private/Files/SetupManagerFactory.php b/lib/private/Files/SetupManagerFactory.php index d2fe978fa9e..369e3089017 100644 --- a/lib/private/Files/SetupManagerFactory.php +++ b/lib/private/Files/SetupManagerFactory.php @@ -8,6 +8,7 @@ declare(strict_types=1); namespace OC\Files; +use OC\Files\Cache\FileAccess; use OC\Share20\ShareDisableChecker; use OCP\App\IAppManager; use OCP\Diagnostics\IEventLogger; @@ -38,6 +39,7 @@ class SetupManagerFactory { private IConfig $config, private ShareDisableChecker $shareDisableChecker, private IAppManager $appManager, + private FileAccess $fileAccess, ) { $this->setupManager = null; } @@ -58,6 +60,7 @@ class SetupManagerFactory { $this->config, $this->shareDisableChecker, $this->appManager, + $this->fileAccess, ); } return $this->setupManager;