feat: make external storage mount provider authoritative

Signed-off-by: Robin Appelman <robin@icewind.nl>

# Conflicts:
#	apps/files_external/lib/AppInfo/Application.php
This commit is contained in:
Robin Appelman 2025-11-17 18:10:48 +01:00
parent 765d1af2a6
commit 5565cdb390
No known key found for this signature in database
GPG key ID: 42B69D8A64526EFB
11 changed files with 184 additions and 29 deletions

View file

@ -11,6 +11,9 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\Files_External\Config\UserPlaceholderHandler;
use OCA\Files_External\ConfigLexicon;
use OCA\Files_External\Event\StorageCreatedEvent;
use OCA\Files_External\Event\StorageDeletedEvent;
use OCA\Files_External\Event\StorageUpdatedEvent;
use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
use OCA\Files_External\Lib\Auth\Builtin;
use OCA\Files_External\Lib\Auth\NullMechanism;
@ -45,6 +48,7 @@ use OCA\Files_External\Listener\LoadAdditionalListener;
use OCA\Files_External\Listener\StorePasswordListener;
use OCA\Files_External\Listener\UserDeletedListener;
use OCA\Files_External\Service\BackendService;
use OCA\Files_External\Service\MountCacheService;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@ -77,6 +81,9 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
$context->registerEventListener(UserLoggedInEvent::class, StorePasswordListener::class);
$context->registerEventListener(PasswordUpdatedEvent::class, StorePasswordListener::class);
$context->registerEventListener(StorageCreatedEvent::class, MountCacheService::class);
$context->registerEventListener(StorageDeletedEvent::class, MountCacheService::class);
$context->registerEventListener(StorageUpdatedEvent::class, MountCacheService::class);
$context->registerConfigLexicon(ConfigLexicon::class);
}

View file

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

View file

@ -12,6 +12,7 @@ use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Auth\IUserProvided;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\ResponseDefinitions;
use OCP\IUser;
/**
* External storage configuration
@ -435,4 +436,8 @@ class StorageConfig implements \JsonSerializable {
}
}
}
public function getMountPointForUser(IUser $user): string {
return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/';
}
}

View file

@ -0,0 +1,156 @@
<?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\User\LazyUser;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\Files_External\Event\StorageCreatedEvent;
use OCA\Files_External\Event\StorageDeletedEvent;
use OCA\Files_External\Event\StorageUpdatedEvent;
use OCA\Files_External\Lib\StorageConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\Event as T;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\IMimeTypeLoader;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
/**
* Listens to config events and update the mounts for the applicable users
*
* @template-implements IEventListener<StorageCreatedEvent|StorageDeletedEvent|StorageUpdatedEvent|Event>
*/
class MountCacheService implements IEventListener {
public function __construct(
private readonly IUserMountCache $userMountCache,
private readonly ConfigAdapter $configAdapter,
private readonly IUserManager $userManager,
private readonly IGroupManager $groupManager,
) {
}
public function handle(Event $event): void {
if ($event instanceof StorageCreatedEvent) {
$this->handleAddedStorage($event->getNewConfig());
}
if ($event instanceof StorageDeletedEvent) {
$this->handleDeletedStorage($event->getOldConfig());
}
if ($event instanceof StorageUpdatedEvent) {
$this->handleUpdatedStorage($event->getOldConfig(), $event->getNewConfig());
}
}
/**
* Get all users that have access to a storage, either directly or through a group
*
* @param StorageConfig $storage
* @return \Iterator<string, IUser>
*/
private function getUsersForStorage(StorageConfig $storage): \Iterator {
$yielded = [];
if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) {
yield from $this->userManager->getSeenUsers();
}
foreach ($storage->getApplicableUsers() as $userId) {
$yielded[$userId] = true;
yield $userId => new LazyUser($userId, $this->userManager);
}
foreach ($storage->getApplicableGroups() as $groupId) {
$group = $this->groupManager->get($groupId);
if ($group !== null) {
foreach ($group->searchUsers('') as $user) {
if (!isset($yielded[$user->getUID()])) {
$yielded[$user->getUID()] = true;
yield $user->getUID() => $user;
}
}
}
}
}
public function handleDeletedStorage(StorageConfig $storage): void {
foreach ($this->getUsersForStorage($storage) as $user) {
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
}
}
public function handleAddedStorage(StorageConfig $storage): void {
foreach ($this->getUsersForStorage($storage) as $user) {
$this->registerForUser($user, $storage);
}
}
public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void {
/** @var array<string, IUser> $oldApplicable */
$oldApplicable = iterator_to_array($this->getUsersForStorage($oldStorage));
/** @var array<string, IUser> $newApplicable */
$newApplicable = iterator_to_array($this->getUsersForStorage($newStorage));
foreach ($oldApplicable as $oldUser) {
if (!isset($newApplicable[$oldUser->getUID()])) {
$this->userMountCache->removeMount($oldStorage->getMountPointForUser($oldUser));
}
}
foreach ($newApplicable as $newUser) {
if (!isset($oldApplicable[$newUser->getUID()])) {
$this->registerForUser($newUser, $newStorage);
}
}
}
private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry {
// todo: reuse these between users when possible
$storage = $this->configAdapter->constructStorageForUser($user, $storage);
$cache = $storage->getCache();
$entry = $cache->get('');
if ($entry) {
return $entry;
}
// create a "fake" root entry so we have a fileid so we don't have to interact with the remote service
// this will be scanned on first access
$data = [
'path' => '',
'path_hash' => md5(''),
'size' => 0,
'unencrypted_size' => 0,
'mtime' => 0,
'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE,
'parent' => -1,
'name' => '',
'storage_mtime' => 0,
'permissions' => 31,
'storage' => $cache->getNumericStorageId(),
'etag' => '',
'encrypted' => 0,
'checksum' => '',
];
$data['fileid'] = $cache->insert('', $data);
return new CacheEntry($data);
}
private function registerForUser(IUser $user, StorageConfig $storage): void {
$this->userMountCache->addMount(
$user,
$storage->getMountPointForUser($user),
$this->getCacheEntryForRoot($user, $storage),
ConfigAdapter::class,
$storage->getId(),
);
}
}

View file

@ -22,7 +22,6 @@ use OCA\Files_External\Lib\DefinitionParameter;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\NotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\InvalidateMountCacheEvent;
use OCP\Files\StorageNotAvailableException;
use OCP\IAppConfig;
@ -38,13 +37,11 @@ abstract class StoragesService {
/**
* @param BackendService $backendService
* @param DBConfigService $dbConfig
* @param IUserMountCache $userMountCache
* @param IEventDispatcher $eventDispatcher
*/
public function __construct(
protected BackendService $backendService,
protected DBConfigService $dbConfig,
protected IUserMountCache $userMountCache,
protected IEventDispatcher $eventDispatcher,
protected IAppConfig $appConfig,
) {
@ -427,15 +424,6 @@ abstract class StoragesService {
$this->triggerChangeHooks($oldStorage, $updatedStorage);
if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly
$this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage));
} else {
$storageId = $this->getStorageId($updatedStorage);
foreach ($removedUsers as $userId) {
$this->userMountCache->removeUserStorageMount($storageId, $userId);
}
}
$this->updateOverwriteHomeFolders();
return $this->getStorage($id);

View file

@ -27,11 +27,10 @@ class UserGlobalStoragesService extends GlobalStoragesService {
DBConfigService $dbConfig,
IUserSession $userSession,
protected IGroupManager $groupManager,
IUserMountCache $userMountCache,
IEventDispatcher $eventDispatcher,
IAppConfig $appConfig,
) {
parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig);
parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig);
$this->userSession = $userSession;
}

View file

@ -14,7 +14,6 @@ use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\MountConfig;
use OCA\Files_External\NotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IUserMountCache;
use OCP\IAppConfig;
use OCP\IUserSession;
@ -32,12 +31,11 @@ class UserStoragesService extends StoragesService {
BackendService $backendService,
DBConfigService $dbConfig,
IUserSession $userSession,
IUserMountCache $userMountCache,
IEventDispatcher $eventDispatcher,
IAppConfig $appConfig,
) {
$this->userSession = $userSession;
parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig);
parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig);
}
protected function readDBConfig() {

View file

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

View file

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

View file

@ -71,7 +71,6 @@ class UserGlobalStoragesServiceTest extends GlobalStoragesServiceTest {
$this->dbConfig,
$userSession,
$this->groupManager,
$this->mountCache,
$this->eventDispatcher,
$this->appConfig,
);

View file

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