diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index 61165ee67fbdc..da5e7329e0f7b 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -37,6 +37,10 @@ '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\\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', @@ -117,6 +121,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..adba91ec55389 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -52,6 +52,10 @@ 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\\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', @@ -132,6 +136,7 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Service\\GlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/GlobalStoragesService.php', 'OCA\\Files_External\\Service\\ImportLegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/ImportLegacyStoragesService.php', 'OCA\\Files_External\\Service\\LegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/LegacyStoragesService.php', + 'OCA\\Files_External\\Service\\MountCacheService' => __DIR__ . '/..' . '/../lib/Service/MountCacheService.php', 'OCA\\Files_External\\Service\\StoragesService' => __DIR__ . '/..' . '/../lib/Service/StoragesService.php', 'OCA\\Files_External\\Service\\UserGlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserGlobalStoragesService.php', 'OCA\\Files_External\\Service\\UserStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserStoragesService.php', diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 1ad1a2ed779c7..924b72bf9822d 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -11,6 +11,9 @@ use OCA\Files_External\Config\ConfigAdapter; use OCA\Files_External\Config\UserPlaceholderHandler; use OCA\Files_External\ConfigLexicon; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; +use OCA\Files_External\Event\StorageUpdatedEvent; use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; use OCA\Files_External\Lib\Auth\Builtin; use OCA\Files_External\Lib\Auth\NullMechanism; @@ -42,19 +45,22 @@ use OCA\Files_External\Lib\Config\IBackendProvider; use OCA\Files_External\Listener\GroupDeletedListener; use OCA\Files_External\Listener\LoadAdditionalListener; -use OCA\Files_External\Listener\StorePasswordListener; use OCA\Files_External\Listener\UserDeletedListener; use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\MountCacheService; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\QueryException; use OCP\Files\Config\IMountProviderCollection; +use OCP\Group\Events\BeforeGroupDeletedEvent; use OCP\Group\Events\GroupDeletedEvent; -use OCP\User\Events\PasswordUpdatedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\UserCreatedEvent; use OCP\User\Events\UserDeletedEvent; -use OCP\User\Events\UserLoggedInEvent; /** * @package OCA\Files_External\AppInfo @@ -75,8 +81,15 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); - $context->registerEventListener(UserLoggedInEvent::class, StorePasswordListener::class); - $context->registerEventListener(PasswordUpdatedEvent::class, StorePasswordListener::class); + $context->registerEventListener(StorageCreatedEvent::class, MountCacheService::class); + $context->registerEventListener(StorageDeletedEvent::class, MountCacheService::class); + $context->registerEventListener(StorageUpdatedEvent::class, MountCacheService::class); + $context->registerEventListener(BeforeGroupDeletedEvent::class, MountCacheService::class); + $context->registerEventListener(UserCreatedEvent::class, MountCacheService::class); + $context->registerEventListener(UserAddedEvent::class, MountCacheService::class); + $context->registerEventListener(UserRemovedEvent::class, MountCacheService::class); + $context->registerEventListener(PostLoginEvent::class, MountCacheService::class); + $context->registerConfigLexicon(ConfigLexicon::class); } diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php index a46c0fd5c6616..042c364bc6718 100644 --- a/apps/files_external/lib/Config/ConfigAdapter.php +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -17,6 +17,7 @@ use OCA\Files_External\Service\UserGlobalStoragesService; use OCA\Files_External\Service\UserStoragesService; use OCP\AppFramework\QueryException; +use OCP\Files\Config\IAuthoritativeMountProvider; use OCP\Files\Config\IMountProvider; use OCP\Files\Mount\IMountPoint; use OCP\Files\ObjectStore\IObjectStore; @@ -32,7 +33,7 @@ /** * Make the old files_external config work with the new public mount config api */ -class ConfigAdapter implements IMountProvider { +class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider { public function __construct( private UserStoragesService $userStoragesService, private UserGlobalStoragesService $userGlobalStoragesService, @@ -73,6 +74,11 @@ private function prepareStorageConfig(StorageConfig &$storage, IUser $user): voi $storage->getBackend()->manipulateStorageConfig($storage, $user); } + public function constructStorageForUser(IUser $user, StorageConfig $storage) { + $this->prepareStorageConfig($storage, $user); + return $this->constructStorage($storage); + } + /** * Construct the storage implementation * @@ -105,8 +111,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $storages = array_map(function (StorageConfig $storageConfig) use ($user) { try { - $this->prepareStorageConfig($storageConfig, $user); - return $this->constructStorage($storageConfig); + return $this->constructStorageForUser($user, $storageConfig); } catch (\Exception $e) { // propagate exception into filesystem return new FailedStorage(['exception' => $e]); @@ -123,7 +128,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $availability = $storage->getAvailability(); if (!$availability['available'] && !Availability::shouldRecheck($availability)) { $storage = new FailedStorage([ - 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available') + 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available'), ]); } } catch (\Exception $e) { @@ -148,7 +153,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { null, $loader, $storageConfig->getMountOptions(), - $storageConfig->getId() + $storageConfig->getId(), ); } else { return new SystemMountPoint( @@ -158,7 +163,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { null, $loader, $storageConfig->getMountOptions(), - $storageConfig->getId() + $storageConfig->getId(), ); } }, $storageConfigs, $availableStorages); diff --git a/apps/files_external/lib/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) { } diff --git a/apps/files_external/lib/Event/StorageCreatedEvent.php b/apps/files_external/lib/Event/StorageCreatedEvent.php new file mode 100644 index 0000000000000..3b2a6424df368 --- /dev/null +++ b/apps/files_external/lib/Event/StorageCreatedEvent.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageCreatedEvent extends Event { + public function __construct( + private readonly StorageConfig $newConfig, + ) { + parent::__construct(); + } + + public function getNewConfig(): StorageConfig { + return $this->newConfig; + } +} diff --git a/apps/files_external/lib/Event/StorageDeletedEvent.php b/apps/files_external/lib/Event/StorageDeletedEvent.php new file mode 100644 index 0000000000000..9be6a61fe6de6 --- /dev/null +++ b/apps/files_external/lib/Event/StorageDeletedEvent.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageDeletedEvent extends Event { + public function __construct( + private readonly StorageConfig $oldConfig, + ) { + parent::__construct(); + } + + public function getOldConfig(): StorageConfig { + return $this->oldConfig; + } +} diff --git a/apps/files_external/lib/Event/StorageUpdatedEvent.php b/apps/files_external/lib/Event/StorageUpdatedEvent.php new file mode 100644 index 0000000000000..4cc5e5177fafb --- /dev/null +++ b/apps/files_external/lib/Event/StorageUpdatedEvent.php @@ -0,0 +1,29 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Event; + +use OCA\Files_External\Lib\StorageConfig; +use OCP\EventDispatcher\Event; + +class StorageUpdatedEvent extends Event { + public function __construct( + private readonly StorageConfig $oldConfig, + private readonly StorageConfig $newConfig, + ) { + parent::__construct(); + } + + public function getOldConfig(): StorageConfig { + return $this->oldConfig; + } + + public function getNewConfig(): StorageConfig { + return $this->newConfig; + } +} diff --git a/apps/files_external/lib/Lib/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 2cb82d3790ae3..133c5392ec772 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,13 @@ protected function formatStorageForUI(): void { } } } + + public function getMountPointForUser(IUser $user): string { + return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/'; + } + + 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 f08f3442a478c..e9d4cbc89e763 100644 --- a/apps/files_external/lib/Service/DBConfigService.php +++ b/apps/files_external/lib/Service/DBConfigService.php @@ -15,6 +15,9 @@ /** * Stores the mount config in the database + * + * @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; @@ -80,6 +83,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 +412,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 = []; @@ -413,9 +452,9 @@ private function getMountsFromQuery(IQueryBuilder $query) { * @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 []; } @@ -447,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/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php index 5b1a9f41e4824..2694058c96845 100644 --- a/apps/files_external/lib/Service/GlobalStoragesService.php +++ b/apps/files_external/lib/Service/GlobalStoragesService.php @@ -8,8 +8,12 @@ namespace OCA\Files_External\Service; use OC\Files\Filesystem; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; +use OCA\Files_External\Event\StorageUpdatedEvent; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\MountConfig; +use OCP\IGroup; /** * Service class to manage global external storage @@ -62,9 +66,13 @@ protected function triggerHooks(StorageConfig $storage, $signal) { protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { // if mount point changed, it's like a deletion + creation if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage)); + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); return; + } else { + $this->eventDispatcher->dispatchTyped(new StorageUpdatedEvent($oldStorage, $newStorage)); } $userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers()); @@ -162,4 +170,31 @@ public function getStorageForAllUsers() { return array_combine($keys, $configs); } + + /** + * Gets all storages for the group, not including any global storages + * @return StorageConfig[] + */ + public function getAllStoragesForGroup(IGroup $group): array { + $mounts = $this->dbConfig->getMountsForGroups([$group->getGID()]); + $configs = array_map($this->getStorageConfigFromDBMount(...), $mounts); + $configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig); + $keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs); + + $storages = array_combine($keys, $configs); + return array_filter($storages, $this->validateStorage(...)); + } + + /** + * @return StorageConfig[] + */ + public function getAllGlobalStorages(): array { + $mounts = $this->dbConfig->getGlobalMounts(); + + $configs = array_map($this->getStorageConfigFromDBMount(...), $mounts); + $configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig); + $keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs); + $storages = array_combine($keys, $configs); + return array_filter($storages, $this->validateStorage(...)); + } } diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php new file mode 100644 index 0000000000000..2be17b9d59863 --- /dev/null +++ b/apps/files_external/lib/Service/MountCacheService.php @@ -0,0 +1,205 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Service; + +use OC\Files\Cache\CacheEntry; +use OC\Files\Storage\FailedStorage; +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; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Config\IUserMountCache; +use OCP\Group\Events\BeforeGroupDeletedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\IGroup; +use OCP\IUser; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\UserCreatedEvent; + +/** + * Listens to config events and update the mounts for the applicable users + * + * @template-implements IEventListener + */ +class MountCacheService implements IEventListener { + private CappedMemoryCache $storageRootCache; + + public function __construct( + private readonly IUserMountCache $userMountCache, + private readonly ConfigAdapter $configAdapter, + private readonly GlobalStoragesService $storagesService, + private readonly ApplicableHelper $applicableHelper, + ) { + $this->storageRootCache = new CappedMemoryCache(); + } + + 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()); + } + 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()); + } + if ($event instanceof PostLoginEvent) { + $this->onLogin($event->getUser()); + } + } + + public function handleDeletedStorage(StorageConfig $storage): void { + foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) { + $this->userMountCache->removeMount($storage->getMountPointForUser($user)); + } + } + + public function handleAddedStorage(StorageConfig $storage): void { + foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) { + $this->registerForUser($user, $storage); + } + } + + public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void { + foreach ($this->applicableHelper->diffApplicable($oldStorage, $newStorage) as $user) { + $this->userMountCache->removeMount($oldStorage->getMountPointForUser($user)); + } + foreach ($this->applicableHelper->diffApplicable($newStorage, $oldStorage) as $user) { + $this->registerForUser($user, $newStorage); + } + } + + private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry { + try { + $userStorage = $this->configAdapter->constructStorageForUser($user, clone $storage); + } catch (\Exception $e) { + $userStorage = new FailedStorage(['exception' => $e]); + } + + $cachedEntry = $this->storageRootCache->get($userStorage->getId()); + if ($cachedEntry !== null) { + return $cachedEntry; + } + + $cache = $userStorage->getCache(); + $entry = $cache->get(''); + if ($entry && $entry->getId() !== -1) { + $this->storageRootCache->set($userStorage->getId(), $entry); + return $entry; + } + + // create a "fake" root entry so we have a fileid so we don't have to interact with the remote service + // this will be scanned on first access + $data = [ + 'path' => '', + 'path_hash' => md5(''), + 'size' => 0, + 'unencrypted_size' => 0, + 'mtime' => 0, + 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE, + 'parent' => -1, + 'name' => '', + 'storage_mtime' => 0, + 'permissions' => 31, + 'storage' => $cache->getNumericStorageId(), + 'etag' => '', + 'encrypted' => 0, + 'checksum' => '', + ]; + if ($cache->getNumericStorageId() !== -1) { + $data['fileid'] = $cache->insert('', $data); + } else { + $data['fileid'] = -1; + } + + $entry = new CacheEntry($data); + $this->storageRootCache->set($userStorage->getId(), $entry); + return $entry; + } + + private function registerForUser(IUser $user, StorageConfig $storage): void { + $this->userMountCache->addMount( + $user, + $storage->getMountPointForUser($user), + $this->getCacheEntryForRoot($user, $storage), + ConfigAdapter::class, + $storage->getId(), + ); + } + + private function handleUserRemoved(IGroup $group, IUser $user): void { + $storages = $this->storagesService->getAllStoragesForGroup($group); + foreach ($storages as $storage) { + if (!$this->applicableHelper->isApplicableForUser($storage, $user)) { + $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) { + $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)); + } + } + } + + private function handleUserCreated(IUser $user): void { + $storages = $this->storagesService->getAllGlobalStorages(); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } + + /** + * Since storage config can rely on login credentials, we might need to update the config + */ + private function onLogin(IUser $user): void { + $storages = $this->storagesService->getAllGlobalStorages(); + foreach ($storages as $storage) { + $this->registerForUser($user, $storage); + } + } +} diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php index 7b1b7ba2dc1ff..c61c19aa3915b 100644 --- a/apps/files_external/lib/Service/StoragesService.php +++ b/apps/files_external/lib/Service/StoragesService.php @@ -12,6 +12,8 @@ use OCA\Files\AppInfo\Application as FilesApplication; use OCA\Files\ConfigLexicon; use OCA\Files_External\AppInfo\Application; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; use OCA\Files_External\Lib\Auth\AuthMechanism; use OCA\Files_External\Lib\Auth\InvalidAuth; use OCA\Files_External\Lib\Backend\Backend; @@ -20,7 +22,6 @@ use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\NotFoundException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\StorageNotAvailableException; use OCP\IAppConfig; @@ -36,13 +37,11 @@ abstract class StoragesService { /** * @param BackendService $backendService * @param DBConfigService $dbConfig - * @param IUserMountCache $userMountCache * @param IEventDispatcher $eventDispatcher */ public function __construct( protected BackendService $backendService, protected DBConfigService $dbConfig, - protected IUserMountCache $userMountCache, protected IEventDispatcher $eventDispatcher, protected IAppConfig $appConfig, ) { @@ -244,6 +243,7 @@ public function addStorage(StorageConfig $newStorage) { // add new storage $allStorages[$configId] = $newStorage; + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); $newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS); @@ -424,15 +424,6 @@ public function updateStorage(StorageConfig $updatedStorage) { $this->triggerChangeHooks($oldStorage, $updatedStorage); - if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly - $this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage)); - } else { - $storageId = $this->getStorageId($updatedStorage); - foreach ($removedUsers as $userId) { - $this->userMountCache->removeUserStorageMount($storageId, $userId); - } - } - $this->updateOverwriteHomeFolders(); return $this->getStorage($id); @@ -455,6 +446,7 @@ public function removeStorage(int $id) { $this->dbConfig->removeMount($id); $deletedStorage = $this->getStorageConfigFromDBMount($existingMount); + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($deletedStorage)); $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount); // delete oc_storages entries and oc_filecache diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php index 6c943247b2069..c2b22344b3b0f 100644 --- a/apps/files_external/lib/Service/UserGlobalStoragesService.php +++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php @@ -9,7 +9,6 @@ use OCA\Files_External\Lib\StorageConfig; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IUser; @@ -27,11 +26,10 @@ public function __construct( DBConfigService $dbConfig, IUserSession $userSession, protected IGroupManager $groupManager, - IUserMountCache $userMountCache, IEventDispatcher $eventDispatcher, IAppConfig $appConfig, ) { - parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig); + parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig); $this->userSession = $userSession; } diff --git a/apps/files_external/lib/Service/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php index bd8dd2d348c1e..21746deee5821 100644 --- a/apps/files_external/lib/Service/UserStoragesService.php +++ b/apps/files_external/lib/Service/UserStoragesService.php @@ -8,11 +8,12 @@ namespace OCA\Files_External\Service; use OC\Files\Filesystem; +use OCA\Files_External\Event\StorageCreatedEvent; +use OCA\Files_External\Event\StorageDeletedEvent; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\MountConfig; use OCA\Files_External\NotFoundException; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\IAppConfig; use OCP\IUserSession; @@ -30,12 +31,11 @@ public function __construct( BackendService $backendService, DBConfigService $dbConfig, IUserSession $userSession, - IUserMountCache $userMountCache, IEventDispatcher $eventDispatcher, IAppConfig $appConfig, ) { $this->userSession = $userSession; - parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig); + parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig); } protected function readDBConfig() { @@ -72,6 +72,8 @@ protected function triggerHooks(StorageConfig $storage, $signal) { protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { // if mount point changed, it's like a deletion + creation if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage)); + $this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage)); $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); $this->triggerHooks($newStorage, Filesystem::signal_create_mount); } diff --git a/apps/files_external/tests/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); + } +} diff --git a/apps/files_external/tests/Service/GlobalStoragesServiceTest.php b/apps/files_external/tests/Service/GlobalStoragesServiceTest.php index a76005718d37a..33e791930b5c5 100644 --- a/apps/files_external/tests/Service/GlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/GlobalStoragesServiceTest.php @@ -17,7 +17,7 @@ class GlobalStoragesServiceTest extends StoragesServiceTestCase { protected function setUp(): void { parent::setUp(); - $this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->eventDispatcher, $this->appConfig); } protected function tearDown(): void { diff --git a/apps/files_external/tests/Service/StoragesServiceTestCase.php b/apps/files_external/tests/Service/StoragesServiceTestCase.php index fdc086751af7d..5f3c60a84a921 100644 --- a/apps/files_external/tests/Service/StoragesServiceTestCase.php +++ b/apps/files_external/tests/Service/StoragesServiceTestCase.php @@ -60,7 +60,6 @@ abstract class StoragesServiceTestCase extends \Test\TestCase { protected string $dataDir; protected CleaningDBConfig $dbConfig; protected static array $hookCalls; - protected IUserMountCache&MockObject $mountCache; protected IEventDispatcher&MockObject $eventDispatcher; protected IAppConfig&MockObject $appConfig; @@ -75,7 +74,6 @@ protected function setUp(): void { ); MountConfig::$skipTest = true; - $this->mountCache = $this->createMock(IUserMountCache::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->appConfig = $this->createMock(IAppConfig::class); diff --git a/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php b/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php index e38835f2077a1..d6117570f0d43 100644 --- a/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php +++ b/apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php @@ -71,7 +71,6 @@ protected function setUp(): void { $this->dbConfig, $userSession, $this->groupManager, - $this->mountCache, $this->eventDispatcher, $this->appConfig, ); diff --git a/apps/files_external/tests/Service/UserStoragesServiceTest.php b/apps/files_external/tests/Service/UserStoragesServiceTest.php index 99482a9cbbeb1..5fe3a2eab724b 100644 --- a/apps/files_external/tests/Service/UserStoragesServiceTest.php +++ b/apps/files_external/tests/Service/UserStoragesServiceTest.php @@ -34,7 +34,7 @@ class UserStoragesServiceTest extends StoragesServiceTestCase { protected function setUp(): void { parent::setUp(); - $this->globalStoragesService = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->globalStoragesService = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->eventDispatcher, $this->appConfig); $this->userId = $this->getUniqueID('user_'); $this->createUser($this->userId, $this->userId); @@ -47,7 +47,7 @@ protected function setUp(): void { ->method('getUser') ->willReturn($this->user); - $this->service = new UserStoragesService($this->backendService, $this->dbConfig, $userSession, $this->mountCache, $this->eventDispatcher, $this->appConfig); + $this->service = new UserStoragesService($this->backendService, $this->dbConfig, $userSession, $this->eventDispatcher, $this->appConfig); } private function makeTestStorageData() { diff --git a/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) 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/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 7079c8a295730..238ee959a0d11 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -7,12 +7,14 @@ */ namespace OC\Files\Config; +use OC\DB\Exceptions\DbalException; use OC\User\LazyUser; use OCP\Cache\CappedMemoryCache; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Config\Event\UserMountAddedEvent; use OCP\Files\Config\Event\UserMountRemovedEvent; use OCP\Files\Config\Event\UserMountUpdatedEvent; @@ -524,4 +526,33 @@ public function getMountsInPath(IUser $user, string $path): array { return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path); }); } + + public function removeMount(string $mountPoint): void { + $query = $this->connection->getQueryBuilder(); + $query->delete('mounts') + ->where($query->expr()->eq('mount_point', $query->createNamedParameter($mountPoint))); + $query->executeStatement(); + } + + public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void { + $query = $this->connection->getQueryBuilder(); + $query->insert('mounts') + ->values([ + 'storage_id' => $query->createNamedParameter($rootCacheEntry->getStorageId()), + 'root_id' => $query->createNamedParameter($rootCacheEntry->getId()), + 'user_id' => $query->createNamedParameter($user->getUID()), + 'mount_point' => $query->createNamedParameter($mountPoint), + 'mount_point_hash' => $query->createNamedParameter(hash('xxh128', $mountPoint)), + 'mount_id' => $query->createNamedParameter($mountId), + 'mount_provider_class' => $query->createNamedParameter($mountProvider) + ]); + + try { + $query->executeStatement(); + } catch (DbalException $e) { + if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + } + } } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 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/Files/Config/IAuthoritativeMountProvider.php b/lib/public/Files/Config/IAuthoritativeMountProvider.php new file mode 100644 index 0000000000000..d0a6b69d8d874 --- /dev/null +++ b/lib/public/Files/Config/IAuthoritativeMountProvider.php @@ -0,0 +1,18 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Files\Config; + +/** + * Marks a mount provider as being authoritative, meaning that it will proactively update the cached mounts + * + * @since 33.0.0 + */ +interface IAuthoritativeMountProvider { + +} diff --git a/lib/public/Files/Config/IUserMountCache.php b/lib/public/Files/Config/IUserMountCache.php index a5b68ded66d15..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; } 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;