mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
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:
parent
765d1af2a6
commit
5565cdb390
11 changed files with 184 additions and 29 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, '/') . '/';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
156
apps/files_external/lib/Service/MountCacheService.php
Normal file
156
apps/files_external/lib/Service/MountCacheService.php
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ class UserGlobalStoragesServiceTest extends GlobalStoragesServiceTest {
|
|||
$this->dbConfig,
|
||||
$userSession,
|
||||
$this->groupManager,
|
||||
$this->mountCache,
|
||||
$this->eventDispatcher,
|
||||
$this->appConfig,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue