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;