diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index c84819b9617..fb53cef79b8 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -54,6 +54,7 @@ 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 4d318bb80f0..0c0f734251f 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -37,6 +37,7 @@ 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', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 131c85bf757..19310ed4e92 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -52,6 +52,7 @@ 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', 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; + } +}