feat: implement support for authoritative mount providers

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
pull/55072/head
Salvatore Martire 2025-09-05 13:28:27 +07:00
parent fcdb28e4a3
commit d14a032220
5 changed files with 214 additions and 43 deletions

@ -3641,12 +3641,6 @@
<InvalidOperand> <InvalidOperand>
<code><![CDATA[$user]]></code> <code><![CDATA[$user]]></code>
</InvalidOperand> </InvalidOperand>
<RedundantCondition>
<code><![CDATA[get_class($provider) !== 'OCA\Files_Sharing\MountProvider']]></code>
</RedundantCondition>
<TypeDoesNotContainType>
<code><![CDATA[get_class($provider) === 'OCA\Files_Sharing\MountProvider']]></code>
</TypeDoesNotContainType>
</file> </file>
<file src="lib/private/Files/Config/UserMountCache.php"> <file src="lib/private/Files/Config/UserMountCache.php">
<InvalidReturnType> <InvalidReturnType>

@ -7,3 +7,6 @@
// PHP 8.4 // PHP 8.4
function array_find(array $array, callable $callback) {} function array_find(array $array, callable $callback) {}
// PHP 8.5
function array_any(array $array, callable $callback): bool {}

@ -9,16 +9,21 @@ namespace OC\Files\Config;
use OC\Hooks\Emitter; use OC\Hooks\Emitter;
use OC\Hooks\EmitterTrait; use OC\Hooks\EmitterTrait;
use OCA\Files_Sharing\MountProvider;
use OCP\Diagnostics\IEventLogger; use OCP\Diagnostics\IEventLogger;
use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IMountProvider; use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IMountProviderArgs;
use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IPartialMountProvider;
use OCP\Files\Config\IRootMountProvider; use OCP\Files\Config\IRootMountProvider;
use OCP\Files\Config\IUserMountCache; use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint; use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage\IStorageFactory; use OCP\Files\Storage\IStorageFactory;
use OCP\IUser; use OCP\IUser;
use function get_class;
use function in_array;
class MountProviderCollection implements IMountProviderCollection, Emitter { class MountProviderCollection implements IMountProviderCollection, Emitter {
use EmitterTrait; use EmitterTrait;
@ -29,7 +34,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
private array $homeProviders = []; private array $homeProviders = [];
/** /**
* @var list<IMountProvider> * @var array<class-string<IMountProvider>, IMountProvider>
*/ */
private array $providers = []; private array $providers = [];
@ -67,7 +72,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
$mounts = array_map(function (IMountProvider $provider) use ($user, $loader) { $mounts = array_map(function (IMountProvider $provider) use ($user, $loader) {
return $this->getMountsFromProvider($provider, $user, $loader); return $this->getMountsFromProvider($provider, $user, $loader);
}, $providers); }, $providers);
$mounts = array_merge(...array_values($mounts)); $mounts = array_merge(...$mounts);
return $this->filterMounts($user, $mounts); return $this->filterMounts($user, $mounts);
} }
@ -75,21 +80,53 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
* @return list<IMountPoint> * @return list<IMountPoint>
*/ */
public function getMountsForUser(IUser $user): array { 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<string, IMountPoint> 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. * Returns the mounts for the user from the specified provider classes.
* Providers not registered in the MountProviderCollection will be skipped. * Providers not registered in the MountProviderCollection will be skipped.
* *
* @inheritdoc
*
* @return list<IMountPoint> * @return list<IMountPoint>
*/ */
public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array { public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array {
$providers = array_filter( $providers = array_filter(
$this->providers, $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 // to check for name collisions
$firstMounts = []; $firstMounts = [];
if ($providerFilter) { if ($providerFilter) {
$providers = array_filter($this->providers, $providerFilter); $providers = array_filter($this->providers, $providerFilter, ARRAY_FILTER_USE_KEY);
} else { } else {
$providers = $this->providers; $providers = $this->providers;
} }
$firstProviders = array_filter($providers, function (IMountProvider $provider) { $firstProviders
return (get_class($provider) !== 'OCA\Files_Sharing\MountProvider'); = array_filter(
}); $providers,
$lastProviders = array_filter($providers, function (IMountProvider $provider) { fn (string $providerClass) => ($providerClass !== MountProvider::class),
return (get_class($provider) === 'OCA\Files_Sharing\MountProvider'); ARRAY_FILTER_USE_KEY
}); );
$lastProviders = array_filter(
$providers,
fn (string $providerClass) => $providerClass === MountProvider::class,
ARRAY_FILTER_USE_KEY
);
foreach ($firstProviders as $provider) { foreach ($firstProviders as $provider) {
$mounts = $this->getMountsFromProvider($provider, $user, $this->loader); $mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
$firstMounts = array_merge($firstMounts, $mounts); $firstMounts = array_merge($firstMounts, $mounts);
@ -151,7 +193,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
* Add a provider for mount points * Add a provider for mount points
*/ */
public function registerProvider(IMountProvider $provider): void { public function registerProvider(IMountProvider $provider): void {
$this->providers[] = $provider; $this->providers[get_class($provider)] = $provider;
$this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]); $this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]);
} }
@ -229,7 +271,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
* @return list<IMountProvider> * @return list<IMountProvider>
*/ */
public function getProviders(): array { public function getProviders(): array {
return $this->providers; return array_values($this->providers);
} }
/** /**

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OC\Files; namespace OC\Files;
use OC\Files\Cache\FileAccess;
use OC\Files\Config\MountProviderCollection; use OC\Files\Config\MountProviderCollection;
use OC\Files\Mount\HomeMountPoint; use OC\Files\Mount\HomeMountPoint;
use OC\Files\Mount\MountPoint; use OC\Files\Mount\MountPoint;
@ -33,6 +34,8 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IMountProvider; use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IMountProviderArgs;
use OCP\Files\Config\IPartialMountProvider;
use OCP\Files\Config\IRootMountProvider; use OCP\Files\Config\IRootMountProvider;
use OCP\Files\Config\IUserMountCache; use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\BeforeFileSystemSetupEvent; use OCP\Files\Events\BeforeFileSystemSetupEvent;
@ -53,6 +56,10 @@ use OCP\IUserSession;
use OCP\Lockdown\ILockdownManager; use OCP\Lockdown\ILockdownManager;
use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\Events\ShareCreatedEvent;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use function array_key_exists;
use function count;
use function dirname;
use function in_array;
class SetupManager { class SetupManager {
private bool $rootSetup = false; private bool $rootSetup = false;
@ -66,11 +73,19 @@ class SetupManager {
* @var array<string, class-string<IMountProvider>[]> * @var array<string, class-string<IMountProvider>[]>
*/ */
private array $setupUserMountProviders = []; private array $setupUserMountProviders = [];
/**
* An array of paths that have already been set up
*
* @var array<string, int>
*/
private array $setupMountProviderPaths = [];
private ICache $cache; private ICache $cache;
private bool $listeningForProviders; private bool $listeningForProviders;
private array $fullSetupRequired = []; private array $fullSetupRequired = [];
private bool $setupBuiltinWrappersDone = false; private bool $setupBuiltinWrappersDone = false;
private bool $forceFullSetup = false; private bool $forceFullSetup = false;
private const SETUP_WITH_CHILDREN = 1;
private const SETUP_WITHOUT_CHILDREN = 0;
public function __construct( public function __construct(
private IEventLogger $eventLogger, private IEventLogger $eventLogger,
@ -86,6 +101,7 @@ class SetupManager {
private IConfig $config, private IConfig $config,
private ShareDisableChecker $shareDisableChecker, private ShareDisableChecker $shareDisableChecker,
private IAppManager $appManager, private IAppManager $appManager,
private FileAccess $fileAccess,
) { ) {
$this->cache = $cacheFactory->createDistributed('setupmanager::'); $this->cache = $cacheFactory->createDistributed('setupmanager::');
$this->listeningForProviders = false; $this->listeningForProviders = false;
@ -102,6 +118,27 @@ class SetupManager {
return in_array($user->getUID(), $this->setupUsersComplete, true); 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() { private function setupBuiltinWrappers() {
if ($this->setupBuiltinWrappersDone) { if ($this->setupBuiltinWrappersDone) {
return; return;
@ -204,9 +241,9 @@ class SetupManager {
$this->setupForUserWith($user, function () use ($user) { $this->setupForUserWith($user, function () use ($user) {
$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function ( $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
IMountProvider $provider, string $providerClass,
) use ($user) { ) use ($user) {
return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]); return !in_array($providerClass, $this->setupUserMountProviders[$user->getUID()]);
}); });
}); });
$this->afterUserFullySetup($user, $previouslySetupProviders); $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 { public function setupForPath(string $path, bool $includeChildren = false): void {
$user = $this->getUserForPath($path); $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', "Setup $path filesystem for user");
$this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path"); $this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path");
$mounts = []; $fullProviderMounts = [];
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { $authoritativeMounts = [];
$currentProviders[] = $cachedMount->getMountProvider();
if ($cachedMount->getMountProvider()) { $mountProvider = $cachedMount->getMountProvider();
$setupProviders[] = $cachedMount->getMountProvider(); $mountPoint = $cachedMount->getMountPoint();
$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]); $isMountProviderSetup = in_array($mountProvider, $setupProviders);
} else { $isPathSetupAsAuthoritative
= $this->isPathSetup($mountPoint);
if (!$isMountProviderSetup && !$isPathSetupAsAuthoritative) {
if ($mountProvider === '') {
$this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup'); $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup');
$this->eventLogger->end('fs:setup:user:path:find'); $this->eventLogger->end('fs:setup:user:path:find');
$this->setupForUser($user); $this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path'); $this->eventLogger->end('fs:setup:user:path');
return; 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) { if ($includeChildren) {
$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path); $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
$this->eventLogger->end('fs:setup:user:path:find'); $this->eventLogger->end('fs:setup:user:path:find');
$needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) { $needsFullSetup
return $needsFullSetup || $cachedMountInfo->getMountProvider() === ''; = array_any(
}, false); $subCachedMounts,
fn (ICachedMountInfo $info) => $info->getMountProvider() === ''
);
if ($needsFullSetup) { if ($needsFullSetup) {
$this->logger->debug('mount has no provider set, performing full setup'); $this->logger->debug('mount has no provider set, performing full setup');
$this->setupForUser($user); $this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path'); $this->eventLogger->end('fs:setup:user:path');
return; return;
} else { }
foreach ($subCachedMounts as $cachedMount) {
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { /** @var array<class-string<IMountProvider>, ICachedMountInfo[]> $authoritativeCachedMounts */
$currentProviders[] = $cachedMount->getMountProvider(); $authoritativeCachedMounts = [];
$setupProviders[] = $cachedMount->getMountProvider(); foreach ($subCachedMounts as $cachedMount) {
$mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()])); /** @var class-string<IMountProvider> $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 { } else {
$this->eventLogger->end('fs:setup:user:path:find'); $this->eventLogger->end('fs:setup:user:path:find');
} }
if (count($mounts)) { $fullProviderMounts = array_merge(...$fullProviderMounts);
$this->registerMounts($user, $mounts, $currentProviders); $authoritativeMounts = array_merge(...$authoritativeMounts);
$this->setupForUserWith($user, function () use ($mounts) {
array_walk($mounts, [$this->mountManager, 'addMount']); 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)) { } elseif (!$this->isSetupStarted($user)) {
$this->oneTimeUserSetup($user); $this->oneTimeUserSetup($user);
@ -545,6 +673,7 @@ class SetupManager {
$this->setupUsers = []; $this->setupUsers = [];
$this->setupUsersComplete = []; $this->setupUsersComplete = [];
$this->setupUserMountProviders = []; $this->setupUserMountProviders = [];
$this->setupMountProviderPaths = [];
$this->fullSetupRequired = []; $this->fullSetupRequired = [];
$this->rootSetup = false; $this->rootSetup = false;
$this->mountManager->clear(); $this->mountManager->clear();

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OC\Files; namespace OC\Files;
use OC\Files\Cache\FileAccess;
use OC\Share20\ShareDisableChecker; use OC\Share20\ShareDisableChecker;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\Diagnostics\IEventLogger; use OCP\Diagnostics\IEventLogger;
@ -38,6 +39,7 @@ class SetupManagerFactory {
private IConfig $config, private IConfig $config,
private ShareDisableChecker $shareDisableChecker, private ShareDisableChecker $shareDisableChecker,
private IAppManager $appManager, private IAppManager $appManager,
private FileAccess $fileAccess,
) { ) {
$this->setupManager = null; $this->setupManager = null;
} }
@ -58,6 +60,7 @@ class SetupManagerFactory {
$this->config, $this->config,
$this->shareDisableChecker, $this->shareDisableChecker,
$this->appManager, $this->appManager,
$this->fileAccess,
); );
} }
return $this->setupManager; return $this->setupManager;