From 8089fab1b24625514178e4af02f12af07c95bd43 Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Wed, 17 Dec 2025 21:15:34 +0800 Subject: [PATCH 1/5] feat(objectstore): get multibucket mappings for all users with --all Signed-off-by: Kent Delante --- apps/files/lib/Command/Object/Multi/Users.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/files/lib/Command/Object/Multi/Users.php b/apps/files/lib/Command/Object/Multi/Users.php index a6cfa0326efa5..3d42165cca104 100644 --- a/apps/files/lib/Command/Object/Multi/Users.php +++ b/apps/files/lib/Command/Object/Multi/Users.php @@ -33,11 +33,14 @@ protected function configure(): void { ->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'); + ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Only show the mapping for the specified user, ignores all other options') + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Show the mapping for all users'); } public function execute(InputInterface $input, OutputInterface $output): int { - if ($userId = $input->getOption('user')) { + if ($input->getOption('all')) { + $users = $this->userManager->getSeenUsers(); + } elseif ($userId = $input->getOption('user')) { $user = $this->userManager->get($userId); if (!$user) { $output->writeln("User $userId not found"); @@ -57,7 +60,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore) )); } else { - $users = $this->userManager->getSeenUsers(); + $output->writeln("No option given. Please specify a user id with --user to show the mapping for the user or --all for all users"); + return 0; } } From c295cfca5a61385799fff93b61c4a195579df5ab Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Fri, 19 Dec 2025 13:10:43 +0800 Subject: [PATCH 2/5] feat: add move command Signed-off-by: Kent Delante --- apps/files/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files/lib/Command/Object/Multi/Move.php | 77 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 apps/files/lib/Command/Object/Multi/Move.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index a594278a787a3..46c1ed6bdd0cc 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -46,6 +46,7 @@ OCA\Files\Command\Object\Put OCA\Files\Command\Object\Multi\Users OCA\Files\Command\Object\Multi\Rename + OCA\Files\Command\Object\Multi\Move diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 97c21fb455068..00aad4ee74889 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -34,6 +34,7 @@ 'OCA\\Files\\Command\\Move' => $baseDir . '/../lib/Command/Move.php', 'OCA\\Files\\Command\\Object\\Delete' => $baseDir . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => $baseDir . '/../lib/Command/Object/Get.php', + 'OCA\\Files\\Command\\Object\\Multi\\Move' => $baseDir . '/..' . '/../lib/Command/Object/Multi/Move.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', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index c406fadb948ba..a14a44d556df6 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -49,6 +49,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Move' => __DIR__ . '/..' . '/../lib/Command/Move.php', 'OCA\\Files\\Command\\Object\\Delete' => __DIR__ . '/..' . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => __DIR__ . '/..' . '/../lib/Command/Object/Get.php', + 'OCA\\Files\\Command\\Object\\Multi\\Move' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/Move.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', diff --git a/apps/files/lib/Command/Object/Multi/Move.php b/apps/files/lib/Command/Object/Multi/Move.php new file mode 100644 index 0000000000000..b56f075080b5a --- /dev/null +++ b/apps/files/lib/Command/Object/Multi/Move.php @@ -0,0 +1,77 @@ +setName('files:object:multi:move') + ->setDescription('Migrate user to specified object-store') + ->addOption('object-store', 'b', InputOption::VALUE_REQUIRED, 'The name of the object store') + ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'The user to migrate') + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Move all users to specified object-store') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Run command without commiting any changes'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $objectStore = $input->getOption('object-store'); + if (!$objectStore) { + $output->writeln('Please specify the object store'); + } + + $configs = $this->objectStoreConfig->getObjectStoreConfigs(); + if (!isset($configs[$objectStore])) { + $output->writeln('Unknown object store configuration: ' . $objectStore . ''); + return 1; + } + + if ($input->getOption('all')) { + $users = $this->userManager->getSeenUsers(); + } elseif ($userId = $input->getOption('user')) { + $user = $this->userManager->get($userId); + if (!$user) { + $output->writeln('User ' . $userId . ' not found'); + return 1; + } + $users = new \ArrayIterator([$user]); + } else { + $output->writeln('Please specify a user id with --user or --all for all users'); + return 1; + } + + $count = 0; + foreach ($users as $user) { + if (!$input->getOption('dry-run')) { + $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'objectstore', $objectStore); + } + $count++; + } + $output->writeln('Moved ' . $count . ' users to ' . $objectStore . ' object store'); + + return 0; + } +} From 263a212b163a08fc6cd1fb288c09b2a169d83f87 Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Fri, 19 Dec 2025 13:12:19 +0800 Subject: [PATCH 3/5] refactor: remove rename command Signed-off-by: Kent Delante --- apps/files/appinfo/info.xml | 1 - .../composer/composer/autoload_classmap.php | 3 +- .../composer/composer/autoload_static.php | 1 - .../files/lib/Command/Object/Multi/Rename.php | 108 ------------------ 4 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 apps/files/lib/Command/Object/Multi/Rename.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index 46c1ed6bdd0cc..1e99cc5f78c07 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -45,7 +45,6 @@ OCA\Files\Command\Object\Get OCA\Files\Command\Object\Put OCA\Files\Command\Object\Multi\Users - OCA\Files\Command\Object\Multi\Rename OCA\Files\Command\Object\Multi\Move diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 00aad4ee74889..7bfef049de228 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -34,8 +34,7 @@ 'OCA\\Files\\Command\\Move' => $baseDir . '/../lib/Command/Move.php', 'OCA\\Files\\Command\\Object\\Delete' => $baseDir . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => $baseDir . '/../lib/Command/Object/Get.php', - 'OCA\\Files\\Command\\Object\\Multi\\Move' => $baseDir . '/..' . '/../lib/Command/Object/Multi/Move.php', - 'OCA\\Files\\Command\\Object\\Multi\\Rename' => $baseDir . '/../lib/Command/Object/Multi/Rename.php', + 'OCA\\Files\\Command\\Object\\Multi\\Move' => $baseDir . '/../lib/Command/Object/Multi/Move.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\\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 a14a44d556df6..e9772365a180d 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -50,7 +50,6 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Object\\Delete' => __DIR__ . '/..' . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => __DIR__ . '/..' . '/../lib/Command/Object/Get.php', 'OCA\\Files\\Command\\Object\\Multi\\Move' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/Move.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\\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 deleted file mode 100644 index 078ed54c198b6..0000000000000 --- a/apps/files/lib/Command/Object/Multi/Rename.php +++ /dev/null @@ -1,108 +0,0 @@ - - * 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 IDBConnection $connection, - private PrimaryObjectStoreConfig $objectStoreConfig, - private 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; - } -} From 32cb3bb59e2485eee0242300507a4be9dcef326f Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Fri, 19 Dec 2025 13:28:12 +0800 Subject: [PATCH 4/5] chore: run cs fixer Signed-off-by: Kent Delante --- apps/files/lib/Command/Object/Multi/Move.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/files/lib/Command/Object/Multi/Move.php b/apps/files/lib/Command/Object/Multi/Move.php index b56f075080b5a..a12c42e743faf 100644 --- a/apps/files/lib/Command/Object/Multi/Move.php +++ b/apps/files/lib/Command/Object/Multi/Move.php @@ -8,11 +8,10 @@ namespace OCA\Files\Command\Object\Multi; -use OCP\IConfig; -use OCP\IUser; -use OCP\IUserManager; use OC\Core\Command\Base; use OC\Files\ObjectStore\PrimaryObjectStoreConfig; +use OCP\IConfig; +use OCP\IUserManager; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; From fc3017650dd3398aa00d387a8ca22824ef380ade Mon Sep 17 00:00:00 2001 From: Kent Delante Date: Tue, 23 Dec 2025 15:38:56 +0800 Subject: [PATCH 5/5] feat: add pre-migrate command Signed-off-by: Kent Delante --- apps/files/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/Command/Object/Multi/PreMigrate.php | 76 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 apps/files/lib/Command/Object/Multi/PreMigrate.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index 1e99cc5f78c07..66bc84fb52a5f 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -46,6 +46,7 @@ OCA\Files\Command\Object\Put OCA\Files\Command\Object\Multi\Users OCA\Files\Command\Object\Multi\Move + OCA\Files\Command\Object\Multi\PreMigrate diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 7bfef049de228..3eaf12900193a 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -35,6 +35,7 @@ 'OCA\\Files\\Command\\Object\\Delete' => $baseDir . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => $baseDir . '/../lib/Command/Object/Get.php', 'OCA\\Files\\Command\\Object\\Multi\\Move' => $baseDir . '/../lib/Command/Object/Multi/Move.php', + 'OCA\\Files\\Command\\Object\\Multi\\PreMigrate' => $baseDir . '/../lib/Command/Object/Multi/PreMigrate.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\\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 e9772365a180d..5a919d03ad7ce 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -50,6 +50,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Object\\Delete' => __DIR__ . '/..' . '/../lib/Command/Object/Delete.php', 'OCA\\Files\\Command\\Object\\Get' => __DIR__ . '/..' . '/../lib/Command/Object/Get.php', 'OCA\\Files\\Command\\Object\\Multi\\Move' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/Move.php', + 'OCA\\Files\\Command\\Object\\Multi\\PreMigrate' => __DIR__ . '/..' . '/../lib/Command/Object/Multi/PreMigrate.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\\Put' => __DIR__ . '/..' . '/../lib/Command/Object/Put.php', diff --git a/apps/files/lib/Command/Object/Multi/PreMigrate.php b/apps/files/lib/Command/Object/Multi/PreMigrate.php new file mode 100644 index 0000000000000..aba38a3ac3611 --- /dev/null +++ b/apps/files/lib/Command/Object/Multi/PreMigrate.php @@ -0,0 +1,76 @@ +setName('files:object:multi:pre-migrate') + ->setDescription('Assign a configured object store to users who don\'t have one assigned yet.') + ->addOption('object-store', 'o', InputOption::VALUE_REQUIRED, 'The name of the configured object store') + ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'The userId of the user to assign the object store') + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Assign the object store to all users'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $objectStore = $input->getOption('object-store'); + if (!$objectStore) { + $output->writeln('Please specify the object store'); + return 1; + } + + $configs = $this->objectStoreConfig->getObjectStoreConfigs(); + if (!isset($configs[$objectStore])) { + $output->writeln('Unknown object store configuration: ' . $objectStore . ''); + return 1; + } + + if ($input->getOption('all')) { + $users = $this->userManager->getSeenUsers(); + } elseif ($userId = $input->getOption('user')) { + $user = $this->userManager->get($userId); + if (!$user) { + $output->writeln('User ' . $userId . ' not found'); + return 1; + } + $users = new \ArrayIterator([$user]); + } else { + $output->writeln('Please specify a user id with --user or --all for all users'); + return 1; + } + + $count = 0; + foreach ($users as $user) { + if (!$this->config->getUserValue($user->getUID(), 'homeobjectstore', 'objectstore', null)) { + $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'objectstore', $objectStore); + $count++; + } + } + $output->writeln('Assigned object store ' . $objectStore . ' to ' . $count . ' users'); + + return 0; + } +}