diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml
index aedcd5b7ed5..fb53cef79b8 100644
--- a/apps/files/appinfo/info.xml
+++ b/apps/files/appinfo/info.xml
@@ -53,6 +53,8 @@
OCA\Files\Command\Object\Info
OCA\Files\Command\Object\ListObject
OCA\Files\Command\Object\Orphans
+ OCA\Files\Command\Object\Multi\Users
+ OCA\Files\Command\Object\Multi\Rename
OCA\Files\Command\WindowsCompatibleFilenames
diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php
index 070cb46de38..0c0f734251f 100644
--- a/apps/files/composer/composer/autoload_classmap.php
+++ b/apps/files/composer/composer/autoload_classmap.php
@@ -37,6 +37,8 @@ return array(
'OCA\\Files\\Command\\Object\\Get' => $baseDir . '/../lib/Command/Object/Get.php',
'OCA\\Files\\Command\\Object\\Info' => $baseDir . '/../lib/Command/Object/Info.php',
'OCA\\Files\\Command\\Object\\ListObject' => $baseDir . '/../lib/Command/Object/ListObject.php',
+ 'OCA\\Files\\Command\\Object\\Multi\\Rename' => $baseDir . '/../lib/Command/Object/Multi/Rename.php',
+ 'OCA\\Files\\Command\\Object\\Multi\\Users' => $baseDir . '/../lib/Command/Object/Multi/Users.php',
'OCA\\Files\\Command\\Object\\ObjectUtil' => $baseDir . '/../lib/Command/Object/ObjectUtil.php',
'OCA\\Files\\Command\\Object\\Orphans' => $baseDir . '/../lib/Command/Object/Orphans.php',
'OCA\\Files\\Command\\Object\\Put' => $baseDir . '/../lib/Command/Object/Put.php',
diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php
index ce79d370e7c..19310ed4e92 100644
--- a/apps/files/composer/composer/autoload_static.php
+++ b/apps/files/composer/composer/autoload_static.php
@@ -52,6 +52,8 @@ class ComposerStaticInitFiles
'OCA\\Files\\Command\\Object\\Get' => __DIR__ . '/..' . '/../lib/Command/Object/Get.php',
'OCA\\Files\\Command\\Object\\Info' => __DIR__ . '/..' . '/../lib/Command/Object/Info.php',
'OCA\\Files\\Command\\Object\\ListObject' => __DIR__ . '/..' . '/../lib/Command/Object/ListObject.php',
+ 'OCA\\Files\\Command\\Object\\Multi\\Rename' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/Rename.php',
+ 'OCA\\Files\\Command\\Object\\Multi\\Users' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/Users.php',
'OCA\\Files\\Command\\Object\\ObjectUtil' => __DIR__ . '/..' . '/../lib/Command/Object/ObjectUtil.php',
'OCA\\Files\\Command\\Object\\Orphans' => __DIR__ . '/..' . '/../lib/Command/Object/Orphans.php',
'OCA\\Files\\Command\\Object\\Put' => __DIR__ . '/..' . '/../lib/Command/Object/Put.php',
diff --git a/apps/files/lib/Command/Object/Multi/Rename.php b/apps/files/lib/Command/Object/Multi/Rename.php
new file mode 100644
index 00000000000..562c68eb07f
--- /dev/null
+++ b/apps/files/lib/Command/Object/Multi/Rename.php
@@ -0,0 +1,108 @@
+
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object\Multi;
+
+use OC\Core\Command\Base;
+use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class Rename extends Base {
+ public function __construct(
+ private readonly IDBConnection $connection,
+ private readonly PrimaryObjectStoreConfig $objectStoreConfig,
+ private readonly IConfig $config,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:multi:rename-config')
+ ->setDescription('Rename an object store configuration and move all users over to the new configuration,')
+ ->addArgument('source', InputArgument::REQUIRED, 'Object store configuration to rename')
+ ->addArgument('target', InputArgument::REQUIRED, 'New name for the object store configuration');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $source = $input->getArgument('source');
+ $target = $input->getArgument('target');
+
+ $configs = $this->objectStoreConfig->getObjectStoreConfigs();
+ if (!isset($configs[$source])) {
+ $output->writeln('Unknown object store configuration: ' . $source . '');
+ return 1;
+ }
+
+ if ($source === 'root') {
+ $output->writeln('Renaming the root configuration is not supported.');
+ return 1;
+ }
+
+ if ($source === 'default') {
+ $output->writeln('Renaming the default configuration is not supported.');
+ return 1;
+ }
+
+ if (!isset($configs[$target])) {
+ $output->writeln('Target object store configuration ' . $target . ' doesn\'t exist yet.');
+ $output->writeln('The target configuration can be created automatically.');
+ $output->writeln('However, as this depends on modifying the config.php, this only works as long as the instance runs on a single node or all nodes in a clustered setup have a shared config file (such as from a shared network mount).');
+ $output->writeln('If the different nodes have a separate copy of the config.php file, the automatic object store configuration creation will lead to the configuration going out of sync.');
+ $output->writeln('If these requirements are not met, you can manually create the target object store configuration in each node\'s configuration before running the command.');
+ $output->writeln('');
+ $output->writeln('Failure to check these requirements will lead to data loss for users.');
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Automatically create target object store configuration? [y/N] ', false);
+ if ($helper->ask($input, $output, $question)) {
+ $configs[$target] = $configs[$source];
+
+ // update all aliases
+ foreach ($configs as &$config) {
+ if ($config === $source) {
+ $config = $target;
+ }
+ }
+ $this->config->setSystemValue('objectstore', $configs);
+ } else {
+ return 0;
+ }
+ } elseif (($configs[$source] !== $configs[$target]) || $configs[$source] !== $target) {
+ $output->writeln('Source and target configuration differ.');
+ $output->writeln('');
+ $output->writeln('To ensure proper migration of users, the source and target configuration must be the same to ensure that the objects for the moved users exist on the target configuration.');
+ $output->writeln('The usual migration process consists of creating a clone of the old configuration, moving the users from the old configuration to the new one, and then adjust the old configuration that is longer used.');
+ return 1;
+ }
+
+ $query = $this->connection->getQueryBuilder();
+ $query->update('preferences')
+ ->set('configvalue', $query->createNamedParameter($target))
+ ->where($query->expr()->eq('appid', $query->createNamedParameter('homeobjectstore')))
+ ->andWhere($query->expr()->eq('configkey', $query->createNamedParameter('objectstore')))
+ ->andWhere($query->expr()->eq('configvalue', $query->createNamedParameter($source)));
+ $count = $query->executeStatement();
+
+ if ($count > 0) {
+ $output->writeln('Moved ' . $count . ' users');
+ } else {
+ $output->writeln('No users moved');
+ }
+
+ return 0;
+ }
+}
diff --git a/apps/files/lib/Command/Object/Multi/Users.php b/apps/files/lib/Command/Object/Multi/Users.php
new file mode 100644
index 00000000000..e8f7d012641
--- /dev/null
+++ b/apps/files/lib/Command/Object/Multi/Users.php
@@ -0,0 +1,98 @@
+
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object\Multi;
+
+use OC\Core\Command\Base;
+use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Users extends Base {
+ public function __construct(
+ private readonly IUserManager $userManager,
+ private readonly PrimaryObjectStoreConfig $objectStoreConfig,
+ private readonly IConfig $config,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:multi:users')
+ ->setDescription('Get the mapping between users and object store buckets')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, 'Only list users using the specified bucket')
+ ->addOption('object-store', 'o', InputOption::VALUE_REQUIRED, 'Only list users using the specified object store configuration')
+ ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Only show the mapping for the specified user, ignores all other options');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ if ($userId = $input->getOption('user')) {
+ $user = $this->userManager->get($userId);
+ if (!$user) {
+ $output->writeln("User $userId not found");
+ return 1;
+ }
+ $users = new \ArrayIterator([$user]);
+ } else {
+ $bucket = (string)$input->getOption('bucket');
+ $objectStore = (string)$input->getOption('object-store');
+ if ($bucket !== '' && $objectStore === '') {
+ $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket));
+ } elseif ($bucket === '' && $objectStore !== '') {
+ $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore));
+ } elseif ($bucket) {
+ $users = $this->getUsers(array_intersect(
+ $this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket),
+ $this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore)
+ ));
+ } else {
+ $users = $this->userManager->getSeenUsers();
+ }
+ }
+
+ $this->writeStreamingTableInOutputFormat($input, $output, $this->infoForUsers($users), 100);
+ return 0;
+ }
+
+ /**
+ * @param string[] $userIds
+ * @return \Iterator
+ */
+ private function getUsers(array $userIds): \Iterator {
+ foreach ($userIds as $userId) {
+ $user = $this->userManager->get($userId);
+ if ($user) {
+ yield $user;
+ }
+ }
+ }
+
+ /**
+ * @param \Iterator $users
+ * @return \Iterator
+ */
+ private function infoForUsers(\Iterator $users): \Iterator {
+ foreach ($users as $user) {
+ yield $this->infoForUser($user);
+ }
+ }
+
+ private function infoForUser(IUser $user): array {
+ return [
+ 'user' => $user->getUID(),
+ 'object-store' => $this->objectStoreConfig->getObjectStoreForUser($user),
+ 'bucket' => $this->objectStoreConfig->getSetBucketForUser($user) ?? 'unset',
+ ];
+ }
+}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index acded1ed539..c2412f0152e 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1687,6 +1687,7 @@ return array(
'OC\\Files\\ObjectStore\\AppdataPreviewObjectStoreStorage' => $baseDir . '/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php',
'OC\\Files\\ObjectStore\\Azure' => $baseDir . '/lib/private/Files/ObjectStore/Azure.php',
'OC\\Files\\ObjectStore\\HomeObjectStoreStorage' => $baseDir . '/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php',
+ 'OC\\Files\\ObjectStore\\InvalidObjectStoreConfigurationException' => $baseDir . '/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php',
'OC\\Files\\ObjectStore\\Mapper' => $baseDir . '/lib/private/Files/ObjectStore/Mapper.php',
'OC\\Files\\ObjectStore\\ObjectStoreScanner' => $baseDir . '/lib/private/Files/ObjectStore/ObjectStoreScanner.php',
'OC\\Files\\ObjectStore\\ObjectStoreStorage' => $baseDir . '/lib/private/Files/ObjectStore/ObjectStoreStorage.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index bb20a68eae3..9c0cf7fb9be 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1728,6 +1728,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Files\\ObjectStore\\AppdataPreviewObjectStoreStorage' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php',
'OC\\Files\\ObjectStore\\Azure' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Azure.php',
'OC\\Files\\ObjectStore\\HomeObjectStoreStorage' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/HomeObjectStoreStorage.php',
+ 'OC\\Files\\ObjectStore\\InvalidObjectStoreConfigurationException' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php',
'OC\\Files\\ObjectStore\\Mapper' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Mapper.php',
'OC\\Files\\ObjectStore\\ObjectStoreScanner' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/ObjectStoreScanner.php',
'OC\\Files\\ObjectStore\\ObjectStoreStorage' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/ObjectStoreStorage.php',
diff --git a/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php
new file mode 100644
index 00000000000..369182b069d
--- /dev/null
+++ b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php
@@ -0,0 +1,13 @@
+
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Files\ObjectStore;
+
+class InvalidObjectStoreConfigurationException extends \Exception {
+
+}
diff --git a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php
index fdfe989addc..ffc33687340 100644
--- a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php
+++ b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php
@@ -34,9 +34,13 @@ class PrimaryObjectStoreConfig {
* @return ?ObjectStoreConfig
*/
public function getObjectStoreConfigForRoot(): ?array {
- $config = $this->getObjectStoreConfig();
+ if (!$this->hasObjectStore()) {
+ return null;
+ }
+
+ $config = $this->getObjectStoreConfiguration('root');
- if ($config && $config['arguments']['multibucket']) {
+ if ($config['arguments']['multibucket']) {
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
@@ -51,38 +55,102 @@ class PrimaryObjectStoreConfig {
* @return ?ObjectStoreConfig
*/
public function getObjectStoreConfigForUser(IUser $user): ?array {
- $config = $this->getObjectStoreConfig();
+ if (!$this->hasObjectStore()) {
+ return null;
+ }
- if ($config && $config['arguments']['multibucket']) {
+ $store = $this->getObjectStoreForUser($user);
+ $config = $this->getObjectStoreConfiguration($store);
+
+ if ($config['arguments']['multibucket']) {
$config['arguments']['bucket'] = $this->getBucketForUser($user, $config);
}
return $config;
}
/**
- * @return ?ObjectStoreConfig
+ * @param string $name
+ * @return ObjectStoreConfig
*/
- private function getObjectStoreConfig(): ?array {
+ public function getObjectStoreConfiguration(string $name): array {
+ $configs = $this->getObjectStoreConfigs();
+ $name = $this->resolveAlias($name);
+ if (!isset($configs[$name])) {
+ throw new \Exception("Object store configuration for '$name' not found");
+ }
+ if (is_string($configs[$name])) {
+ throw new \Exception("Object store configuration for '{$configs[$name]}' not found");
+ }
+ return $configs[$name];
+ }
+
+ public function resolveAlias(string $name): string {
+ $configs = $this->getObjectStoreConfigs();
+
+ while (isset($configs[$name]) && is_string($configs[$name])) {
+ $name = $configs[$name];
+ }
+ return $name;
+ }
+
+ public function hasObjectStore(): bool {
+ $objectStore = $this->config->getSystemValue('objectstore', null);
+ $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null);
+ return $objectStore || $objectStoreMultiBucket;
+ }
+
+ public function hasMultipleObjectStorages(): bool {
+ $objectStore = $this->config->getSystemValue('objectstore', []);
+ return isset($objectStore['default']);
+ }
+
+ /**
+ * @return ?array
+ * @throws InvalidObjectStoreConfigurationException
+ */
+ public function getObjectStoreConfigs(): ?array {
$objectStore = $this->config->getSystemValue('objectstore', null);
$objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null);
// new-style multibucket config uses the same 'objectstore' key but sets `'multibucket' => true`, transparently upgrade older style config
if ($objectStoreMultiBucket) {
$objectStoreMultiBucket['arguments']['multibucket'] = true;
- return $this->validateObjectStoreConfig($objectStoreMultiBucket);
+ return [
+ 'default' => 'server1',
+ 'server1' => $this->validateObjectStoreConfig($objectStoreMultiBucket),
+ 'root' => 'server1',
+ ];
} elseif ($objectStore) {
- return $this->validateObjectStoreConfig($objectStore);
+ if (!isset($objectStore['default'])) {
+ $objectStore = [
+ 'default' => 'server1',
+ 'root' => 'server1',
+ 'server1' => $objectStore,
+ ];
+ }
+ if (!isset($objectStore['root'])) {
+ $objectStore['root'] = 'default';
+ }
+
+ if (!is_string($objectStore['default'])) {
+ throw new InvalidObjectStoreConfigurationException('The \'default\' object storage configuration is required to be a reference to another configuration.');
+ }
+ return array_map($this->validateObjectStoreConfig(...), $objectStore);
} else {
return null;
}
}
/**
- * @return ObjectStoreConfig
+ * @param array|string $config
+ * @return string|ObjectStoreConfig
*/
- private function validateObjectStoreConfig(array $config) {
+ private function validateObjectStoreConfig(array|string $config): array|string {
+ if (is_string($config)) {
+ return $config;
+ }
if (!isset($config['class'])) {
- throw new \Exception('No class configured for object store');
+ throw new InvalidObjectStoreConfigurationException('No class configured for object store');
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
@@ -90,17 +158,17 @@ class PrimaryObjectStoreConfig {
$class = $config['class'];
$arguments = $config['arguments'];
if (!is_array($arguments)) {
- throw new \Exception('Configured object store arguments are not an array');
+ throw new InvalidObjectStoreConfigurationException('Configured object store arguments are not an array');
}
if (!isset($arguments['multibucket'])) {
$arguments['multibucket'] = false;
}
if (!is_bool($arguments['multibucket'])) {
- throw new \Exception('arguments.multibucket must be a boolean in object store configuration');
+ throw new InvalidObjectStoreConfigurationException('arguments.multibucket must be a boolean in object store configuration');
}
if (!is_string($class)) {
- throw new \Exception('Configured class for object store is not a string');
+ throw new InvalidObjectStoreConfigurationException('Configured class for object store is not a string');
}
if (str_starts_with($class, 'OCA\\') && substr_count($class, '\\') >= 2) {
@@ -109,7 +177,7 @@ class PrimaryObjectStoreConfig {
}
if (!is_a($class, IObjectStore::class, true)) {
- throw new \Exception('Configured class for object store is not an object store');
+ throw new InvalidObjectStoreConfigurationException('Configured class for object store is not an object store');
}
return [
'class' => $class,
@@ -117,8 +185,8 @@ class PrimaryObjectStoreConfig {
];
}
- private function getBucketForUser(IUser $user, array $config): string {
- $bucket = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null);
+ public function getBucketForUser(IUser $user, array $config): string {
+ $bucket = $this->getSetBucketForUser($user);
if ($bucket === null) {
/*
@@ -129,7 +197,7 @@ class PrimaryObjectStoreConfig {
$config['arguments']['bucket'] = '';
}
$mapper = new Mapper($user, $this->config);
- $numBuckets = isset($config['arguments']['num_buckets']) ? $config['arguments']['num_buckets'] : 64;
+ $numBuckets = $config['arguments']['num_buckets'] ?? 64;
$bucket = $config['arguments']['bucket'] . $mapper->getBucket($numBuckets);
$this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $bucket);
@@ -137,4 +205,21 @@ class PrimaryObjectStoreConfig {
return $bucket;
}
+
+ public function getSetBucketForUser(IUser $user): ?string {
+ return $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null);
+ }
+
+ public function getObjectStoreForUser(IUser $user): string {
+ if ($this->hasMultipleObjectStorages()) {
+ $value = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'objectstore', null);
+ if ($value === null) {
+ $value = $this->resolveAlias('default');
+ $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'objectstore', $value);
+ }
+ return $value;
+ } else {
+ return 'default';
+ }
+ }
}
diff --git a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php b/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php
index dd696279b86..ae0a53f2cc0 100644
--- a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php
+++ b/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php
@@ -84,7 +84,7 @@ class ObjectHomeMountProviderTest extends \Test\TestCase {
$this->config->method('getUserValue')
->willReturn(null);
- $this->config->expects($this->once())
+ $this->config
->method('setUserValue')
->with(
$this->equalTo('uid'),
diff --git a/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php b/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php
new file mode 100644
index 00000000000..b60b7ca4f83
--- /dev/null
+++ b/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php
@@ -0,0 +1,285 @@
+
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace lib\Files\ObjectStore;
+
+use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
+use OC\Files\ObjectStore\StorageObjectStore;
+use OCP\App\IAppManager;
+use OCP\IConfig;
+use OCP\IUser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class PrimaryObjectStoreConfigTest extends TestCase {
+ private array $systemConfig = [];
+ private array $userConfig = [];
+ private IConfig&MockObject $config;
+ private IAppManager&MockObject $appManager;
+ private PrimaryObjectStoreConfig $objectStoreConfig;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->systemConfig = [];
+ $this->config = $this->createMock(IConfig::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->config->method('getSystemValue')
+ ->willReturnCallback(function ($key, $default = '') {
+ if (isset($this->systemConfig[$key])) {
+ return $this->systemConfig[$key];
+ } else {
+ return $default;
+ }
+ });
+ $this->config->method('getUserValue')
+ ->willReturnCallback(function ($userId, $appName, $key, $default = '') {
+ if (isset($this->userConfig[$userId][$appName][$key])) {
+ return $this->userConfig[$userId][$appName][$key];
+ } else {
+ return $default;
+ }
+ });
+ $this->config->method('setUserValue')
+ ->willReturnCallback(function ($userId, $appName, $key, $value) {
+ $this->userConfig[$userId][$appName][$key] = $value;
+ });
+
+ $this->objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->appManager);
+ }
+
+ private function getUser(string $uid): IUser {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn($uid);
+ return $user;
+ }
+
+ private function setConfig(string $key, $value) {
+ $this->systemConfig[$key] = $value;
+ }
+
+ public function testNewUserGetsDefault() {
+ $this->setConfig('objectstore', [
+ 'default' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+
+ $this->assertEquals('server1', $this->config->getUserValue('test', 'homeobjectstore', 'objectstore', null));
+ }
+
+ public function testExistingUserKeepsStorage() {
+ // setup user with `server1` as storage
+ $this->testNewUserGetsDefault();
+
+ $this->setConfig('objectstore', [
+ 'default' => 'server2',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ],
+ 'server2' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server2',
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+
+ $this->assertEquals('server1', $this->config->getUserValue('test', 'homeobjectstore', 'objectstore', null));
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('other-user'));
+ $this->assertEquals('server2', $result['arguments']['host']);
+ }
+
+ public function testNestedAliases() {
+ $this->setConfig('objectstore', [
+ 'default' => 'a1',
+ 'a1' => 'a2',
+ 'a2' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ],
+ ]);
+ $this->assertEquals('server1', $this->objectStoreConfig->resolveAlias('default'));
+ }
+
+ public function testMultibucketChangedConfig() {
+ $this->setConfig('objectstore', [
+ 'default' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => true,
+ 'num_buckets' => 8,
+ 'bucket' => 'bucket-'
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+ $this->assertEquals('bucket-7', $result['arguments']['bucket']);
+
+ $this->setConfig('objectstore', [
+ 'default' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => true,
+ 'num_buckets' => 64,
+ 'bucket' => 'bucket-'
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+ $this->assertEquals('bucket-7', $result['arguments']['bucket']);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test-foo'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+ $this->assertEquals('bucket-40', $result['arguments']['bucket']);
+
+ $this->setConfig('objectstore', [
+ 'default' => 'server2',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => true,
+ 'num_buckets' => 64,
+ 'bucket' => 'bucket-'
+ ],
+ ],
+ 'server2' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server2',
+ 'multibucket' => true,
+ 'num_buckets' => 16,
+ 'bucket' => 'bucket-'
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test'));
+ $this->assertEquals('server1', $result['arguments']['host']);
+ $this->assertEquals('bucket-7', $result['arguments']['bucket']);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test-bar'));
+ $this->assertEquals('server2', $result['arguments']['host']);
+ $this->assertEquals('bucket-4', $result['arguments']['bucket']);
+ }
+
+ public function testMultibucketOldConfig() {
+ $this->setConfig('objectstore_multibucket', [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => true,
+ 'num_buckets' => 8,
+ 'bucket' => 'bucket-'
+ ],
+ ]);
+ $configs = $this->objectStoreConfig->getObjectStoreConfigs();
+ $this->assertEquals([
+ 'default' => 'server1',
+ 'root' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => true,
+ 'num_buckets' => 8,
+ 'bucket' => 'bucket-'
+ ],
+ ],
+ ], $configs);
+ }
+
+ public function testSingleObjectStore() {
+ $this->setConfig('objectstore', [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ]);
+ $configs = $this->objectStoreConfig->getObjectStoreConfigs();
+ $this->assertEquals([
+ 'default' => 'server1',
+ 'root' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ 'multibucket' => false,
+ ],
+ ],
+ ], $configs);
+ }
+
+ public function testRoot() {
+ $this->setConfig('objectstore', [
+ 'default' => 'server1',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ],
+ 'server2' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server2',
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForRoot();
+ $this->assertEquals('server1', $result['arguments']['host']);
+
+ $this->setConfig('objectstore', [
+ 'default' => 'server1',
+ 'root' => 'server2',
+ 'server1' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server1',
+ ],
+ ],
+ 'server2' => [
+ 'class' => StorageObjectStore::class,
+ 'arguments' => [
+ 'host' => 'server2',
+ ],
+ ],
+ ]);
+
+ $result = $this->objectStoreConfig->getObjectStoreConfigForRoot();
+ $this->assertEquals('server2', $result['arguments']['host']);
+ }
+}