Robin Appelman 3 days ago
committed by GitHub
parent
commit
e37a40bb2a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      apps/files_external/composer/composer/autoload_classmap.php
  2. 5
      apps/files_external/composer/composer/autoload_static.php
  3. 23
      apps/files_external/lib/AppInfo/Application.php
  4. 17
      apps/files_external/lib/Config/ConfigAdapter.php
  5. 6
      apps/files_external/lib/Config/UserContext.php
  6. 24
      apps/files_external/lib/Event/StorageCreatedEvent.php
  7. 24
      apps/files_external/lib/Event/StorageDeletedEvent.php
  8. 29
      apps/files_external/lib/Event/StorageUpdatedEvent.php
  9. 114
      apps/files_external/lib/Lib/ApplicableHelper.php
  10. 10
      apps/files_external/lib/Lib/StorageConfig.php
  11. 49
      apps/files_external/lib/Service/DBConfigService.php
  12. 35
      apps/files_external/lib/Service/GlobalStoragesService.php
  13. 205
      apps/files_external/lib/Service/MountCacheService.php
  14. 16
      apps/files_external/lib/Service/StoragesService.php
  15. 4
      apps/files_external/lib/Service/UserGlobalStoragesService.php
  16. 8
      apps/files_external/lib/Service/UserStoragesService.php
  17. 170
      apps/files_external/tests/ApplicableHelperTest.php
  18. 2
      apps/files_external/tests/Service/GlobalStoragesServiceTest.php
  19. 2
      apps/files_external/tests/Service/StoragesServiceTestCase.php
  20. 1
      apps/files_external/tests/Service/UserGlobalStoragesServiceTest.php
  21. 4
      apps/files_external/tests/Service/UserStoragesServiceTest.php
  22. 6
      cypress/e2e/files_external/files-external-failed.cy.ts
  23. 1
      lib/composer/composer/autoload_classmap.php
  24. 1
      lib/composer/composer/autoload_static.php
  25. 31
      lib/private/Files/Config/UserMountCache.php
  26. 2
      lib/private/User/Manager.php
  27. 18
      lib/public/Files/Config/IAuthoritativeMountProvider.php
  28. 15
      lib/public/Files/Config/IUserMountCache.php
  29. 4
      lib/public/IUserManager.php

5
apps/files_external/composer/composer/autoload_classmap.php

@ -37,6 +37,10 @@ return array(
'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 @@ return array(
'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',

5
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',

23
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;
@ -42,19 +45,22 @@ use OCA\Files_External\Lib\Config\IAuthMechanismProvider;
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 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
$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);
}

17
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);

6
apps/files_external/lib/Config/UserContext.php

@ -43,8 +43,10 @@ class UserContext {
}
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) {
}

24
apps/files_external/lib/Event/StorageCreatedEvent.php

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
* 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;
}
}

24
apps/files_external/lib/Event/StorageDeletedEvent.php

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
* 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;
}
}

29
apps/files_external/lib/Event/StorageUpdatedEvent.php

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
* 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;
}
}

114
apps/files_external/lib/Lib/ApplicableHelper.php

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_External\Lib;
use OC\User\LazyUser;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
class ApplicableHelper {
public function __construct(
private readonly IUserManager $userManager,
private readonly IGroupManager $groupManager,
) {
}
/**
* Get all users that have access to a storage
*
* @return \Iterator<string, IUser>
*/
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<IUser>
*/
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;
}
}
}
}
}
}
}

10
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,13 @@ class StorageConfig implements \JsonSerializable {
}
}
}
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;
}
}

49
apps/files_external/lib/Service/DBConfigService.php

@ -15,6 +15,9 @@ use OCP\Security\ICrypto;
/**
* 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<ApplicableConfig>, config: array, options: array, ...<string, mixed>}
*/
class DBConfigService {
public const MOUNT_TYPE_ADMIN = 1;
@ -80,6 +83,39 @@ class DBConfigService {
return $this->getMountsFromQuery($query);
}
/**
* @param list<string> $groupIds
* @return list<StorageConfigData>
*/
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<StorageConfigData>
*/
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 @@ class DBConfigService {
$query->executeStatement();
}
private function getMountsFromQuery(IQueryBuilder $query) {
/**
* @return list<StorageConfigData>
*/
private function getMountsFromQuery(IQueryBuilder $query): array {
$result = $query->executeQuery();
$mounts = $result->fetchAllAssociative();
$uniqueMounts = [];
@ -413,9 +452,9 @@ class DBConfigService {
* @param string $table
* @param string[] $fields
* @param int[] $mountIds
* @return array [$mountId => [['field1' => $value1, ...], ...], ...]
* @return array<int, list<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 @@ class DBConfigService {
/**
* @param int[] $mountIds
* @return array [$id => [['type' => $type, 'value' => $value], ...], ...]
* @return array<int, list<ApplicableConfig>> [$id => [['type' => $type, 'value' => $value], ...], ...]
*/
public function getApplicableForMounts($mountIds) {
public function getApplicableForMounts(array $mountIds): array {
return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
}

35
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 @@ class GlobalStoragesService extends StoragesService {
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 @@ class GlobalStoragesService extends StoragesService {
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(...));
}
}

205
apps/files_external/lib/Service/MountCacheService.php

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
* 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<StorageCreatedEvent|StorageDeletedEvent|StorageUpdatedEvent|BeforeGroupDeletedEvent|UserCreatedEvent|UserAddedEvent|UserRemovedEvent|PostLoginEvent|Event>
*/
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);
}
}
}

16
apps/files_external/lib/Service/StoragesService.php

@ -12,6 +12,8 @@ use OC\Files\Filesystem;
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\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;
@ -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 @@ abstract class StoragesService {
// 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 @@ 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);
@ -455,6 +446,7 @@ abstract class StoragesService {
$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

4
apps/files_external/lib/Service/UserGlobalStoragesService.php

@ -9,7 +9,6 @@ namespace OCA\Files_External\Service;
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 @@ 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;
}

8
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 @@ 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() {
@ -72,6 +72,8 @@ class UserStoragesService extends StoragesService {
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);
}

170
apps/files_external/tests/ApplicableHelperTest.php

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_External\Tests;
use OCA\Files_External\Lib\ApplicableHelper;
use OCA\Files_External\Lib\StorageConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ApplicableHelperTest extends TestCase {
private IUserManager|MockObject $userManager;
private IGroupManager|MockObject $groupManager;
/** @var list<string> */
private array $users = [];
/** @var array<string, list<string>> */
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);
}
}

2
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 {

2
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);

1
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,
);

4
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() {

6
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)

1
lib/composer/composer/autoload_classmap.php

@ -418,6 +418,7 @@ return array(
'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',

1
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',

31
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 @@ class UserMountCache implements IUserMountCache {
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;
}
}
}
}

2
lib/private/User/Manager.php

@ -826,7 +826,7 @@ class Manager extends PublicEmitter implements IUserManager {
foreach ($this->backends as $backend) {
if ($backend->userExists($userId)) {
$user = new LazyUser($userId, $this, null, $backend);
yield $user;
yield $userId => $user;
break;
}
}

18
lib/public/Files/Config/IAuthoritativeMountProvider.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
* 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 {
}

15
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 @@ interface IUserMountCache {
* @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;
}

4
lib/public/IUserManager.php

@ -240,9 +240,11 @@ interface IUserManager {
* 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<IUser> list of IUser object
* @return \Iterator<string, IUser> list of IUser object
* @since 32.0.0
*/
public function getSeenUsers(int $offset = 0, ?int $limit = null): \Iterator;

Loading…
Cancel
Save