mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
Merge pull request #56499 from nextcloud/authoritative-mount-provider-files_external
Add api for authoritative mount providers and implement it for files_external
This commit is contained in:
commit
ae74dbef2b
29 changed files with 786 additions and 45 deletions
|
|
@ -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',
|
||||
|
|
@ -116,6 +120,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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -131,6 +135,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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -41,19 +44,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
|
||||
|
|
@ -74,8 +80,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,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);
|
||||
|
|
|
|||
|
|
@ -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
Normal file
24
apps/files_external/lib/Event/StorageCreatedEvent.php
Normal file
|
|
@ -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
Normal file
24
apps/files_external/lib/Event/StorageDeletedEvent.php
Normal file
|
|
@ -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
Normal file
29
apps/files_external/lib/Event/StorageUpdatedEvent.php
Normal file
|
|
@ -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
Normal file
114
apps/files_external/lib/Lib/ApplicableHelper.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Normal file
205
apps/files_external/lib/Service/MountCacheService.php
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,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
Normal file
170
apps/files_external/tests/ApplicableHelperTest.php
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -419,6 +419,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',
|
||||
|
|
|
|||
|
|
@ -460,6 +460,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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Normal file
18
lib/public/Files/Config/IAuthoritativeMountProvider.php
Normal file
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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…
Reference in a new issue