From 5e9ddc79a920d4868d1a73f2f4029f186de8a58b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 11 Nov 2025 20:46:39 +0100 Subject: [PATCH 1/9] 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 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/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 7fdfe90ad870fcfb455d676414192beb800c1c05 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 12 Nov 2025 22:09:51 +0100 Subject: [PATCH 2/9] 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 | 21 +++++++++++++++ 2 files changed, 51 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..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; } From 53590789ee613264ea1d03305c90fc7b67d4f480 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 16:07:46 +0100 Subject: [PATCH 3/9] 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 cfe1059cf1493ebd6be260cfc8d262bf57f0b579 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 18:08:18 +0100 Subject: [PATCH 4/9] 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 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/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; From a49cc566374eaab5cc1177d7a4c33b52f8eb1b6b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 18:10:48 +0100 Subject: [PATCH 5/9] 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 | 155 ++++++++++++++++++ .../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, 183 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..5798a95b75a71 --- /dev/null +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -0,0 +1,155 @@ + + * 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->registerAddedStorage($event->getNewConfig()); + } + if ($event instanceof StorageDeletedEvent) { + $this->registerDeletedStorage($event->getOldConfig()); + } + if ($event instanceof StorageUpdatedEvent) { + $this->registerUpdatedStorage($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) { + 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 { + // 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 1efb8ca5ef08b32af2fc16f2664d27f27e3f8b55 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 21 Nov 2025 18:42:13 +0100 Subject: [PATCH 6/9] 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 | 26 +++++++++ .../lib/Service/GlobalStoragesService.php | 28 ++++++++++ .../lib/Service/MountCacheService.php | 54 ++++++++++++++++++- .../lib/Service/UserGlobalStoragesService.php | 1 - 5 files changed, 115 insertions(+), 8 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..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 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 5798a95b75a71..307aa8d2d759b 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->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()); + } } @@ -152,4 +168,38 @@ private function registerForUser(IUser $user, StorageConfig $storage): void { $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); + } + } } 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 edea8cdfd537d9eaf689e7773fc0cd767232b869 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 16:55:10 +0100 Subject: [PATCH 7/9] 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 307aa8d2d759b..59edc62f9f2bf 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 { @@ -128,11 +132,16 @@ public function registerUpdatedStorage(StorageConfig $oldStorage, StorageConfig } 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; } @@ -156,7 +165,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 d77ad42a99015f828ef90f71e5f5bdbe8e4c122a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 19:31:49 +0100 Subject: [PATCH 8/9] fix: improve handling of unavailable storages Signed-off-by: Robin Appelman --- apps/files_external/lib/Lib/StorageConfig.php | 15 ++++++++++++ .../lib/Service/MountCacheService.php | 24 ++++++++++++------- lib/private/Files/Config/UserMountCache.php | 1 + 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index 3111456ac36b3..04ed47336d4e4 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -440,4 +440,19 @@ 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/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php index 59edc62f9f2bf..1ffbf5b3854ad 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; @@ -132,16 +132,20 @@ public function registerUpdatedStorage(StorageConfig $oldStorage, StorageConfig } 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())) { + if ($cachedEntry = $this->storageRootCache->get($userStorage->getId())) { 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; } @@ -163,10 +167,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 e16d968c9894bfd4f92a09ab447e9949d7cee64d Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 18 Dec 2025 19:40:14 +0100 Subject: [PATCH 9/9] 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 1ffbf5b3854ad..66cc609c71a03 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->addUser($event->getUser()); } + if ($event instanceof PostLoginEvent) { + $this->onLogin($event->getUser()); + } } @@ -221,4 +225,14 @@ private function addUser(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); + } + } }