From 198d500ac6d2aea88f4457abc481b8a8aac4f180 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 11 Nov 2025 20:46:39 +0100 Subject: [PATCH 01/13] feat: add interface to mark mount providers as authoritative Signed-off-by: Robin Appelman --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Config/IAuthoritativeMountProvider.php | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 lib/public/Files/Config/IAuthoritativeMountProvider.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 27b42cb14b75d..6f689c1f5da15 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -418,6 +418,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 03bd8e7d0bf64..2dd99a8b1c6fa 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -459,6 +459,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/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 { + +} From e3551e576c205560dbc0b819bdd8acb8c30cba56 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 12 Nov 2025 22:09:51 +0100 Subject: [PATCH 02/13] feat: add api for authoritative mount providers to update the user mounts Signed-off-by: Robin Appelman --- lib/private/Files/Config/UserMountCache.php | 30 +++++++++++++++++++++ lib/public/Files/Config/IUserMountCache.php | 15 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 7079c8a295730..2a78491a37819 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,32 @@ 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_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/public/Files/Config/IUserMountCache.php b/lib/public/Files/Config/IUserMountCache.php index a5b68ded66d15..6e4ca41cd2816 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,18 @@ 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 + * + * @since 33.0.0 + */ + public function removeMount(string $mountPoint): void; + + /** + * Register a new mountpoint for a user + * + * @since 33.0.0 + */ + public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void; } From c80c980e299a36035ba567780fb7feb7b2317997 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 16:07:46 +0100 Subject: [PATCH 03/13] feat: add typed events for external storage config changes Signed-off-by: Robin Appelman --- .../composer/composer/autoload_classmap.php | 4 +++ .../composer/composer/autoload_static.php | 4 +++ .../lib/Event/StorageCreatedEvent.php | 24 +++++++++++++++ .../lib/Event/StorageDeletedEvent.php | 24 +++++++++++++++ .../lib/Event/StorageUpdatedEvent.php | 29 +++++++++++++++++++ .../lib/Service/GlobalStoragesService.php | 7 +++++ .../lib/Service/StoragesService.php | 4 +++ .../lib/Service/UserStoragesService.php | 4 +++ 8 files changed, 100 insertions(+) create mode 100644 apps/files_external/lib/Event/StorageCreatedEvent.php create mode 100644 apps/files_external/lib/Event/StorageDeletedEvent.php create mode 100644 apps/files_external/lib/Event/StorageUpdatedEvent.php 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/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/Service/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php index 5b1a9f41e4824..0358a597ff8db 100644 --- a/apps/files_external/lib/Service/GlobalStoragesService.php +++ b/apps/files_external/lib/Service/GlobalStoragesService.php @@ -8,6 +8,9 @@ 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; @@ -62,9 +65,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()); diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php index 7b1b7ba2dc1ff..119217a21bd05 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; @@ -244,6 +246,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); @@ -455,6 +458,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/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php index bd8dd2d348c1e..feb26ba2a7c44 100644 --- a/apps/files_external/lib/Service/UserStoragesService.php +++ b/apps/files_external/lib/Service/UserStoragesService.php @@ -8,6 +8,8 @@ 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; @@ -72,6 +74,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); } From 765d1af2a6556aa5fe62fea2d7f770f8fed4af28 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 18:08:18 +0100 Subject: [PATCH 04/13] feat: yield user by id in IUserManager::getSeenUsers Signed-off-by: Robin Appelman --- lib/private/User/Manager.php | 2 +- lib/public/IUserManager.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 2bea9bb65dd74..a94e6d979f1fb 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -826,7 +826,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/IUserManager.php b/lib/public/IUserManager.php index caf1a704cce99..9b146ae64159e 100644 --- a/lib/public/IUserManager.php +++ b/lib/public/IUserManager.php @@ -240,9 +240,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; From 5565cdb39035317a20d4ae62bb2b005650b0d7b6 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 18:10:48 +0100 Subject: [PATCH 05/13] feat: make external storage mount provider authoritative Signed-off-by: Robin Appelman # Conflicts: # apps/files_external/lib/AppInfo/Application.php --- .../lib/AppInfo/Application.php | 7 + .../lib/Config/ConfigAdapter.php | 17 +- apps/files_external/lib/Lib/StorageConfig.php | 5 + .../lib/Service/MountCacheService.php | 156 ++++++++++++++++++ .../lib/Service/StoragesService.php | 12 -- .../lib/Service/UserGlobalStoragesService.php | 3 +- .../lib/Service/UserStoragesService.php | 4 +- .../Service/GlobalStoragesServiceTest.php | 2 +- .../tests/Service/StoragesServiceTestCase.php | 2 - .../Service/UserGlobalStoragesServiceTest.php | 1 - .../tests/Service/UserStoragesServiceTest.php | 4 +- 11 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 apps/files_external/lib/Service/MountCacheService.php diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 1ad1a2ed779c7..08a96e6265c14 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; @@ -45,6 +48,7 @@ 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; @@ -77,6 +81,9 @@ public function register(IRegistrationContext $context): void { $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->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/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 2cb82d3790ae3..3111456ac36b3 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,8 @@ protected function formatStorageForUI(): void { } } } + + public function getMountPointForUser(IUser $user): string { + return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/'; + } } diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php new file mode 100644 index 0000000000000..3e536ce999314 --- /dev/null +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -0,0 +1,156 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Service; + +use OC\Files\Cache\CacheEntry; +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\EventDispatcher\Event; +use OCP\EventDispatcher\Event as T; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\IMimeTypeLoader; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; + +/** + * Listens to config events and update the mounts for the applicable users + * + * @template-implements IEventListener + */ +class MountCacheService implements IEventListener { + public function __construct( + private readonly IUserMountCache $userMountCache, + private readonly ConfigAdapter $configAdapter, + private readonly IUserManager $userManager, + private readonly IGroupManager $groupManager, + ) { + } + + public function handle(Event $event): void { + if ($event instanceof StorageCreatedEvent) { + $this->handleAddedStorage($event->getNewConfig()); + } + if ($event instanceof StorageDeletedEvent) { + $this->handleDeletedStorage($event->getOldConfig()); + } + if ($event instanceof StorageUpdatedEvent) { + $this->handleUpdatedStorage($event->getOldConfig(), $event->getNewConfig()); + } + } + + + /** + * 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) { + $group = $this->groupManager->get($groupId); + if ($group !== null) { + foreach ($group->searchUsers('') as $user) { + if (!isset($yielded[$user->getUID()])) { + $yielded[$user->getUID()] = true; + yield $user->getUID() => $user; + } + } + } + } + } + + public function handleDeletedStorage(StorageConfig $storage): void { + foreach ($this->getUsersForStorage($storage) as $user) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); + } + } + + public function handleAddedStorage(StorageConfig $storage): void { + foreach ($this->getUsersForStorage($storage) as $user) { + $this->registerForUser($user, $storage); + } + } + + public function handleUpdatedStorage(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 { + // todo: reuse these between users when possible + $storage = $this->configAdapter->constructStorageForUser($user, $storage); + $cache = $storage->getCache(); + $entry = $cache->get(''); + if ($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' => '', + ]; + $data['fileid'] = $cache->insert('', $data); + + return new CacheEntry($data); + } + + private function registerForUser(IUser $user, StorageConfig $storage): void { + $this->userMountCache->addMount( + $user, + $storage->getMountPointForUser($user), + $this->getCacheEntryForRoot($user, $storage), + ConfigAdapter::class, + $storage->getId(), + ); + } +} diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php index 119217a21bd05..c61c19aa3915b 100644 --- a/apps/files_external/lib/Service/StoragesService.php +++ b/apps/files_external/lib/Service/StoragesService.php @@ -22,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; @@ -38,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, ) { @@ -427,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); diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php index 6c943247b2069..2607472e96945 100644 --- a/apps/files_external/lib/Service/UserGlobalStoragesService.php +++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php @@ -27,11 +27,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 feb26ba2a7c44..21746deee5821 100644 --- a/apps/files_external/lib/Service/UserStoragesService.php +++ b/apps/files_external/lib/Service/UserStoragesService.php @@ -14,7 +14,6 @@ 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; @@ -32,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() { 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() { From d64f7eb93950513e9d9992cf55b2871cd051ccc1 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 21 Nov 2025 18:42:13 +0100 Subject: [PATCH 06/13] feat: listen to user/group events and update external storage mounts Signed-off-by: Robin Appelman --- .../lib/AppInfo/Application.php | 14 +++-- .../lib/Service/DBConfigService.php | 40 +++++++++++++- .../lib/Service/GlobalStoragesService.php | 28 ++++++++++ .../lib/Service/MountCacheService.php | 54 ++++++++++++++++++- .../lib/Service/UserGlobalStoragesService.php | 1 - 5 files changed, 128 insertions(+), 9 deletions(-) diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 08a96e6265c14..c54c2eea38911 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -45,7 +45,6 @@ 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; @@ -55,10 +54,12 @@ 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\UserCreatedEvent; use OCP\User\Events\UserDeletedEvent; -use OCP\User\Events\UserLoggedInEvent; /** * @package OCA\Files_External\AppInfo @@ -79,11 +80,14 @@ 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->registerConfigLexicon(ConfigLexicon::class); } diff --git a/apps/files_external/lib/Service/DBConfigService.php b/apps/files_external/lib/Service/DBConfigService.php index f08f3442a478c..207bb1b79b244 100644 --- a/apps/files_external/lib/Service/DBConfigService.php +++ b/apps/files_external/lib/Service/DBConfigService.php @@ -15,6 +15,8 @@ /** * Stores the mount config in the database + * + * @psalm-type StorageConfigData = array{type: int, priority: int, applicable: list, config: array, options: array} */ class DBConfigService { public const MOUNT_TYPE_ADMIN = 1; @@ -80,6 +82,39 @@ public function getMountsForUser($userId, $groupIds) { return $this->getMountsFromQuery($query); } + /** + * @param list $groupIds + * @return list + */ + public function getMountsForGroups(array $groupIds): array { + $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); + } + + /** + * @return list + */ + public function getGlobalMounts(): array { + $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); } @@ -376,7 +411,10 @@ public function removeApplicable($mountId, $type, $value) { $query->executeStatement(); } - private function getMountsFromQuery(IQueryBuilder $query) { + /** + * @return list + */ + private function getMountsFromQuery(IQueryBuilder $query): array { $result = $query->executeQuery(); $mounts = $result->fetchAllAssociative(); $uniqueMounts = []; diff --git a/apps/files_external/lib/Service/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php index 0358a597ff8db..2694058c96845 100644 --- a/apps/files_external/lib/Service/GlobalStoragesService.php +++ b/apps/files_external/lib/Service/GlobalStoragesService.php @@ -13,6 +13,7 @@ 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 @@ -169,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 index 3e536ce999314..924691ba5fa2e 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -20,16 +20,19 @@ use OCP\EventDispatcher\IEventListener; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Config\IUserMountCache; -use OCP\Files\IMimeTypeLoader; +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\UserCreatedEvent; /** * Listens to config events and update the mounts for the applicable users * - * @template-implements IEventListener + * @template-implements IEventListener */ class MountCacheService implements IEventListener { public function __construct( @@ -37,6 +40,7 @@ public function __construct( private readonly ConfigAdapter $configAdapter, private readonly IUserManager $userManager, private readonly IGroupManager $groupManager, + private readonly GlobalStoragesService $storagesService, ) { } @@ -50,6 +54,18 @@ public function handle(Event $event): void { if ($event instanceof StorageUpdatedEvent) { $this->handleUpdatedStorage($event->getOldConfig(), $event->getNewConfig()); } + if ($event instanceof UserAddedEvent) { + $this->handleUserAdded($event->getGroup(), $event->getUser()); + } + if ($event instanceof UserRemovedEvent) { + $this->handleUserRemoved($event->getGroup(), $event->getUser()); + } + if ($event instanceof BeforeGroupDeletedEvent) { + $this->handleGroupDeleted($event->getGroup()); + } + if ($event instanceof UserCreatedEvent) { + $this->handleUserCreated($event->getUser()); + } } @@ -153,4 +169,38 @@ private function registerForUser(IUser $user, StorageConfig $storage): void { $storage->getId(), ); } + + private function handleUserRemoved(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 handleUserAdded(IGroup $group, IUser $user): void { + $storages = $this->storagesService->getAllStoragesForGroup($group); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } + + private function handleGroupDeleted(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 handleUserCreated(IUser $user): void { + $storages = $this->storagesService->getAllGlobalStorages(); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } } diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php index 2607472e96945..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; From 73e84f9bf551364fa7a8f1939602a700918ebb06 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 16:55:10 +0100 Subject: [PATCH 07/13] perf: cache root cache entries for external storage in MountCacheService Signed-off-by: Robin Appelman --- .../lib/Service/MountCacheService.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index 924691ba5fa2e..56c2b39259054 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -15,6 +15,7 @@ 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\Event as T; use OCP\EventDispatcher\IEventListener; @@ -35,6 +36,8 @@ * @template-implements IEventListener */ class MountCacheService implements IEventListener { + private CappedMemoryCache $storageRootCache; + public function __construct( private readonly IUserMountCache $userMountCache, private readonly ConfigAdapter $configAdapter, @@ -42,6 +45,7 @@ public function __construct( private readonly IGroupManager $groupManager, private readonly GlobalStoragesService $storagesService, ) { + $this->storageRootCache = new CappedMemoryCache(); } public function handle(Event $event): void { @@ -129,11 +133,16 @@ public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $n } private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry { - // todo: reuse these between users when possible $storage = $this->configAdapter->constructStorageForUser($user, $storage); + + if ($cachedEntry = $this->storageRootCache->get($storage->getId())) { + return $cachedEntry; + } + $cache = $storage->getCache(); $entry = $cache->get(''); if ($entry) { + $this->storageRootCache->set($storage->getId(), $entry); return $entry; } @@ -157,7 +166,9 @@ private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICac ]; $data['fileid'] = $cache->insert('', $data); - return new CacheEntry($data); + $entry = new CacheEntry($data); + $this->storageRootCache->set($storage->getId(), $entry); + return $entry; } private function registerForUser(IUser $user, StorageConfig $storage): void { From 9dffca2f07e6e5800e5cf1e269813c5087d9af76 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 19:31:49 +0100 Subject: [PATCH 08/13] fix: improve handling of unavailable storages Signed-off-by: Robin Appelman --- apps/files_external/lib/Lib/StorageConfig.php | 5 ++++ .../lib/Service/MountCacheService.php | 25 +++++++++++++------ lib/private/Files/Config/UserMountCache.php | 1 + 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 3111456ac36b3..3f8a318b280fb 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -440,4 +440,9 @@ protected function formatStorageForUI(): void { public function getMountPointForUser(IUser $user): string { return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/'; } + + public function __clone(): void { + $this->backend = clone $this->backend; + $this->authMechanism = clone $this->authMechanism; + } } diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index 56c2b39259054..e284bf156d1fc 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -9,6 +9,7 @@ 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; @@ -17,7 +18,6 @@ use OCA\Files_External\Lib\StorageConfig; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\Event as T; use OCP\EventDispatcher\IEventListener; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Config\IUserMountCache; @@ -133,16 +133,21 @@ public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $n } private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry { - $storage = $this->configAdapter->constructStorageForUser($user, $storage); + try { + $userStorage = $this->configAdapter->constructStorageForUser($user, clone $storage); + } catch (\Exception $e) { + $userStorage = new FailedStorage(['exception' => $e]); + } - if ($cachedEntry = $this->storageRootCache->get($storage->getId())) { + $cachedEntry = $this->storageRootCache->get($userStorage->getId()); + if ($cachedEntry !== null) { return $cachedEntry; } - $cache = $storage->getCache(); + $cache = $userStorage->getCache(); $entry = $cache->get(''); - if ($entry) { - $this->storageRootCache->set($storage->getId(), $entry); + if ($entry && $entry->getId() !== -1) { + $this->storageRootCache->set($userStorage->getId(), $entry); return $entry; } @@ -164,10 +169,14 @@ private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICac 'encrypted' => 0, 'checksum' => '', ]; - $data['fileid'] = $cache->insert('', $data); + if ($cache->getNumericStorageId() !== -1) { + $data['fileid'] = $cache->insert('', $data); + } else { + $data['fileid'] = -1; + } $entry = new CacheEntry($data); - $this->storageRootCache->set($storage->getId(), $entry); + $this->storageRootCache->set($userStorage->getId(), $entry); return $entry; } diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 2a78491a37819..238ee959a0d11 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -542,6 +542,7 @@ public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCache '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) ]); From 6dd6d06606a55b07d243f481e8b78eea66651f56 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 19:40:14 +0100 Subject: [PATCH 09/13] fix: update external storage mounts on login Signed-off-by: Robin Appelman --- apps/files_external/lib/AppInfo/Application.php | 2 ++ .../lib/Service/MountCacheService.php | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index c54c2eea38911..924b72bf9822d 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -58,6 +58,7 @@ use OCP\Group\Events\GroupDeletedEvent; 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; @@ -87,6 +88,7 @@ public function register(IRegistrationContext $context): void { $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/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index e284bf156d1fc..3f0d59358277d 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -28,12 +28,13 @@ 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 + * @template-implements IEventListener */ class MountCacheService implements IEventListener { private CappedMemoryCache $storageRootCache; @@ -70,6 +71,9 @@ public function handle(Event $event): void { if ($event instanceof UserCreatedEvent) { $this->handleUserCreated($event->getUser()); } + if ($event instanceof PostLoginEvent) { + $this->onLogin($event->getUser()); + } } @@ -223,4 +227,14 @@ private function handleUserCreated(IUser $user): void { $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); + } + } } From fa61c4c57da86fa6e53e9cfd102a7abcc73a85b8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 2 Jan 2026 17:24:20 +0100 Subject: [PATCH 10/13] fix: don't error when checking login credential storage from cli Signed-off-by: Robin Appelman --- apps/files_external/lib/Config/UserContext.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/files_external/lib/Config/UserContext.php b/apps/files_external/lib/Config/UserContext.php index fb5c79a932974..a6ba80a5da22f 100644 --- a/apps/files_external/lib/Config/UserContext.php +++ b/apps/files_external/lib/Config/UserContext.php @@ -43,8 +43,10 @@ protected function getUserId(): ?string { } try { $shareToken = $this->request->getParam('token'); - $share = $this->shareManager->getShareByToken($shareToken); - return $share->getShareOwner(); + if ($shareToken !== null) { + $share = $this->shareManager->getShareByToken($shareToken); + return $share->getShareOwner(); + } } catch (ShareNotFound $e) { } From c14948f6293dfa3243f621515d5e6a4ffe44a4a7 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 2 Jan 2026 17:24:43 +0100 Subject: [PATCH 11/13] test: update cypress test for new files_external mount behavior Signed-off-by: Robin Appelman --- cypress/e2e/files_external/files-external-failed.cy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/files_external/files-external-failed.cy.ts b/cypress/e2e/files_external/files-external-failed.cy.ts index 59ece1db6f08f..3bb9696ab201f 100644 --- a/cypress/e2e/files_external/files-external-failed.cy.ts +++ b/cypress/e2e/files_external/files-external-failed.cy.ts @@ -35,7 +35,9 @@ describe('Files user credentials', { testIsolation: true }, () => { it('Create a failed user storage with invalid url', () => { const url = 'http://cloud.domain.com/remote.php/dav/files/abcdef123456' - createStorageWithConfig('Storage1', StorageBackend.DAV, AuthBackend.LoginCredentials, { host: url.replace('index.php/', ''), secure: 'false' }) + createStorageWithConfig('Storage1', StorageBackend.DAV, AuthBackend.LoginCredentials, { host: url.replace('index.php/', ''), secure: 'false' }).then((id) => { + cy.runOccCommand(`files_external:verify ${id}`) + }) cy.login(currentUser) cy.visit('/apps/files') @@ -59,6 +61,8 @@ describe('Files user credentials', { testIsolation: true }, () => { user: 'invaliduser', password: 'invalidpassword', secure: 'false', + }).then((id) => { + cy.runOccCommand(`files_external:verify ${id}`) }) cy.login(currentUser) From be3bbf22e00315821b453ccd35f25cd0d377363b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 7 Jan 2026 18:56:24 +0100 Subject: [PATCH 12/13] fix: better applicable check for updating external mounts Signed-off-by: Robin Appelman --- .../lib/Service/MountCacheService.php | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index 3f0d59358277d..9b4b594efe8cd 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -194,10 +194,26 @@ private function registerForUser(IUser $user, StorageConfig $storage): void { ); } + private function isApplicableForUser(StorageConfig $storage, IUser $user): bool { + if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) { + return true; + } + if (in_array($user->getUID(), $storage->getApplicableUsers())) { + return true; + } + $groupIds = $this->groupManager->getUserGroupIds($user); + foreach ($groupIds as $groupId) { + if (in_array($groupId, $storage->getApplicableGroups())) { + return true; + } + } + return false; + } + private function handleUserRemoved(IGroup $group, IUser $user): void { $storages = $this->storagesService->getAllStoragesForGroup($group); foreach ($storages as $storage) { - if (!in_array($user->getUID(), $storage->getApplicableUsers())) { + if (!$this->isApplicableForUser($storage, $user)) { $this->userMountCache->removeMount($storage->getMountPointForUser($user)); } } @@ -214,7 +230,7 @@ private function handleGroupDeleted(IGroup $group): void { $storages = $this->storagesService->getAllStoragesForGroup($group); foreach ($storages as $storage) { foreach ($group->searchUsers('') as $user) { - if (!in_array($user->getUID(), $storage->getApplicableUsers())) { + if (!$this->isApplicableForUser($storage, $user)) { $this->userMountCache->removeMount($storage->getMountPointForUser($user)); } } From 272d6141cae1c3a507e042a51fa1bd2d8524ee26 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 8 Jan 2026 00:00:01 +0100 Subject: [PATCH 13/13] fix: improve handling updated storages Signed-off-by: Robin Appelman --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/Lib/ApplicableHelper.php | 114 ++++++++++++ apps/files_external/lib/Lib/StorageConfig.php | 2 +- .../lib/Service/DBConfigService.php | 11 +- .../lib/Service/MountCacheService.php | 91 +++------- .../tests/ApplicableHelperTest.php | 170 ++++++++++++++++++ 7 files changed, 313 insertions(+), 77 deletions(-) create mode 100644 apps/files_external/lib/Lib/ApplicableHelper.php create mode 100644 apps/files_external/tests/ApplicableHelperTest.php diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index 8526237f829b5..da5e7329e0f7b 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -40,6 +40,7 @@ '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\\ApplicableHelper' => $baseDir . '/../lib/Lib/ApplicableHelper.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', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index b5b992356404b..adba91ec55389 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -55,6 +55,7 @@ class ComposerStaticInitFiles_External '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\\ApplicableHelper' => __DIR__ . '/..' . '/../lib/Lib/ApplicableHelper.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', diff --git a/apps/files_external/lib/Lib/ApplicableHelper.php b/apps/files_external/lib/Lib/ApplicableHelper.php new file mode 100644 index 0000000000000..1c603ec2c647b --- /dev/null +++ b/apps/files_external/lib/Lib/ApplicableHelper.php @@ -0,0 +1,114 @@ + + */ + public 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) { + $group = $this->groupManager->get($groupId); + if ($group !== null) { + foreach ($group->getUsers() as $user) { + if (!isset($yielded[$user->getUID()])) { + $yielded[$user->getUID()] = true; + yield $user->getUID() => $user; + } + } + } + } + } + + public function isApplicableForUser(StorageConfig $storage, IUser $user): bool { + if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) { + return true; + } + if (in_array($user->getUID(), $storage->getApplicableUsers())) { + return true; + } + $groupIds = $this->groupManager->getUserGroupIds($user); + foreach ($groupIds as $groupId) { + if (in_array($groupId, $storage->getApplicableGroups())) { + return true; + } + } + return false; + } + + /** + * Return all users that are applicable for storage $a, but not for $b + * + * @return \Iterator + */ + public function diffApplicable(StorageConfig $a, StorageConfig $b): \Iterator { + $aIsAll = count($a->getApplicableUsers()) + count($a->getApplicableGroups()) === 0; + $bIsAll = count($b->getApplicableUsers()) + count($b->getApplicableGroups()) === 0; + if ($bIsAll) { + return; + } + + if ($aIsAll) { + foreach ($this->getUsersForStorage($a) as $user) { + if (!$this->isApplicableForUser($b, $user)) { + yield $user; + } + } + } else { + $yielded = []; + foreach ($a->getApplicableGroups() as $groupId) { + if (!in_array($groupId, $b->getApplicableGroups())) { + $group = $this->groupManager->get($groupId); + if ($group) { + foreach ($group->getUsers() as $user) { + if (!$this->isApplicableForUser($b, $user)) { + if (!isset($yielded[$user->getUID()])) { + $yielded[$user->getUID()] = true; + yield $user; + } + } + } + } + } + } + foreach ($a->getApplicableUsers() as $userId) { + if (!in_array($userId, $b->getApplicableUsers())) { + $user = $this->userManager->get($userId); + if ($user && !$this->isApplicableForUser($b, $user)) { + if (!isset($yielded[$user->getUID()])) { + $yielded[$user->getUID()] = true; + yield $user; + } + } + } + } + } + } +} diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 3f8a318b280fb..133c5392ec772 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -441,7 +441,7 @@ public function getMountPointForUser(IUser $user): string { return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/'; } - public function __clone(): void { + public function __clone() { $this->backend = clone $this->backend; $this->authMechanism = clone $this->authMechanism; } diff --git a/apps/files_external/lib/Service/DBConfigService.php b/apps/files_external/lib/Service/DBConfigService.php index 207bb1b79b244..e9d4cbc89e763 100644 --- a/apps/files_external/lib/Service/DBConfigService.php +++ b/apps/files_external/lib/Service/DBConfigService.php @@ -16,7 +16,8 @@ /** * Stores the mount config in the database * - * @psalm-type StorageConfigData = array{type: int, priority: int, applicable: list, config: array, options: array} + * @psalm-type ApplicableConfig = array{type: int, value: string} + * @psalm-type StorageConfigData = array{type: int, priority: int, applicable: list, config: array, options: array, ...} */ class DBConfigService { public const MOUNT_TYPE_ADMIN = 1; @@ -451,9 +452,9 @@ private function getMountsFromQuery(IQueryBuilder $query): array { * @param string $table * @param string[] $fields * @param int[] $mountIds - * @return array [$mountId => [['field1' => $value1, ...], ...], ...] + * @return array> [$mountId => [['field1' => $value1, ...], ...], ...] */ - private function selectForMounts($table, array $fields, array $mountIds) { + private function selectForMounts(string $table, array $fields, array $mountIds): array { if (count($mountIds) === 0) { return []; } @@ -485,9 +486,9 @@ private function selectForMounts($table, array $fields, array $mountIds) { /** * @param int[] $mountIds - * @return array [$id => [['type' => $type, 'value' => $value], ...], ...] + * @return array> [$id => [['type' => $type, 'value' => $value], ...], ...] */ - public function getApplicableForMounts($mountIds) { + public function getApplicableForMounts(array $mountIds): array { return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds); } diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index 9b4b594efe8cd..2be17b9d59863 100644 --- a/apps/files_external/lib/Service/MountCacheService.php +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -10,11 +10,11 @@ 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\ApplicableHelper; use OCA\Files_External\Lib\StorageConfig; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\Event; @@ -25,9 +25,7 @@ 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; @@ -42,9 +40,8 @@ class MountCacheService implements IEventListener { public function __construct( private readonly IUserMountCache $userMountCache, private readonly ConfigAdapter $configAdapter, - private readonly IUserManager $userManager, - private readonly IGroupManager $groupManager, private readonly GlobalStoragesService $storagesService, + private readonly ApplicableHelper $applicableHelper, ) { $this->storageRootCache = new CappedMemoryCache(); } @@ -76,63 +73,24 @@ public function handle(Event $event): void { } } - - /** - * 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) { - $group = $this->groupManager->get($groupId); - if ($group !== null) { - foreach ($group->searchUsers('') as $user) { - if (!isset($yielded[$user->getUID()])) { - $yielded[$user->getUID()] = true; - yield $user->getUID() => $user; - } - } - } - } - } - public function handleDeletedStorage(StorageConfig $storage): void { - foreach ($this->getUsersForStorage($storage) as $user) { + foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) { $this->userMountCache->removeMount($storage->getMountPointForUser($user)); } } public function handleAddedStorage(StorageConfig $storage): void { - foreach ($this->getUsersForStorage($storage) as $user) { + foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) { $this->registerForUser($user, $storage); } } public function handleUpdatedStorage(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 ($this->applicableHelper->diffApplicable($oldStorage, $newStorage) as $user) { + $this->userMountCache->removeMount($oldStorage->getMountPointForUser($user)); } - - foreach ($newApplicable as $newUser) { - if (!isset($oldApplicable[$newUser->getUID()])) { - $this->registerForUser($newUser, $newStorage); - } + foreach ($this->applicableHelper->diffApplicable($newStorage, $oldStorage) as $user) { + $this->registerForUser($user, $newStorage); } } @@ -194,26 +152,10 @@ private function registerForUser(IUser $user, StorageConfig $storage): void { ); } - private function isApplicableForUser(StorageConfig $storage, IUser $user): bool { - if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) { - return true; - } - if (in_array($user->getUID(), $storage->getApplicableUsers())) { - return true; - } - $groupIds = $this->groupManager->getUserGroupIds($user); - foreach ($groupIds as $groupId) { - if (in_array($groupId, $storage->getApplicableGroups())) { - return true; - } - } - return false; - } - private function handleUserRemoved(IGroup $group, IUser $user): void { $storages = $this->storagesService->getAllStoragesForGroup($group); foreach ($storages as $storage) { - if (!$this->isApplicableForUser($storage, $user)) { + if (!$this->applicableHelper->isApplicableForUser($storage, $user)) { $this->userMountCache->removeMount($storage->getMountPointForUser($user)); } } @@ -229,10 +171,17 @@ private function handleUserAdded(IGroup $group, IUser $user): void { private function handleGroupDeleted(IGroup $group): void { $storages = $this->storagesService->getAllStoragesForGroup($group); foreach ($storages as $storage) { - foreach ($group->searchUsers('') as $user) { - if (!$this->isApplicableForUser($storage, $user)) { - $this->userMountCache->removeMount($storage->getMountPointForUser($user)); - } + $this->removeGroupFromStorage($storage, $group); + } + } + + /** + * Remove mounts from users in a group, if they don't have access to the storage trough other means + */ + private function removeGroupFromStorage(StorageConfig $storage, IGroup $group): void { + foreach ($group->searchUsers('') as $user) { + if (!$this->applicableHelper->isApplicableForUser($storage, $user)) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); } } } diff --git a/apps/files_external/tests/ApplicableHelperTest.php b/apps/files_external/tests/ApplicableHelperTest.php new file mode 100644 index 0000000000000..3085cf71d42b8 --- /dev/null +++ b/apps/files_external/tests/ApplicableHelperTest.php @@ -0,0 +1,170 @@ + */ + private array $users = []; + /** @var array> */ + private array $groups = []; + + private ApplicableHelper $applicableHelper; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + $this->userManager->method('get') + ->willReturnCallback(function (string $id) { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($id); + return $user; + }); + $this->userManager->method('getSeenUsers') + ->willReturnCallback(fn () => new \ArrayIterator(array_map($this->userManager->get(...), $this->users))); + $this->groupManager->method('get') + ->willReturnCallback(function (string $id) { + $group = $this->createMock(IGroup::class); + $group->method('getGID')->willReturn($id); + $group->method('getUsers') + ->willReturn(array_map($this->userManager->get(...), $this->groups[$id] ?: [])); + return $group; + }); + $this->groupManager->method('getUserGroupIds') + ->willReturnCallback(function (IUser $user) { + $groups = []; + foreach ($this->groups as $group => $users) { + if (in_array($user->getUID(), $users)) { + $groups[] = $group; + } + } + return $groups; + }); + + $this->applicableHelper = new ApplicableHelper($this->userManager, $this->groupManager); + + $this->users = ['user1', 'user2', 'user3', 'user4']; + $this->groups = [ + 'group1' => ['user1', 'user2'], + 'group2' => ['user3'], + ]; + } + + public static function usersForStorageProvider(): array { + return [ + [[], [], ['user1', 'user2', 'user3', 'user4']], + [['user1'], [], ['user1']], + [['user1', 'user3'], [], ['user1', 'user3']], + [['user1'], ['group1'], ['user1', 'user2']], + [['user1'], ['group2'], ['user1', 'user3']], + ]; + } + + #[DataProvider('usersForStorageProvider')] + public function testGetUsersForStorage(array $applicableUsers, array $applicableGroups, array $expected) { + $storage = $this->createMock(StorageConfig::class); + $storage->method('getApplicableUsers') + ->willReturn($applicableUsers); + $storage->method('getApplicableGroups') + ->willReturn($applicableGroups); + + $result = iterator_to_array($this->applicableHelper->getUsersForStorage($storage)); + $result = array_map(fn (IUser $user) => $user->getUID(), $result); + sort($result); + sort($expected); + $this->assertEquals($expected, $result); + } + + public static function applicableProvider(): array { + return [ + [[], [], 'user1', true], + [['user1'], [], 'user1', true], + [['user1'], [], 'user2', false], + [['user1', 'user3'], [], 'user1', true], + [['user1', 'user3'], [], 'user2', false], + [['user1'], ['group1'], 'user1', true], + [['user1'], ['group1'], 'user2', true], + [['user1'], ['group1'], 'user3', false], + [['user1'], ['group1'], 'user4', false], + [['user1'], ['group2'], 'user1', true], + [['user1'], ['group2'], 'user2', false], + [['user1'], ['group2'], 'user3', true], + [['user1'], ['group1'], 'user4', false], + ]; + } + + #[DataProvider('applicableProvider')] + public function testIsApplicable(array $applicableUsers, array $applicableGroups, string $user, bool $expected) { + $storage = $this->createMock(StorageConfig::class); + $storage->method('getApplicableUsers') + ->willReturn($applicableUsers); + $storage->method('getApplicableGroups') + ->willReturn($applicableGroups); + + $this->assertEquals($expected, $this->applicableHelper->isApplicableForUser($storage, $this->userManager->get($user))); + } + + public static function diffProvider(): array { + return [ + [[], [], [], [], []], // both all + [['user1'], [], [], [], []], // all added + [[], [], ['user1'], [], ['user2', 'user3', 'user4']], // all removed + [[], [], [], ['group1'], ['user3', 'user4']], // all removed + [[], [], ['user3'], ['group1'], ['user4']], // all removed + [['user1'], [], ['user1'], [], []], + [['user1'], [], ['user1', 'user2'], [], []], + [['user1'], [], ['user2'], [], ['user1']], + [['user1'], [], [], ['group1'], []], + [['user1'], [], [], ['group2'], ['user1']], + [[], ['group1'], [], ['group2'], ['user1', 'user2']], + [[], ['group1'], ['user1'], [], ['user2']], + [['user1'], ['group1'], ['user1'], [], ['user2']], + [['user1'], ['group1'], [], ['group1'], []], + [['user1'], ['group1'], [], ['group2'], ['user1', 'user2']], + [['user1'], ['group1'], ['user1'], ['group2'], ['user2']], + ]; + } + + #[DataProvider('diffProvider')] + public function testDiff(array $applicableUsersA, array $applicableGroupsA, array $applicableUsersB, array $applicableGroupsB, array $expected) { + $storageA = $this->createMock(StorageConfig::class); + $storageA->method('getApplicableUsers') + ->willReturn($applicableUsersA); + $storageA->method('getApplicableGroups') + ->willReturn($applicableGroupsA); + + $storageB = $this->createMock(StorageConfig::class); + $storageB->method('getApplicableUsers') + ->willReturn($applicableUsersB); + $storageB->method('getApplicableGroups') + ->willReturn($applicableGroupsB); + + $result = iterator_to_array($this->applicableHelper->diffApplicable($storageA, $storageB)); + $result = array_map(fn (IUser $user) => $user->getUID(), $result); + sort($result); + sort($expected); + $this->assertEquals($expected, $result); + } +}