Skip to content

Commit 9d6f486

Browse files
committed
feat: Add user_migration:manage command and last export date
Exporting a user from occ now stores the date of last export in the database. A new command user_migration:manage allows to list exported users and delete them as a batch. Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
1 parent 945dde8 commit 9d6f486

4 files changed

Lines changed: 131 additions & 0 deletions

File tree

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This app allows users to easily migrate from one instance to another using an ex
4646
</dependencies>
4747

4848
<commands>
49+
<command>OCA\UserMigration\Command\Manage</command>
4950
<command>OCA\UserMigration\Command\Export</command>
5051
<command>OCA\UserMigration\Command\Import</command>
5152
</commands>

lib/Command/Export.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
namespace OCA\UserMigration\Command;
1111

1212
use OC\Core\Command\Base;
13+
use OCA\UserMigration\AppInfo\Application;
1314
use OCA\UserMigration\ExportDestination;
1415
use OCA\UserMigration\Service\UserMigrationService;
16+
use OCP\AppFramework\Utility\ITimeFactory;
17+
use OCP\IConfig;
1518
use OCP\IUser;
1619
use OCP\IUserManager;
1720
use OCP\UserMigration\IMigrator;
@@ -26,6 +29,8 @@ class Export extends Base {
2629
public function __construct(
2730
private IUserManager $userManager,
2831
private UserMigrationService $migrationService,
32+
private IConfig $config,
33+
private ITimeFactory $timeFactory,
2934
) {
3035
parent::__construct();
3136
}
@@ -173,6 +178,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
173178
if (rename($path, $finalPath) === false) {
174179
throw new \Exception('Failed to rename ' . basename($path) . ' to ' . basename($finalPath));
175180
}
181+
$this->config->setUserValue($user->getUID(), Application::APP_ID, 'lastExport', $this->timeFactory->getTime());
176182
$io->writeln("Export saved in $finalPath");
177183
} catch (\Exception $e) {
178184
if ($io->isDebug()) {

lib/Command/Import.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
namespace OCA\UserMigration\Command;
1111

12+
use OCA\UserMigration\AppInfo\Application;
1213
use OCA\UserMigration\ImportSource;
1314
use OCA\UserMigration\Service\UserMigrationService;
15+
use OCP\IConfig;
1416
use OCP\IUserManager;
1517
use Symfony\Component\Console\Command\Command;
1618
use Symfony\Component\Console\Input\InputArgument;
@@ -23,6 +25,7 @@ class Import extends Command {
2325
public function __construct(
2426
private IUserManager $userManager,
2527
private UserMigrationService $migrationService,
28+
private IConfig $config,
2629
) {
2730
parent::__construct();
2831
}
@@ -73,6 +76,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7376
$io->writeln("Importing from {$path}");
7477
$importSource = new ImportSource($path);
7578
$this->migrationService->import($importSource, $user, $io);
79+
/* Reset exported state of user after import */
80+
$this->config->deleteUserValue($user->getUID(), Application::APP_ID, 'lastExport');
7681
$io->writeln("Successfully imported from {$path}");
7782
} catch (\Exception $e) {
7883
if ($io->isDebug()) {

lib/Command/Manage.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-only
8+
*/
9+
10+
namespace OCA\UserMigration\Command;
11+
12+
use OC\Core\Command\Base;
13+
use OCA\UserMigration\AppInfo\Application;
14+
use OCA\UserMigration\Service\UserMigrationService;
15+
use OCP\AppFramework\Utility\ITimeFactory;
16+
use OCP\IConfig;
17+
use OCP\IDBConnection;
18+
use OCP\IUserManager;
19+
use Symfony\Component\Console\Helper\QuestionHelper;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Input\InputOption;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Console\Question\ConfirmationQuestion;
24+
25+
class Manage extends Base {
26+
public function __construct(
27+
private IDBConnection $connection,
28+
private IUserManager $userManager,
29+
private UserMigrationService $migrationService,
30+
private IConfig $config,
31+
private ITimeFactory $timeFactory,
32+
) {
33+
parent::__construct();
34+
}
35+
36+
protected function configure(): void {
37+
parent::configure();
38+
$this
39+
->setName('user_migration:manage')
40+
->setDescription('List users exported by the admin, delete them by batch')
41+
->addOption(
42+
'limit',
43+
'l',
44+
InputOption::VALUE_REQUIRED,
45+
'Limit the number of listed users',
46+
100,
47+
)
48+
->addOption(
49+
'delete',
50+
null,
51+
InputOption::VALUE_NONE,
52+
'Delete the exported users',
53+
);
54+
}
55+
56+
protected function execute(InputInterface $input, OutputInterface $output): int {
57+
$values = iterator_to_array($this->queryUsers((int)$input->getOption('limit')));
58+
$this->writeTableInOutputFormat($input, $output, $values);
59+
if ($input->getOption('delete')) {
60+
/** @var QuestionHelper $helper */
61+
$helper = $this->getHelper('question');
62+
$question = new ConfirmationQuestion('Please confirm to delete the above listed users [y/n]', !$input->isInteractive());
63+
64+
if (!$helper->ask($input, $output, $question)) {
65+
$output->writeln('<info>Deletion canceled</info>');
66+
return self::SUCCESS;
67+
}
68+
$errors = $this->deleteUsers(array_column($values, 'userid'), $output);
69+
if ($errors > 0) {
70+
return self::FAILURE;
71+
}
72+
}
73+
return self::SUCCESS;
74+
}
75+
76+
private function queryUsers(int $limit): \Generator {
77+
$qb = $this->connection->getQueryBuilder();
78+
$qb->select('userid', 'configvalue')
79+
->from('preferences')
80+
->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID)))
81+
->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lastExport')))
82+
->orderBy('configvalue')
83+
->setMaxResults($limit);
84+
85+
$result = $qb->executeQuery();
86+
87+
while ($row = $result->fetch()) {
88+
yield [
89+
'userid' => $row['userid'],
90+
'Last export' => date(\DateTimeInterface::ATOM, (int)$row['configvalue'])
91+
];
92+
}
93+
94+
$result->closeCursor();
95+
}
96+
97+
/**
98+
* @param iterable<string> $uids
99+
*/
100+
private function deleteUsers(iterable $uids, OutputInterface $output): int {
101+
$errors = 0;
102+
foreach ($uids as $uid) {
103+
$user = $this->userManager->get($uid);
104+
if (is_null($user)) {
105+
$output->writeln('<error>User ' . $uid . ' does not exist</error>');
106+
$errors++;
107+
continue;
108+
}
109+
110+
if ($user->delete()) {
111+
$output->writeln('<info>User "' . $uid . '" was deleted</info>');
112+
} else {
113+
$output->writeln('<error>User "' . $uid . '" could not be deleted. Please check the logs.</error>');
114+
$errors++;
115+
}
116+
}
117+
return $errors;
118+
}
119+
}

0 commit comments

Comments
 (0)