Merge pull request #57497 from nextcloud/carl/external-storage-ipartialmountpoint

feat(external-storage): Implement IPartialMountPoint
This commit is contained in:
Andy Scherzinger 2026-01-29 13:14:12 +01:00 committed by GitHub
commit f2373f1102
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 483 additions and 175 deletions

View file

@ -18,6 +18,7 @@ use OCA\Files_External\Service\UserGlobalStoragesService;
use OCA\Files_External\Service\UserStoragesService;
use OCP\Files\Config\IAuthoritativeMountProvider;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IPartialMountProvider;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\Files\Storage\IConstructableStorage;
@ -26,6 +27,7 @@ use OCP\Files\Storage\IStorageFactory;
use OCP\Files\StorageNotAvailableException;
use OCP\IUser;
use OCP\Server;
use Override;
use Psr\Clock\ClockInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;
@ -33,7 +35,7 @@ use Psr\Log\LoggerInterface;
/**
* Make the old files_external config work with the new public mount config api
*/
class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider, IPartialMountProvider {
public function __construct(
private UserStoragesService $userStoragesService,
private UserGlobalStoragesService $userGlobalStoragesService,
@ -81,8 +83,6 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
/**
* Construct the storage implementation
*
* @param StorageConfig $storageConfig
*/
private function constructStorage(StorageConfig $storageConfig): IStorage {
$class = $storageConfig->getBackend()->getStorageClass();
@ -99,17 +99,12 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
}
/**
* Get all mountpoints applicable for the user
*
* @return IMountPoint[]
* @param list<StorageConfig> $storageConfigs
* @return array
* @throws ContainerExceptionInterface
*/
public function getMountsForUser(IUser $user, IStorageFactory $loader) {
$this->userStoragesService->setUser($user);
$this->userGlobalStoragesService->setUser($user);
$storageConfigs = $this->userGlobalStoragesService->getAllStoragesForUser();
$storages = array_map(function (StorageConfig $storageConfig) use ($user) {
private function getAvailableStorages(array $storageConfigs, IUser $user): array {
$storages = array_map(function (StorageConfig $storageConfig) use ($user): IStorage {
try {
return $this->constructStorageForUser($user, $storageConfig);
} catch (\Exception $e) {
@ -123,7 +118,7 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
return $storage->getId();
}, $storages));
$availableStorages = array_map(function (IStorage $storage, StorageConfig $storageConfig): IStorage {
return array_map(function (IStorage $storage, StorageConfig $storageConfig): IStorage {
try {
$availability = $storage->getAvailability();
if (!$availability['available'] && !Availability::shouldRecheck($availability)) {
@ -137,35 +132,23 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
}
return $storage;
}, $storages, $storageConfigs);
}
/**
* Get all mountpoints applicable for the user
*
* @return IMountPoint[]
*/
public function getMountsForUser(IUser $user, IStorageFactory $loader): array {
$this->userStoragesService->setUser($user);
$this->userGlobalStoragesService->setUser($user);
$storageConfigs = $this->userGlobalStoragesService->getAllStoragesForUser();
$availableStorages = $this->getAvailableStorages($storageConfigs, $user);
$mounts = array_map(function (StorageConfig $storageConfig, IStorage $storage) use ($user, $loader) {
$storage->setOwner($user->getUID());
if ($storageConfig->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) {
return new PersonalMount(
$this->userStoragesService,
$storageConfig,
$storageConfig->getId(),
new KnownMtime([
'storage' => $storage,
'clock' => $this->clock,
]),
'/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(),
null,
$loader,
$storageConfig->getMountOptions(),
$storageConfig->getId(),
);
} else {
return new SystemMountPoint(
$storageConfig,
$storage,
'/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(),
null,
$loader,
$storageConfig->getMountOptions(),
$storageConfig->getId(),
);
}
$mountpoint = '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint();
return $this->storageConfigToMount($user, $mountpoint, $loader, $storage, $storageConfig);
}, $storageConfigs, $availableStorages);
$this->userStoragesService->resetUser();
@ -173,4 +156,65 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
return $mounts;
}
#[Override]
public function getMountsForPath(string $setupPathHint, bool $forChildren, array $mountProviderArgs, IStorageFactory $loader): array {
$user = $mountProviderArgs[0]->mountInfo->getUser();
if (!$forChildren) {
// override path with mount point when fetching without children
$setupPathHint = $mountProviderArgs[0]->mountInfo->getMountPoint();
}
$this->userStoragesService->setUser($user);
$this->userGlobalStoragesService->setUser($user);
$storageConfigs = $this->userGlobalStoragesService->getAllStoragesForUserWithPath($setupPathHint, $forChildren);
$availableStorages = $this->getAvailableStorages($storageConfigs, $user);
$mounts = [];
$i = 0;
foreach ($storageConfigs as $storageConfig) {
$storage = $availableStorages[$i];
$i++;
$mountPoint = '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint();
$mounts[$mountPoint] = $this->storageConfigToMount($user, $mountPoint, $loader, $storage, $storageConfig);
}
$this->userStoragesService->resetUser();
$this->userGlobalStoragesService->resetUser();
return $mounts;
}
private function storageConfigToMount(IUser $user, string $mountPoint, IStorageFactory $loader, IStorage $storage, StorageConfig $storageConfig): IMountPoint {
$storage->setOwner($user->getUID());
if ($storageConfig->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) {
return new PersonalMount(
$this->userStoragesService,
$storageConfig,
$storageConfig->getId(),
new KnownMtime([
'storage' => $storage,
'clock' => $this->clock,
]),
$mountPoint,
null,
$loader,
$storageConfig->getMountOptions(),
$storageConfig->getId()
);
} else {
return new SystemMountPoint(
$storageConfig,
$storage,
$mountPoint,
null,
$loader,
$storageConfig->getMountOptions(),
$storageConfig->getId()
);
}
}
}

View file

@ -76,10 +76,8 @@ class StorageConfig implements \JsonSerializable {
/**
* Priority
*
* @var int
*/
private $priority;
private int $priority = 100;
/**
* List of users who have access to this storage
@ -242,7 +240,7 @@ class StorageConfig implements \JsonSerializable {
*
* @return int priority
*/
public function getPriority() {
public function getPriority(): int {
return $this->priority;
}
@ -251,7 +249,7 @@ class StorageConfig implements \JsonSerializable {
*
* @param int $priority priority
*/
public function setPriority($priority) {
public function setPriority(int $priority): void {
$this->priority = $priority;
}
@ -260,7 +258,7 @@ class StorageConfig implements \JsonSerializable {
*
* @return list<string> applicable users
*/
public function getApplicableUsers() {
public function getApplicableUsers(): array {
return $this->applicableUsers;
}
@ -399,9 +397,7 @@ class StorageConfig implements \JsonSerializable {
$result['backend'] = $this->backend->getIdentifier();
$result['authMechanism'] = $this->authMechanism->getIdentifier();
$result['backendOptions'] = $this->backendOptions;
if (!is_null($this->priority)) {
$result['priority'] = $this->priority;
}
$result['priority'] = $this->priority;
if (!empty($this->applicableUsers)) {
$result['applicableUsers'] = $this->applicableUsers;
}

View file

@ -9,6 +9,7 @@
namespace OCA\Files_External\Service;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IParameter;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Security\ICrypto;
@ -17,7 +18,17 @@ 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>}
* @psalm-type ExternalMountInfo = array{
* mount_id: int,
* mount_point: string,
* storage_backend: string,
* auth_backend: string,
* priority: int,
* type: self::MOUNT_TYPE_ADMIN|self::MOUNT_TYPE_PERSONAL,
* applicable: list<ApplicableConfig>,
* config: array,
* options: array,
* }
*/
class DBConfigService {
public const MOUNT_TYPE_ADMIN = 1;
@ -35,6 +46,9 @@ class DBConfigService {
) {
}
/**
* @return ?ExternalMountInfo
*/
public function getMountById(int $mountId): ?array {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
@ -51,57 +65,88 @@ class DBConfigService {
/**
* Get all configured mounts
*
* @return array
* @return list<ExternalMountInfo>
*/
public function getAllMounts() {
public function getAllMounts(): array {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
->from('external_mounts');
return $this->getMountsFromQuery($query);
}
public function getMountsForUser($userId, $groupIds) {
$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()->orX(
$builder->expr()->andX( // global mounts
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
$builder->expr()->isNull('a.value'),
),
$builder->expr()->andX( // mounts for user
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
$builder->expr()->eq('a.value', $builder->createNamedParameter($userId)),
),
$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)),
),
));
public function getMountsForUser(string $userId, array $groupIds): array {
$builder = $this->getSelectQueryBuilder();
$builder = $builder->where($builder->expr()->orX(
$builder->expr()->andX( // global mounts
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
$builder->expr()->isNull('a.value'),
),
$builder->expr()->andX( // mounts for user
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
$builder->expr()->eq('a.value', $builder->createNamedParameter($userId)),
),
$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 $this->getMountsFromQuery($builder);
}
/**
* @param list<string> $groupIds
* @return list<StorageConfigData>
* @return list<ExternalMountInfo>
*/
public function getMountsForGroups(array $groupIds): array {
$builder = $this->getSelectQueryBuilder();
$builder = $builder->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($builder);
}
private function getSelectQueryBuilder(): IQueryBuilder {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
return $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
->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'));
}
/*
* @return list<ExternalMountInfo>
*/
public function getMountsForUserAndPath(string $userId, array $groupIds, string $path, bool $forChildren): array {
$path = str_replace('/' . $userId . '/files', '', $path);
$builder = $this->getSelectQueryBuilder();
$builder->where($builder->expr()->orX(
$builder->expr()->andX( // global mounts
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
$builder->expr()->isNull('a.value'),
$forChildren ? $builder->expr()->like('m.mount_point', $builder->createNamedParameter($this->connection->escapeLikeParameter($path) . '_%', IQueryBuilder::PARAM_STR))
: $builder->expr()->eq('m.mount_point', $builder->createNamedParameter($path, IQueryBuilder::PARAM_STR)),
),
$builder->expr()->andX( // mounts for user
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
$builder->expr()->eq('a.value', $builder->createNamedParameter($userId)),
$forChildren ? $builder->expr()->like('m.mount_point', $builder->createNamedParameter($this->connection->escapeLikeParameter($path) . '_%', IQueryBuilder::PARAM_STR))
: $builder->expr()->eq('m.mount_point', $builder->createNamedParameter($path, IQueryBuilder::PARAM_STR)),
),
$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)),
));
$forChildren ? $builder->expr()->like('m.mount_point', $builder->createNamedParameter($this->connection->escapeLikeParameter($path) . '_%', IQueryBuilder::PARAM_STR))
: $builder->expr()->eq('m.mount_point', $builder->createNamedParameter($path, IQueryBuilder::PARAM_STR)),
),
));
return $this->getMountsFromQuery($query);
return $this->getMountsFromQuery($builder);
}
/**
* @return list<StorageConfigData>
* @return list<ExternalMountInfo>
*/
public function getGlobalMounts(): array {
$builder = $this->connection->getQueryBuilder();
@ -151,9 +196,9 @@ class DBConfigService {
/**
* Get admin defined mounts
*
* @return array
* @return list<ExternalMountInfo>
*/
public function getAdminMounts() {
public function getAdminMounts(): array {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
->from('external_mounts')
@ -161,19 +206,17 @@ class DBConfigService {
return $this->getMountsFromQuery($query);
}
protected function getForQuery(IQueryBuilder $builder, $type, $value) {
$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()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
protected function getForQuery(IQueryBuilder $builder, int $type, ?string $value): IQueryBuilder {
$builder = $this->getSelectQueryBuilder();
$builder = $builder->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
if (is_null($value)) {
$query = $query->andWhere($builder->expr()->isNull('a.value'));
$builder = $builder->andWhere($builder->expr()->isNull('a.value'));
} else {
$query = $query->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value)));
$builder = $builder->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value)));
}
return $query;
return $builder;
}
/**
@ -181,9 +224,9 @@ class DBConfigService {
*
* @param int $type any of the self::APPLICABLE_TYPE_ constants
* @param string|null $value user_id, group_id or null for global mounts
* @return array
* @return list<ExternalMountInfo>
*/
public function getMountsFor($type, $value) {
public function getMountsFor(int $type, ?string $value): array {
$builder = $this->connection->getQueryBuilder();
$query = $this->getForQuery($builder, $type, $value);
@ -195,9 +238,9 @@ class DBConfigService {
*
* @param int $type any of the self::APPLICABLE_TYPE_ constants
* @param string|null $value user_id, group_id or null for global mounts
* @return array
* @return list<ExternalMountInfo>
*/
public function getAdminMountsFor($type, $value) {
public function getAdminMountsFor(int $type, ?string $value): array {
$builder = $this->connection->getQueryBuilder();
$query = $this->getForQuery($builder, $type, $value);
$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
@ -210,22 +253,20 @@ class DBConfigService {
*
* @param int $type any of the self::APPLICABLE_TYPE_ constants
* @param string[] $values user_ids or group_ids
* @return array
* @return list<ExternalMountInfo>
*/
public function getAdminMountsForMultiple($type, array $values) {
$builder = $this->connection->getQueryBuilder();
$params = array_map(function ($value) use ($builder) {
public function getAdminMountsForMultiple(int $type, array $values): array {
$builder = $this->getSelectQueryBuilder();
$params = array_map(function (string $value) use ($builder): IParameter {
return $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR);
}, $values);
$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'))
$builder = $builder
->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
->andWhere($builder->expr()->in('a.value', $params));
$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
$builder->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
return $this->getMountsFromQuery($query);
return $this->getMountsFromQuery($builder);
}
/**
@ -233,9 +274,9 @@ class DBConfigService {
*
* @param int $type any of the self::APPLICABLE_TYPE_ constants
* @param string|null $value user_id, group_id or null for global mounts
* @return array
* @return list<ExternalMountInfo>
*/
public function getUserMountsFor($type, $value) {
public function getUserMountsFor(int $type, ?string $value): array {
$builder = $this->connection->getQueryBuilder();
$query = $this->getForQuery($builder, $type, $value);
$query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_PERSONAL, IQueryBuilder::PARAM_INT)));
@ -246,14 +287,10 @@ class DBConfigService {
/**
* Add a mount to the database
*
* @param string $mountPoint
* @param string $storageBackend
* @param string $authBackend
* @param int $priority
* @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
* @param self::MOUNT_TYPE_ADMIN|self::MOUNT_TYPE_PERSONAL $type
* @return int the id of the new mount
*/
public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
public function addMount(string $mountPoint, string $storageBackend, string $authBackend, ?int $priority, int $type): int {
if (!$priority) {
$priority = 100;
}
@ -272,10 +309,8 @@ class DBConfigService {
/**
* Remove a mount from the database
*
* @param int $mountId
*/
public function removeMount($mountId) {
public function removeMount(int $mountId): void {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('external_mounts')
->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
@ -297,11 +332,7 @@ class DBConfigService {
$query->executeStatement();
}
/**
* @param int $mountId
* @param string $newMountPoint
*/
public function setMountPoint($mountId, $newMountPoint) {
public function setMountPoint(int $mountId, string $newMountPoint): void {
$builder = $this->connection->getQueryBuilder();
$query = $builder->update('external_mounts')
@ -311,11 +342,7 @@ class DBConfigService {
$query->executeStatement();
}
/**
* @param int $mountId
* @param string $newAuthBackend
*/
public function setAuthBackend($mountId, $newAuthBackend) {
public function setAuthBackend(int $mountId, string $newAuthBackend): void {
$builder = $this->connection->getQueryBuilder();
$query = $builder->update('external_mounts')
@ -325,12 +352,7 @@ class DBConfigService {
$query->executeStatement();
}
/**
* @param int $mountId
* @param string $key
* @param string $value
*/
public function setConfig($mountId, $key, $value) {
public function setConfig(int $mountId, string $key, string $value): void {
if ($key === 'password') {
$value = $this->encryptValue($value);
}
@ -355,12 +377,7 @@ class DBConfigService {
}
}
/**
* @param int $mountId
* @param string $key
* @param string $value
*/
public function setOption($mountId, $key, $value) {
public function setOption(int $mountId, string $key, string $value): void {
try {
$builder = $this->connection->getQueryBuilder();
$builder->insert('external_options')
@ -381,7 +398,7 @@ class DBConfigService {
}
}
public function addApplicable($mountId, $type, $value) {
public function addApplicable(int $mountId, int $type, ?string $value): void {
try {
$builder = $this->connection->getQueryBuilder();
$builder->insert('external_applicable')
@ -397,7 +414,7 @@ class DBConfigService {
}
}
public function removeApplicable($mountId, $type, $value) {
public function removeApplicable(int $mountId, int $type, ?string $value): void {
$builder = $this->connection->getQueryBuilder();
$query = $builder->delete('external_applicable')
->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
@ -413,11 +430,13 @@ class DBConfigService {
}
/**
* @return list<StorageConfigData>
* @return list<ExternalMountInfo>
* @throws Exception
*/
private function getMountsFromQuery(IQueryBuilder $query): array {
$result = $query->executeQuery();
$mounts = $result->fetchAllAssociative();
/** @var \Generator<array{mount_id: int, mount_point: string, storage_backend: string, auth_backend: string, priority: mixed, type: int}> $mounts */
$mounts = $result->iterateAssociative();
$uniqueMounts = [];
foreach ($mounts as $mount) {
$id = $mount['mount_id'];
@ -427,7 +446,7 @@ class DBConfigService {
}
$uniqueMounts = array_values($uniqueMounts);
$mountIds = array_map(function ($mount) {
$mountIds = array_map(function (array $mount): int {
return $mount['mount_id'];
}, $uniqueMounts);
$mountIds = array_values(array_unique($mountIds));
@ -436,8 +455,10 @@ class DBConfigService {
$config = $this->getConfigForMounts($mountIds);
$options = $this->getOptionsForMounts($mountIds);
return array_map(function ($mount, $applicable, $config, $options) {
$mount['type'] = (int)$mount['type'];
return array_map(function (array $mount, array $applicable, array $config, array $options): array {
$mountType = (int)$mount['type'];
assert($mountType === self::MOUNT_TYPE_ADMIN || $mountType === self::MOUNT_TYPE_PERSONAL);
$mount['type'] = $mountType;
$mount['priority'] = (int)$mount['priority'];
$mount['applicable'] = $applicable;
$mount['config'] = $config;
@ -449,7 +470,6 @@ class DBConfigService {
/**
* Get mount options from a table grouped by mount id
*
* @param string $table
* @param string[] $fields
* @param int[] $mountIds
* @return array<int, list<array>> [$mountId => [['field1' => $value1, ...], ...], ...]
@ -460,9 +480,7 @@ class DBConfigService {
}
$builder = $this->connection->getQueryBuilder();
$fields[] = 'mount_id';
$placeHolders = array_map(function ($id) use ($builder) {
return $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT);
}, $mountIds);
$placeHolders = array_map(fn ($id) => $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT), $mountIds);
$query = $builder->select($fields)
->from($table)
->where($builder->expr()->in('mount_id', $placeHolders));
@ -486,26 +504,28 @@ class DBConfigService {
/**
* @param int[] $mountIds
* @return array<int, list<ApplicableConfig>> [$id => [['type' => $type, 'value' => $value], ...], ...]
* @return array<int, list<array{type: mixed, value: string}>> [$id => [['type' => $type, 'value' => $value], ...], ...]
*/
public function getApplicableForMounts(array $mountIds): array {
return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
/** @var array<int, list<array{type: mixed, value: string}>> $result */
$result = $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
return $result;
}
/**
* @param int[] $mountIds
* @return array [$id => ['key1' => $value1, ...], ...]
* @return array<int, array> [$id => ['key1' => $value1, ...], ...]
*/
public function getConfigForMounts($mountIds) {
public function getConfigForMounts(array $mountIds): array {
$mountConfigs = $this->selectForMounts('external_config', ['key', 'value'], $mountIds);
return array_map([$this, 'createKeyValueMap'], $mountConfigs);
}
/**
* @param int[] $mountIds
* @return array [$id => ['key1' => $value1, ...], ...]
* @return array<int, array> [$id => ['key1' => $value1, ...], ...]
*/
public function getOptionsForMounts($mountIds) {
public function getOptionsForMounts(array $mountIds): array {
$mountOptions = $this->selectForMounts('external_options', ['key', 'value'], $mountIds);
$optionsMap = array_map([$this, 'createKeyValueMap'], $mountOptions);
return array_map(function (array $options) {
@ -516,10 +536,10 @@ class DBConfigService {
}
/**
* @param array $keyValuePairs [['key'=>$key, 'value=>$value], ...]
* @param list<array{key: string, value: string}> $keyValuePairs [['key'=>$key, 'value=>$value], ...]
* @return array ['key1' => $value1, ...]
*/
private function createKeyValueMap(array $keyValuePairs) {
private function createKeyValueMap(array $keyValuePairs): array {
$decryptedPairts = array_map(function ($pair) {
if ($pair['key'] === 'password') {
$pair['value'] = $this->decryptValue($pair['value']);
@ -536,14 +556,14 @@ class DBConfigService {
return array_combine($keys, $values);
}
private function encryptValue($value) {
private function encryptValue(string $value): string {
return $this->crypto->encrypt($value);
}
private function decryptValue($value) {
private function decryptValue(string $value): string {
try {
return $this->crypto->decrypt($value);
} catch (\Exception $e) {
} catch (\Exception) {
return $value;
}
}

View file

@ -120,10 +120,9 @@ abstract class StoragesService {
*
* @param int $id storage id
*
* @return StorageConfig
* @throws NotFoundException if the storage with the given id was not found
*/
public function getStorage(int $id) {
public function getStorage(int $id): StorageConfig {
$mount = $this->dbConfig->getMountById($id);
if (!is_array($mount)) {

View file

@ -58,18 +58,18 @@ class UserGlobalStoragesService extends GlobalStoragesService {
return array_merge($userMounts, $groupMounts, $globalMounts);
}
public function addStorage(StorageConfig $newStorage) {
public function addStorage(StorageConfig $newStorage): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
public function updateStorage(StorageConfig $updatedStorage) {
public function updateStorage(StorageConfig $updatedStorage): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
/**
* @param integer $id
*/
public function removeStorage($id) {
public function removeStorage($id): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
@ -164,16 +164,33 @@ class UserGlobalStoragesService extends GlobalStoragesService {
}
$groupIds = $this->groupManager->getUserGroupIds($user);
$mounts = $this->dbConfig->getMountsForUser($user->getUID(), $groupIds);
$configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
$configs = array_filter($configs, function ($config) {
return $config instanceof StorageConfig;
});
$configs = array_map($this->getStorageConfigFromDBMount(...), $mounts);
$configs = array_filter($configs, static fn ($config) => $config instanceof StorageConfig);
$keys = array_map(function (StorageConfig $config) {
return $config->getId();
}, $configs);
$keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs);
$storages = array_combine($keys, $configs);
return array_filter($storages, [$this, 'validateStorage']);
return array_filter($storages, $this->validateStorage(...));
}
/**
* @return StorageConfig[]
*/
public function getAllStoragesForUserWithPath(string $path, bool $forChildren): array {
$user = $this->getUser();
if (is_null($user)) {
return [];
}
$groupIds = $this->groupManager->getUserGroupIds($user);
$mounts = $this->dbConfig->getMountsForUserAndPath($user->getUID(), $groupIds, $path, $forChildren);
$configs = array_map($this->getStorageConfigFromDBMount(...), $mounts);
$configs = array_filter($configs, static fn ($config) => $config instanceof StorageConfig);
$keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs);
$storages = array_combine($keys, $configs);
return array_filter($storages, $this->validateStorage(...));
}
}

View file

@ -116,11 +116,11 @@ class UserStoragesService extends StoragesService {
*
* @return int BackendService::VISIBILITY_* constants
*/
public function getVisibilityType() {
public function getVisibilityType(): int {
return BackendService::VISIBILITY_PERSONAL;
}
protected function isApplicable(StorageConfig $config) {
protected function isApplicable(StorageConfig $config): bool {
return ($config->getApplicableUsers() === [$this->getUser()->getUID()]) && $config->getType() === StorageConfig::MOUNT_TYPE_PERSONAL;
}

View file

@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_External\Tests\Config;
use OC\Files\Storage\StorageFactory;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Auth\NullMechanism;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\Lib\Backend\SMB;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\Service\BackendService;
use OCA\Files_External\Service\UserGlobalStoragesService;
use OCA\Files_External\Service\UserStoragesService;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Config\ICachedMountFileInfo;
use OCP\Files\Config\MountProviderArgs;
use OCP\IUser;
use OCP\Server;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Clock\ClockInterface;
use Test\TestCase;
#[Group(name: 'DB')]
class ConfigAdapterTest extends TestCase {
private ConfigAdapter $adapter;
private BackendService&MockObject $backendService;
private IUser&MockObject $user;
private UserStoragesService $userStoragesService;
private UserGlobalStoragesService $userGlobalStoragesService;
private array $storageIds = [];
protected function makeStorageConfig(array $data): StorageConfig {
$storage = new StorageConfig();
if (isset($data['id'])) {
$storage->setId($data['id']);
}
$storage->setMountPoint($data['mountPoint']);
$data['backend'] = $this->backendService->getBackend($data['backendIdentifier']);
if (!isset($data['backend'])) {
throw new \Exception('oops, no backend');
}
$data['authMechanism'] = $this->backendService->getAuthMechanism($data['authMechanismIdentifier']);
if (!isset($data['authMechanism'])) {
throw new \Exception('oops, no auth mechanism');
}
$storage->setId(StorageConfig::MOUNT_TYPE_PERSONAL);
$storage->setApplicableUsers([$this->user->getUID()]);
$storage->setBackend($data['backend']);
$storage->setAuthMechanism($data['authMechanism']);
$storage->setBackendOptions($data['backendOptions']);
$storage->setPriority($data['priority']);
if (isset($data['mountOptions'])) {
$storage->setMountOptions($data['mountOptions']);
}
return $storage;
}
protected function getBackendMock($class = SMB::class, $storageClass = \OCA\Files_External\Lib\Storage\SMB::class) {
$backend = $this->createMock(Backend::class);
$backend->method('getStorageClass')
->willReturn($storageClass);
$backend->method('getIdentifier')
->willReturn('identifier:' . $class);
return $backend;
}
protected function getAuthMechMock($scheme = 'null', $class = NullMechanism::class) {
$authMech = $this->createMock(AuthMechanism::class);
$authMech->method('getScheme')
->willReturn($scheme);
$authMech->method('getIdentifier')
->willReturn('identifier:' . $class);
return $authMech;
}
public function setUp(): void {
// prepare BackendService mock
$this->backendService = $this->createMock(BackendService::class);
$authMechanisms = [
'identifier:\Auth\Mechanism' => $this->getAuthMechMock('null', '\Auth\Mechanism'),
'identifier:\Other\Auth\Mechanism' => $this->getAuthMechMock('null', '\Other\Auth\Mechanism'),
'identifier:\OCA\Files_External\Lib\Auth\NullMechanism' => $this->getAuthMechMock(),
];
$this->backendService->method('getAuthMechanism')
->willReturnCallback(function ($class) use ($authMechanisms) {
if (isset($authMechanisms[$class])) {
return $authMechanisms[$class];
}
return null;
});
$this->backendService->method('getAuthMechanismsByScheme')
->willReturnCallback(function ($schemes) use ($authMechanisms) {
return array_filter($authMechanisms, function ($authMech) use ($schemes) {
return in_array($authMech->getScheme(), $schemes, true);
});
});
$backends = [
'identifier:\OCA\Files_External\Lib\Backend\DAV' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\DAV', '\OC\Files\Storage\DAV'),
'identifier:\OCA\Files_External\Lib\Backend\SMB' => $this->getBackendMock('\OCA\Files_External\Lib\Backend\SMB', '\OCA\Files_External\Lib\Storage\SMB'),
];
$this->backendService->method('getBackend')
->willReturnCallback(function ($backendClass) use ($backends) {
if (isset($backends[$backendClass])) {
return $backends[$backendClass];
}
return null;
});
$this->backendService->method('getAuthMechanisms')
->willReturn($authMechanisms);
$this->backendService->method('getBackends')
->willReturn($backends);
$this->userStoragesService = Server::get(UserStoragesService::class);
$this->userGlobalStoragesService = Server::get(UserGlobalStoragesService::class);
$this->adapter = new ConfigAdapter($this->userStoragesService, $this->userGlobalStoragesService, $this->createMock(ClockInterface::class));
$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')->willReturn('user1');
$this->userStoragesService->setUser($this->user);
$storageConfig = $this->makeStorageConfig([
'mountPoint' => '/mountpoint',
'backendIdentifier' => 'identifier:\OCA\Files_External\Lib\Backend\SMB',
'authMechanismIdentifier' => 'identifier:\Auth\Mechanism',
'backendOptions' => [
'option1' => 'value1',
'option2' => 'value2',
'password' => 'testPassword',
],
'priority' => 15,
'mountOptions' => [
'preview' => false,
]
]);
$this->storageIds[] = $this->userStoragesService->addStorage($storageConfig)->getId();
$storageConfig = $this->makeStorageConfig([
'mountPoint' => '/subfolder/mountpoint',
'backendIdentifier' => 'identifier:\OCA\Files_External\Lib\Backend\SMB',
'authMechanismIdentifier' => 'identifier:\Auth\Mechanism',
'backendOptions' => [
'option1' => 'value1',
'option2' => 'value2',
'password' => 'testPassword',
],
'priority' => 15,
'mountOptions' => [
'preview' => false,
]
]);
$this->storageIds[] = $this->userStoragesService->addStorage($storageConfig)->getId();
$storageConfig = $this->makeStorageConfig([
'mountPoint' => '/subfolder/subfolder/mountpoint',
'backendIdentifier' => 'identifier:\OCA\Files_External\Lib\Backend\SMB',
'authMechanismIdentifier' => 'identifier:\Auth\Mechanism',
'backendOptions' => [
'option1' => 'value1',
'option2' => 'value2',
'password' => 'testPassword',
],
'priority' => 15,
'mountOptions' => [
'preview' => false,
]
]);
$this->storageIds[] = $this->userStoragesService->addStorage($storageConfig)->getId();
}
public function tearDown(): void {
$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')->willReturn('user1');
$this->userStoragesService->setUser($this->user);
foreach ($this->storageIds as $storageId) {
$this->userStoragesService->removeStorage($storageId);
}
}
public static function pathsProvider(): \Generator {
yield ['/user1/files/subfolder', 2];
yield ['/user1/files/subfolder/subfolder', 1];
yield ['/user1/files/nothing', 0];
yield ['/user1/files/mountpoint', 0]; // we only want the children
}
#[DataProvider(methodName: 'pathsProvider')]
public function testPartialMountpointWithChildren(string $path, int $count): void {
$mountFileInfo = $this->createMock(ICachedMountFileInfo::class);
$mountFileInfo->method('getUser')->willReturn($this->user);
$cacheEntry = $this->createMock(ICacheEntry::class);
$result = $this->adapter->getMountsForPath($path, true, [
new MountProviderArgs($mountFileInfo, $cacheEntry),
], $this->createMock(StorageFactory::class));
$this->assertCount($count, $result);
}
public function testPartialMountpointExact(): void {
$mountFileInfo = $this->createMock(ICachedMountFileInfo::class);
$mountFileInfo->method('getUser')->willReturn($this->user);
$mountFileInfo->method('getMountPoint')->willReturn('/user1/files/subfolder/subfolder');
$cacheEntry = $this->createMock(ICacheEntry::class);
$result = $this->adapter->getMountsForPath('/user1/files/subfolder/subfolder', true, [
new MountProviderArgs($mountFileInfo, $cacheEntry),
], $this->createMock(StorageFactory::class));
$this->assertCount(1, $result);
}
}

View file

@ -40,13 +40,13 @@ use PHPUnit\Framework\MockObject\MockObject;
class CleaningDBConfig extends DBConfigService {
private array $mountIds = [];
public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type): int {
$id = parent::addMount($mountPoint, $storageBackend, $authBackend, $priority, $type); // TODO: Change the autogenerated stub
$this->mountIds[] = $id;
return $id;
}
public function clean() {
public function clean(): void {
foreach ($this->mountIds as $id) {
$this->removeMount($id);
}
@ -205,6 +205,7 @@ abstract class StoragesServiceTestCase extends \Test\TestCase {
if (isset($data['mountOptions'])) {
$storage->setMountOptions($data['mountOptions']);
}
$storage->setPriority(100);
return $storage;
}
@ -258,6 +259,7 @@ abstract class StoragesServiceTestCase extends \Test\TestCase {
$storage->setBackend($backend);
$storage->setAuthMechanism($authMechanism);
$storage->setBackendOptions($backendOptions);
$storage->setPriority(0);
$newStorage = $this->service->addStorage($storage);
$id = $newStorage->getId();
@ -399,6 +401,7 @@ abstract class StoragesServiceTestCase extends \Test\TestCase {
$storage->setBackend($backend);
$storage->setAuthMechanism($authMechanism);
$storage->setBackendOptions(['password' => 'testPassword']);
$storage->setPriority(0);
$newStorage = $this->service->addStorage($storage);
@ -424,6 +427,7 @@ abstract class StoragesServiceTestCase extends \Test\TestCase {
$storage->setBackend($backend);
$storage->setAuthMechanism($authMechanism);
$storage->setBackendOptions(['password' => 'testPassword']);
$storage->setPriority(0);
$newStorage = $this->service->addStorage($storage);
@ -480,6 +484,7 @@ abstract class StoragesServiceTestCase extends \Test\TestCase {
$storage->setBackend($backend);
$storage->setAuthMechanism($authMechanism);
$storage->setBackendOptions(['password' => 'testPassword']);
$storage->setPriority(0);
$savedStorage = $this->service->addStorage($storage);

View file

@ -108,6 +108,7 @@ class UserGlobalStoragesServiceTest extends GlobalStoragesServiceTest {
$storage->setBackendOptions(['password' => 'testPassword']);
$storage->setApplicableUsers($applicableUsers);
$storage->setApplicableGroups($applicableGroups);
$storage->setPriority(0);
$newStorage = $this->globalStoragesService->addStorage($storage);