diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index 61165ee67fbdc..8526237f829b5 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -37,6 +37,9 @@ 'OCA\\Files_External\\Controller\\StoragesController' => $baseDir . '/../lib/Controller/StoragesController.php', 'OCA\\Files_External\\Controller\\UserGlobalStoragesController' => $baseDir . '/../lib/Controller/UserGlobalStoragesController.php', 'OCA\\Files_External\\Controller\\UserStoragesController' => $baseDir . '/../lib/Controller/UserStoragesController.php', + 'OCA\\Files_External\\Event\\StorageCreatedEvent' => $baseDir . '/../lib/Event/StorageCreatedEvent.php', + 'OCA\\Files_External\\Event\\StorageDeletedEvent' => $baseDir . '/../lib/Event/StorageDeletedEvent.php', + 'OCA\\Files_External\\Event\\StorageUpdatedEvent' => $baseDir . '/../lib/Event/StorageUpdatedEvent.php', 'OCA\\Files_External\\Lib\\Auth\\AmazonS3\\AccessKey' => $baseDir . '/../lib/Lib/Auth/AmazonS3/AccessKey.php', 'OCA\\Files_External\\Lib\\Auth\\AuthMechanism' => $baseDir . '/../lib/Lib/Auth/AuthMechanism.php', 'OCA\\Files_External\\Lib\\Auth\\Builtin' => $baseDir . '/../lib/Lib/Auth/Builtin.php', @@ -117,6 +120,7 @@ 'OCA\\Files_External\\Service\\GlobalStoragesService' => $baseDir . '/../lib/Service/GlobalStoragesService.php', 'OCA\\Files_External\\Service\\ImportLegacyStoragesService' => $baseDir . '/../lib/Service/ImportLegacyStoragesService.php', 'OCA\\Files_External\\Service\\LegacyStoragesService' => $baseDir . '/../lib/Service/LegacyStoragesService.php', + 'OCA\\Files_External\\Service\\MountCacheService' => $baseDir . '/../lib/Service/MountCacheService.php', 'OCA\\Files_External\\Service\\StoragesService' => $baseDir . '/../lib/Service/StoragesService.php', 'OCA\\Files_External\\Service\\UserGlobalStoragesService' => $baseDir . '/../lib/Service/UserGlobalStoragesService.php', 'OCA\\Files_External\\Service\\UserStoragesService' => $baseDir . '/../lib/Service/UserStoragesService.php', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index 11001c58c9cbc..b5b992356404b 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -52,6 +52,9 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Controller\\StoragesController' => __DIR__ . '/..' . '/../lib/Controller/StoragesController.php', 'OCA\\Files_External\\Controller\\UserGlobalStoragesController' => __DIR__ . '/..' . '/../lib/Controller/UserGlobalStoragesController.php', 'OCA\\Files_External\\Controller\\UserStoragesController' => __DIR__ . '/..' . '/../lib/Controller/UserStoragesController.php', + 'OCA\\Files_External\\Event\\StorageCreatedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageCreatedEvent.php', + 'OCA\\Files_External\\Event\\StorageDeletedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageDeletedEvent.php', + 'OCA\\Files_External\\Event\\StorageUpdatedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageUpdatedEvent.php', 'OCA\\Files_External\\Lib\\Auth\\AmazonS3\\AccessKey' => __DIR__ . '/..' . '/../lib/Lib/Auth/AmazonS3/AccessKey.php', 'OCA\\Files_External\\Lib\\Auth\\AuthMechanism' => __DIR__ . '/..' . '/../lib/Lib/Auth/AuthMechanism.php', 'OCA\\Files_External\\Lib\\Auth\\Builtin' => __DIR__ . '/..' . '/../lib/Lib/Auth/Builtin.php', @@ -132,6 +135,7 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Service\\GlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/GlobalStoragesService.php', 'OCA\\Files_External\\Service\\ImportLegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/ImportLegacyStoragesService.php', 'OCA\\Files_External\\Service\\LegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/LegacyStoragesService.php', + 'OCA\\Files_External\\Service\\MountCacheService' => __DIR__ . '/..' . '/../lib/Service/MountCacheService.php', 'OCA\\Files_External\\Service\\StoragesService' => __DIR__ . '/..' . '/../lib/Service/StoragesService.php', 'OCA\\Files_External\\Service\\UserGlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserGlobalStoragesService.php', 'OCA\\Files_External\\Service\\UserStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserStoragesService.php', diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 1ad1a2ed779c7..924b72bf9822d 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -11,6 +11,9 @@ use OCA\Files_External\Config\ConfigAdapter; use OCA\Files_External\Config\UserPlaceholderHandler; use OCA\Files_External\ConfigLexicon; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; +use OCA\Files_External\Event\StorageUpdatedEvent; use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; use OCA\Files_External\Lib\Auth\Builtin; use OCA\Files_External\Lib\Auth\NullMechanism; @@ -42,19 +45,22 @@ use OCA\Files_External\Lib\Config\IBackendProvider; use OCA\Files_External\Listener\GroupDeletedListener; use OCA\Files_External\Listener\LoadAdditionalListener; -use OCA\Files_External\Listener\StorePasswordListener; use OCA\Files_External\Listener\UserDeletedListener; use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\MountCacheService; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\QueryException; use OCP\Files\Config\IMountProviderCollection; +use OCP\Group\Events\BeforeGroupDeletedEvent; use OCP\Group\Events\GroupDeletedEvent; -use OCP\User\Events\PasswordUpdatedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\UserCreatedEvent; use OCP\User\Events\UserDeletedEvent; -use OCP\User\Events\UserLoggedInEvent; /** * @package OCA\Files_External\AppInfo @@ -75,8 +81,15 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); - $context->registerEventListener(UserLoggedInEvent::class, StorePasswordListener::class); - $context->registerEventListener(PasswordUpdatedEvent::class, StorePasswordListener::class); + $context->registerEventListener(StorageCreatedEvent::class, MountCacheService::class); + $context->registerEventListener(StorageDeletedEvent::class, MountCacheService::class); + $context->registerEventListener(StorageUpdatedEvent::class, MountCacheService::class); + $context->registerEventListener(BeforeGroupDeletedEvent::class, MountCacheService::class); + $context->registerEventListener(UserCreatedEvent::class, MountCacheService::class); + $context->registerEventListener(UserAddedEvent::class, MountCacheService::class); + $context->registerEventListener(UserRemovedEvent::class, MountCacheService::class); + $context->registerEventListener(PostLoginEvent::class, MountCacheService::class); + $context->registerConfigLexicon(ConfigLexicon::class); } diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php index a46c0fd5c6616..042c364bc6718 100644 --- a/apps/files_external/lib/Config/ConfigAdapter.php +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -17,6 +17,7 @@ use OCA\Files_External\Service\UserGlobalStoragesService; use OCA\Files_External\Service\UserStoragesService; use OCP\AppFramework\QueryException; +use OCP\Files\Config\IAuthoritativeMountProvider; use OCP\Files\Config\IMountProvider; use OCP\Files\Mount\IMountPoint; use OCP\Files\ObjectStore\IObjectStore; @@ -32,7 +33,7 @@ /** * Make the old files_external config work with the new public mount config api */ -class ConfigAdapter implements IMountProvider { +class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider { public function __construct( private UserStoragesService $userStoragesService, private UserGlobalStoragesService $userGlobalStoragesService, @@ -73,6 +74,11 @@ private function prepareStorageConfig(StorageConfig &$storage, IUser $user): voi $storage->getBackend()->manipulateStorageConfig($storage, $user); } + public function constructStorageForUser(IUser $user, StorageConfig $storage) { + $this->prepareStorageConfig($storage, $user); + return $this->constructStorage($storage); + } + /** * Construct the storage implementation * @@ -105,8 +111,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $storages = array_map(function (StorageConfig $storageConfig) use ($user) { try { - $this->prepareStorageConfig($storageConfig, $user); - return $this->constructStorage($storageConfig); + return $this->constructStorageForUser($user, $storageConfig); } catch (\Exception $e) { // propagate exception into filesystem return new FailedStorage(['exception' => $e]); @@ -123,7 +128,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $availability = $storage->getAvailability(); if (!$availability['available'] && !Availability::shouldRecheck($availability)) { $storage = new FailedStorage([ - 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available') + 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available'), ]); } } catch (\Exception $e) { @@ -148,7 +153,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { null, $loader, $storageConfig->getMountOptions(), - $storageConfig->getId() + $storageConfig->getId(), ); } else { return new SystemMountPoint( @@ -158,7 +163,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { null, $loader, $storageConfig->getMountOptions(), - $storageConfig->getId() + $storageConfig->getId(), ); } }, $storageConfigs, $availableStorages); diff --git a/apps/files_external/lib/Event/StorageCreatedEvent.php b/apps/files_external/lib/Event/StorageCreatedEvent.php new file mode 100644 index 0000000000000..3b2a6424df368 --- /dev/null +++ b/apps/files_external/lib/Event/StorageCreatedEvent.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageCreatedEvent extends Event { + public function __construct( + private readonly StorageConfig $newConfig, + ) { + parent::__construct(); + } + + public function getNewConfig(): StorageConfig { + return $this->newConfig; + } +} diff --git a/apps/files_external/lib/Event/StorageDeletedEvent.php b/apps/files_external/lib/Event/StorageDeletedEvent.php new file mode 100644 index 0000000000000..9be6a61fe6de6 --- /dev/null +++ b/apps/files_external/lib/Event/StorageDeletedEvent.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageDeletedEvent extends Event { + public function __construct( + private readonly StorageConfig $oldConfig, + ) { + parent::__construct(); + } + + public function getOldConfig(): StorageConfig { + return $this->oldConfig; + } +} diff --git a/apps/files_external/lib/Event/StorageUpdatedEvent.php b/apps/files_external/lib/Event/StorageUpdatedEvent.php new file mode 100644 index 0000000000000..4cc5e5177fafb --- /dev/null +++ b/apps/files_external/lib/Event/StorageUpdatedEvent.php @@ -0,0 +1,29 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageUpdatedEvent extends Event { + public function __construct( + private readonly StorageConfig $oldConfig, + private readonly StorageConfig $newConfig, + ) { + parent::__construct(); + } + + public function getOldConfig(): StorageConfig { + return $this->oldConfig; + } + + public function getNewConfig(): StorageConfig { + return $this->newConfig; + } +} diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 2cb82d3790ae3..04ed47336d4e4 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -12,6 +12,7 @@ use OCA\Files_External\Lib\Auth\IUserProvided; use OCA\Files_External\Lib\Backend\Backend; use OCA\Files_External\ResponseDefinitions; +use OCP\IUser; /** * External storage configuration @@ -435,4 +436,23 @@ protected function formatStorageForUI(): void { } } } + + public function getMountPointForUser(IUser $user): string { + return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/'; + } + + public function __clone() { + $clone = new StorageConfig($this->getId()); + $clone->setBackend(clone $this->getBackend()); + $clone->setAuthMechanism(clone $this->getAuthMechanism()); + $clone->setBackendOptions($this->getBackendOptions()); + $clone->setMountPoint($this->getMountPoint()); + $clone->setStatus($this->getStatus(), $this->getStatusMessage()); + $clone->setPriority($this->getPriority()); + $clone->setApplicableUsers($this->getApplicableUsers()); + $clone->setApplicableGroups($this->getApplicableGroups()); + $clone->setMountOptions($this->getMountOptions()); + $clone->setType($this->getType()); + return $clone; + } } diff --git a/apps/files_external/lib/Service/DBConfigService.php b/apps/files_external/lib/Service/DBConfigService.php index f08f3442a478c..43380cb215b96 100644 --- a/apps/files_external/lib/Service/DBConfigService.php +++ b/apps/files_external/lib/Service/DBConfigService.php @@ -80,6 +80,32 @@ public function getMountsForUser($userId, $groupIds) { return $this->getMountsFromQuery($query); } + public function getMountsForGroups($groupIds) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type']) + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id')) + ->where($builder->expr()->andX( // mounts for group + $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GROUP, IQueryBuilder::PARAM_INT)), + $builder->expr()->in('a.value', $builder->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)), + )); + + return $this->getMountsFromQuery($query); + } + + public function getGlobalMounts() { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type']) + ->from('external_mounts', 'm') + ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id')) + ->where($builder->expr()->andX( // global mounts + $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)), + $builder->expr()->isNull('a.value'), + ), ); + + return $this->getMountsFromQuery($query); + } + public function modifyMountsOnUserDelete(string $uid): void { $this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER); } diff --git a/apps/files_external/lib/Service/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php index 5b1a9f41e4824..2694058c96845 100644 --- a/apps/files_external/lib/Service/GlobalStoragesService.php +++ b/apps/files_external/lib/Service/GlobalStoragesService.php @@ -8,8 +8,12 @@ namespace OCA\Files_External\Service; use OC\Files\Filesystem; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; +use OCA\Files_External\Event\StorageUpdatedEvent; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\MountConfig; +use OCP\IGroup; /** * Service class to manage global external storage @@ -62,9 +66,13 @@ protected function triggerHooks(StorageConfig $storage, $signal) { protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { // if mount point changed, it's like a deletion + creation if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage)); + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); return; + } else { + $this->eventDispatcher->dispatchTyped(new StorageUpdatedEvent($oldStorage, $newStorage)); } $userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers()); @@ -162,4 +170,31 @@ public function getStorageForAllUsers() { return array_combine($keys, $configs); } + + /** + * Gets all storages for the group, not including any global storages + * @return StorageConfig[] + */ + public function getAllStoragesForGroup(IGroup $group): array { + $mounts = $this->dbConfig->getMountsForGroups([$group->getGID()]); + $configs = array_map($this->getStorageConfigFromDBMount(...), $mounts); + $configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig); + $keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs); + + $storages = array_combine($keys, $configs); + return array_filter($storages, $this->validateStorage(...)); + } + + /** + * @return StorageConfig[] + */ + public function getAllGlobalStorages(): array { + $mounts = $this->dbConfig->getGlobalMounts(); + + $configs = array_map($this->getStorageConfigFromDBMount(...), $mounts); + $configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig); + $keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs); + $storages = array_combine($keys, $configs); + return array_filter($storages, $this->validateStorage(...)); + } } diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php new file mode 100644 index 0000000000000..66cc609c71a03 --- /dev/null +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -0,0 +1,238 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Service; + +use OC\Files\Cache\CacheEntry; +use OC\Files\Storage\FailedStorage; +use OC\User\LazyUser; +use OCA\Files_External\Config\ConfigAdapter; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; +use OCA\Files_External\Event\StorageUpdatedEvent; +use OCA\Files_External\Lib\StorageConfig; +use OCP\Cache\CappedMemoryCache; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Config\IUserMountCache; +use OCP\Group\Events\BeforeGroupDeletedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\UserCreatedEvent; + +/** + * Listens to config events and update the mounts for the applicable users + * + * @template-implements IEventListener + */ +class MountCacheService implements IEventListener { + private CappedMemoryCache $storageRootCache; + + public function __construct( + private readonly IUserMountCache $userMountCache, + private readonly ConfigAdapter $configAdapter, + private readonly IUserManager $userManager, + private readonly IGroupManager $groupManager, + private readonly GlobalStoragesService $storagesService, + ) { + $this->storageRootCache = new CappedMemoryCache(); + } + + public function handle(Event $event): void { + if ($event instanceof StorageCreatedEvent) { + $this->registerAddedStorage($event->getNewConfig()); + } + if ($event instanceof StorageDeletedEvent) { + $this->registerDeletedStorage($event->getOldConfig()); + } + if ($event instanceof StorageUpdatedEvent) { + $this->registerUpdatedStorage($event->getOldConfig(), $event->getNewConfig()); + } + if ($event instanceof UserAddedEvent) { + $this->addUserToGroup($event->getGroup(), $event->getUser()); + } + if ($event instanceof UserRemovedEvent) { + $this->removeUserFromGroup($event->getGroup(), $event->getUser()); + } + if ($event instanceof BeforeGroupDeletedEvent) { + $this->removeGroup($event->getGroup()); + } + if ($event instanceof UserCreatedEvent) { + $this->addUser($event->getUser()); + } + if ($event instanceof PostLoginEvent) { + $this->onLogin($event->getUser()); + } + } + + + /** + * Get all users that have access to a storage, either directly or through a group + * + * @param StorageConfig $storage + * @return \Iterator + */ + private function getUsersForStorage(StorageConfig $storage): \Iterator { + $yielded = []; + if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) { + yield from $this->userManager->getSeenUsers(); + } + foreach ($storage->getApplicableUsers() as $userId) { + $yielded[$userId] = true; + yield $userId => new LazyUser($userId, $this->userManager); + } + foreach ($storage->getApplicableGroups() as $groupId) { + if ($group = $this->groupManager->get($groupId)) { + foreach ($group->searchUsers('') as $user) { + if (!isset($yielded[$user->getUID()])) { + $yielded[$user->getUID()] = true; + yield $user->getUID() => $user; + } + } + } + } + } + + public function registerDeletedStorage(StorageConfig $storage): void { + foreach ($this->getUsersForStorage($storage) as $user) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); + } + } + + public function registerAddedStorage(StorageConfig $storage): void { + foreach ($this->getUsersForStorage($storage) as $user) { + $this->registerForUser($user, $storage); + } + } + + public function registerUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void { + /** @var array $oldApplicable */ + $oldApplicable = iterator_to_array($this->getUsersForStorage($oldStorage)); + /** @var array $newApplicable */ + $newApplicable = iterator_to_array($this->getUsersForStorage($newStorage)); + + foreach ($oldApplicable as $oldUser) { + if (!isset($newApplicable[$oldUser->getUID()])) { + $this->userMountCache->removeMount($oldStorage->getMountPointForUser($oldUser)); + } + } + + foreach ($newApplicable as $newUser) { + if (!isset($oldApplicable[$newUser->getUID()])) { + $this->registerForUser($newUser, $newStorage); + } + } + } + + private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry { + try { + $userStorage = $this->configAdapter->constructStorageForUser($user, clone $storage); + } catch (\Exception $e) { + $userStorage = new FailedStorage(['exception' => $e]); + } + + if ($cachedEntry = $this->storageRootCache->get($userStorage->getId())) { + return $cachedEntry; + } + + $cache = $userStorage->getCache(); + $entry = $cache->get(''); + if ($entry && $entry->getId() !== -1) { + $this->storageRootCache->set($userStorage->getId(), $entry); + return $entry; + } + + // create a "fake" root entry so we have a fileid so we don't have to interact with the remote service + // this will be scanned on first access + $data = [ + 'path' => '', + 'path_hash' => md5(''), + 'size' => 0, + 'unencrypted_size' => 0, + 'mtime' => 0, + 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE, + 'parent' => -1, + 'name' => '', + 'storage_mtime' => 0, + 'permissions' => 31, + 'storage' => $cache->getNumericStorageId(), + 'etag' => '', + 'encrypted' => 0, + 'checksum' => '', + ]; + if ($cache->getNumericStorageId() !== -1) { + $data['fileid'] = $cache->insert('', $data); + } else { + $data['fileid'] = -1; + } + + $entry = new CacheEntry($data); + $this->storageRootCache->set($userStorage->getId(), $entry); + return $entry; + } + + private function registerForUser(IUser $user, StorageConfig $storage): void { + $this->userMountCache->addMount( + $user, + $storage->getMountPointForUser($user), + $this->getCacheEntryForRoot($user, $storage), + ConfigAdapter::class, + $storage->getId(), + ); + } + + private function removeUserFromGroup(IGroup $group, IUser $user): void { + $storages = $this->storagesService->getAllStoragesForGroup($group); + foreach ($storages as $storage) { + if (!in_array($user->getUID(), $storage->getApplicableUsers())) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); + } + } + } + + private function addUserToGroup(IGroup $group, IUser $user): void { + $storages = $this->storagesService->getAllStoragesForGroup($group); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } + + private function removeGroup(IGroup $group): void { + $storages = $this->storagesService->getAllStoragesForGroup($group); + foreach ($storages as $storage) { + foreach ($group->searchUsers('') as $user) { + if (!in_array($user->getUID(), $storage->getApplicableUsers())) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); + } + } + } + } + + private function addUser(IUser $user): void { + $storages = $this->storagesService->getAllGlobalStorages(); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } + + /** + * Since storage config can rely on login credentials, we might need to update the config + */ + private function onLogin(IUser $user): void { + $storages = $this->storagesService->getAllGlobalStorages(); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } +} diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php index 7b1b7ba2dc1ff..c61c19aa3915b 100644 --- a/apps/files_external/lib/Service/StoragesService.php +++ b/apps/files_external/lib/Service/StoragesService.php @@ -12,6 +12,8 @@ use OCA\Files\AppInfo\Application as FilesApplication; use OCA\Files\ConfigLexicon; use OCA\Files_External\AppInfo\Application; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\InvalidAuth; use OCA\Files_External\Lib\Backend\Backend; @@ -20,7 +22,6 @@ use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\NotFoundException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\StorageNotAvailableException; use OCP\IAppConfig; @@ -36,13 +37,11 @@ abstract class StoragesService { /** * @param BackendService $backendService * @param DBConfigService $dbConfig - * @param IUserMountCache $userMountCache * @param IEventDispatcher $eventDispatcher */ public function __construct( protected BackendService $backendService, protected DBConfigService $dbConfig, - protected IUserMountCache $userMountCache, protected IEventDispatcher $eventDispatcher, protected IAppConfig $appConfig, ) { @@ -244,6 +243,7 @@ public function addStorage(StorageConfig $newStorage) { // add new storage $allStorages[$configId] = $newStorage; + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); $newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS); @@ -424,15 +424,6 @@ public function updateStorage(StorageConfig $updatedStorage) { $this->triggerChangeHooks($oldStorage, $updatedStorage); - if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly - $this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage)); - } else { - $storageId = $this->getStorageId($updatedStorage); - foreach ($removedUsers as $userId) { - $this->userMountCache->removeUserStorageMount($storageId, $userId); - } - } - $this->updateOverwriteHomeFolders(); return $this->getStorage($id); @@ -455,6 +446,7 @@ public function removeStorage(int $id) { $this->dbConfig->removeMount($id); $deletedStorage = $this->getStorageConfigFromDBMount($existingMount); + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($deletedStorage)); $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount); // delete oc_storages entries and oc_filecache diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php index 6c943247b2069..c2b22344b3b0f 100644 --- a/apps/files_external/lib/Service/UserGlobalStoragesService.php +++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php @@ -9,7 +9,6 @@ use OCA\Files_External\Lib\StorageConfig; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IUser; @@ -27,11 +26,10 @@ public function __construct( DBConfigService $dbConfig, IUserSession $userSession, protected IGroupManager $groupManager, - IUserMountCache $userMountCache, IEventDispatcher $eventDispatcher, IAppConfig $appConfig, ) { - parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig); + parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig); $this->userSession = $userSession; } diff --git a/apps/files_external/lib/Service/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php index bd8dd2d348c1e..21746deee5821 100644 --- a/apps/files_external/lib/Service/UserStoragesService.php +++ b/apps/files_external/lib/Service/UserStoragesService.php @@ -8,11 +8,12 @@ namespace OCA\Files_External\Service; use OC\Files\Filesystem; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\MountConfig; use OCA\Files_External\NotFoundException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\IAppConfig; use OCP\IUserSession; @@ -30,12 +31,11 @@ public function __construct( BackendService $backendService, DBConfigService $dbConfig, IUserSession $userSession, - IUserMountCache $userMountCache, IEventDispatcher $eventDispatcher, IAppConfig $appConfig, ) { $this->userSession = $userSession; - parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig); + parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig); } protected function readDBConfig() { @@ -72,6 +72,8 @@ protected function triggerHooks(StorageConfig $storage, $signal) { protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { // if mount point changed, it's like a deletion + creation if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage)); + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); } diff --git a/apps/files_external/tests/Service/GlobalStoragesServiceTest.php b/apps/files_external/tests/Service/GlobalStoragesServiceTest.php index a76005718d37a..33e791930b5c5 100644 --- a/apps/files_external/tests/Service/GlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/GlobalStoragesServiceTest.php @@ -17,7 +17,7 @@ class GlobalStoragesServiceTest extends StoragesServiceTestCase { protected function setUp(): void { parent::setUp(); - $this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->eventDispatcher, $this->appConfig); } protected function tearDown(): void { diff --git a/apps/files_external/tests/Service/StoragesServiceTestCase.php b/apps/files_external/tests/Service/StoragesServiceTestCase.php index fdc086751af7d..5f3c60a84a921 100644 --- a/apps/files_external/tests/Service/StoragesServiceTestCase.php +++ b/apps/files_external/tests/Service/StoragesServiceTestCase.php @@ -60,7 +60,6 @@ abstract class StoragesServiceTestCase extends \Test\TestCase { protected string $dataDir; protected CleaningDBConfig $dbConfig; protected static array $hookCalls; - protected IUserMountCache&MockObject $mountCache; protected IEventDispatcher&MockObject $eventDispatcher; protected IAppConfig&MockObject $appConfig; @@ -75,7 +74,6 @@ protected function setUp(): void { ); MountConfig::$skipTest = true; - $this->mountCache = $this->createMock(IUserMountCache::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->appConfig = $this->createMock(IAppConfig::class); diff --git a/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php b/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php index e38835f2077a1..d6117570f0d43 100644 --- a/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php @@ -71,7 +71,6 @@ protected function setUp(): void { $this->dbConfig, $userSession, $this->groupManager, - $this->mountCache, $this->eventDispatcher, $this->appConfig, ); diff --git a/apps/files_external/tests/Service/UserStoragesServiceTest.php b/apps/files_external/tests/Service/UserStoragesServiceTest.php index 99482a9cbbeb1..5fe3a2eab724b 100644 --- a/apps/files_external/tests/Service/UserStoragesServiceTest.php +++ b/apps/files_external/tests/Service/UserStoragesServiceTest.php @@ -34,7 +34,7 @@ class UserStoragesServiceTest extends StoragesServiceTestCase { protected function setUp(): void { parent::setUp(); - $this->globalStoragesService = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->globalStoragesService = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->eventDispatcher, $this->appConfig); $this->userId = $this->getUniqueID('user_'); $this->createUser($this->userId, $this->userId); @@ -47,7 +47,7 @@ protected function setUp(): void { ->method('getUser') ->willReturn($this->user); - $this->service = new UserStoragesService($this->backendService, $this->dbConfig, $userSession, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->service = new UserStoragesService($this->backendService, $this->dbConfig, $userSession, $this->eventDispatcher, $this->appConfig); } private function makeTestStorageData() { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5d515c66dd32a..d37a8a459b889 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -416,6 +416,7 @@ 'OCP\\Files\\Config\\Event\\UserMountAddedEvent' => $baseDir . '/lib/public/Files/Config/Event/UserMountAddedEvent.php', 'OCP\\Files\\Config\\Event\\UserMountRemovedEvent' => $baseDir . '/lib/public/Files/Config/Event/UserMountRemovedEvent.php', 'OCP\\Files\\Config\\Event\\UserMountUpdatedEvent' => $baseDir . '/lib/public/Files/Config/Event/UserMountUpdatedEvent.php', + 'OCP\\Files\\Config\\IAuthoritativeMountProvider' => $baseDir . '/lib/public/Files/Config/IAuthoritativeMountProvider.php', 'OCP\\Files\\Config\\ICachedMountFileInfo' => $baseDir . '/lib/public/Files/Config/ICachedMountFileInfo.php', 'OCP\\Files\\Config\\ICachedMountInfo' => $baseDir . '/lib/public/Files/Config/ICachedMountInfo.php', 'OCP\\Files\\Config\\IHomeMountProvider' => $baseDir . '/lib/public/Files/Config/IHomeMountProvider.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d25b0f94870d0..8916271ef6225 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -457,6 +457,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Files\\Config\\Event\\UserMountAddedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Config/Event/UserMountAddedEvent.php', 'OCP\\Files\\Config\\Event\\UserMountRemovedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Config/Event/UserMountRemovedEvent.php', 'OCP\\Files\\Config\\Event\\UserMountUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Config/Event/UserMountUpdatedEvent.php', + 'OCP\\Files\\Config\\IAuthoritativeMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IAuthoritativeMountProvider.php', 'OCP\\Files\\Config\\ICachedMountFileInfo' => __DIR__ . '/../../..' . '/lib/public/Files/Config/ICachedMountFileInfo.php', 'OCP\\Files\\Config\\ICachedMountInfo' => __DIR__ . '/../../..' . '/lib/public/Files/Config/ICachedMountInfo.php', 'OCP\\Files\\Config\\IHomeMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IHomeMountProvider.php', diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 7079c8a295730..238ee959a0d11 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -7,12 +7,14 @@ */ namespace OC\Files\Config; +use OC\DB\Exceptions\DbalException; use OC\User\LazyUser; use OCP\Cache\CappedMemoryCache; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Config\Event\UserMountAddedEvent; use OCP\Files\Config\Event\UserMountRemovedEvent; use OCP\Files\Config\Event\UserMountUpdatedEvent; @@ -524,4 +526,33 @@ public function getMountsInPath(IUser $user, string $path): array { return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path); }); } + + public function removeMount(string $mountPoint): void { + $query = $this->connection->getQueryBuilder(); + $query->delete('mounts') + ->where($query->expr()->eq('mount_point', $query->createNamedParameter($mountPoint))); + $query->executeStatement(); + } + + public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void { + $query = $this->connection->getQueryBuilder(); + $query->insert('mounts') + ->values([ + 'storage_id' => $query->createNamedParameter($rootCacheEntry->getStorageId()), + 'root_id' => $query->createNamedParameter($rootCacheEntry->getId()), + 'user_id' => $query->createNamedParameter($user->getUID()), + 'mount_point' => $query->createNamedParameter($mountPoint), + 'mount_point_hash' => $query->createNamedParameter(hash('xxh128', $mountPoint)), + 'mount_id' => $query->createNamedParameter($mountId), + 'mount_provider_class' => $query->createNamedParameter($mountProvider) + ]); + + try { + $query->executeStatement(); + } catch (DbalException $e) { + if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + } + } } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 4a42a397a8e47..161c3922c374a 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -824,7 +824,7 @@ public function getSeenUsers(int $offset = 0, ?int $limit = null): \Iterator { foreach ($this->backends as $backend) { if ($backend->userExists($userId)) { $user = new LazyUser($userId, $this, null, $backend); - yield $user; + yield $userId => $user; break; } } diff --git a/lib/public/Files/Config/IAuthoritativeMountProvider.php b/lib/public/Files/Config/IAuthoritativeMountProvider.php new file mode 100644 index 0000000000000..d0a6b69d8d874 --- /dev/null +++ b/lib/public/Files/Config/IAuthoritativeMountProvider.php @@ -0,0 +1,18 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Files\Config; + +/** + * Marks a mount provider as being authoritative, meaning that it will proactively update the cached mounts + * + * @since 33.0.0 + */ +interface IAuthoritativeMountProvider { + +} diff --git a/lib/public/Files/Config/IUserMountCache.php b/lib/public/Files/Config/IUserMountCache.php index a5b68ded66d15..89e23577f5dd3 100644 --- a/lib/public/Files/Config/IUserMountCache.php +++ b/lib/public/Files/Config/IUserMountCache.php @@ -7,6 +7,7 @@ */ namespace OCP\Files\Config; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\IUser; @@ -132,4 +133,24 @@ public function getMountForPath(IUser $user, string $path): ICachedMountInfo; * @since 24.0.0 */ public function getMountsInPath(IUser $user, string $path): array; + + /** + * Remove a mount by it's mountpoint + * + * @param string $mountPoint + * @return void + */ + public function removeMount(string $mountPoint): void; + + /** + * Register a new mountpoint for a user + * + * @param IUser $user + * @param string $mountPoint + * @param ICacheEntry $rootCacheEntry + * @param string $mountProvider + * @param int|null $mountId + * @return void + */ + public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void; } diff --git a/lib/public/IUserManager.php b/lib/public/IUserManager.php index 226a52809a3d0..e805d82c5abcd 100644 --- a/lib/public/IUserManager.php +++ b/lib/public/IUserManager.php @@ -239,9 +239,11 @@ public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string * An iterator is returned allowing the caller to stop the iteration at any time. * The offset argument allows the caller to continue the iteration at a specific offset. * + * @since 33.0.0 users are yielded with the user id as key + * * @param int $offset from which offset to fetch * @param int|null $limit maximum number of records to fetch - * @return \Iterator list of IUser object + * @return \Iterator list of IUser object * @since 32.0.0 */ public function getSeenUsers(int $offset = 0, ?int $limit = null): \Iterator;