From 5565cdb39035317a20d4ae62bb2b005650b0d7b6 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 17 Nov 2025 18:10:48 +0100 Subject: [PATCH] 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 1ad1a2ed779..08a96e6265c 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -11,6 +11,9 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent; 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\LoadAdditionalListener; use OCA\Files_External\Listener\StorePasswordListener; use OCA\Files_External\Listener\UserDeletedListener; use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\MountCacheService; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -77,6 +81,9 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide $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 a46c0fd5c66..042c364bc67 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\MountConfig; 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 @@ use Psr\Log\LoggerInterface; /** * 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 @@ class ConfigAdapter implements IMountProvider { $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 @@ class ConfigAdapter implements IMountProvider { $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 @@ class ConfigAdapter implements IMountProvider { $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 @@ class ConfigAdapter implements IMountProvider { null, $loader, $storageConfig->getMountOptions(), - $storageConfig->getId() + $storageConfig->getId(), ); } else { return new SystemMountPoint( @@ -158,7 +163,7 @@ class ConfigAdapter implements IMountProvider { 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 2cb82d3790a..3111456ac36 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\AuthMechanism; 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 @@ class StorageConfig implements \JsonSerializable { } } } + + 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 00000000000..3e536ce9993 --- /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 119217a21bd..c61c19aa391 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\DefinitionParameter; 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 @@ abstract class StoragesService { $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 6c943247b20..2607472e969 100644 --- a/apps/files_external/lib/Service/UserGlobalStoragesService.php +++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php @@ -27,11 +27,10 @@ class UserGlobalStoragesService extends GlobalStoragesService { 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 feb26ba2a7c..21746deee58 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\Lib\StorageConfig; use OCA\Files_External\MountConfig; use OCA\Files_External\NotFoundException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\IAppConfig; use OCP\IUserSession; @@ -32,12 +31,11 @@ class UserStoragesService extends StoragesService { 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 a76005718d3..33e791930b5 100644 --- a/apps/files_external/tests/Service/GlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/GlobalStoragesServiceTest.php @@ -17,7 +17,7 @@ use OCA\Files_External\Service\GlobalStoragesService; 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 fdc086751af..5f3c60a84a9 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 @@ abstract class StoragesServiceTestCase extends \Test\TestCase { ); 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 e38835f2077..d6117570f0d 100644 --- a/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php @@ -71,7 +71,6 @@ class UserGlobalStoragesServiceTest extends GlobalStoragesServiceTest { $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 99482a9cbbe..5fe3a2eab72 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 @@ class UserStoragesServiceTest extends StoragesServiceTestCase { ->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() {