Skip to content

Commit 3f404eb

Browse files
Merge pull request #993 from nextcloud/feat/add-export-date-and-manage-command
2 parents 1ed957d + 5e0c9bd commit 3f404eb

File tree

5 files changed

+152
-1
lines changed

5 files changed

+152
-1
lines changed

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

tests/stubs/stub.phpstub

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ namespace OC\Cache {
125125
namespace OC\Core\Command {
126126
use Symfony\Component\Console\Input\InputInterface;
127127
use Symfony\Component\Console\Output\OutputInterface;
128+
use Symfony\Component\Console\Command\Command;
128129

129-
class Base {
130+
class Base extends Command {
130131
public const OUTPUT_FORMAT_PLAIN = 'plain';
131132
public const OUTPUT_FORMAT_JSON = 'json';
132133
public const OUTPUT_FORMAT_JSON_PRETTY = 'json_pretty';
@@ -139,5 +140,6 @@ namespace OC\Core\Command {
139140
public function setName(string $name) {}
140141
public function getHelper(string $name) {}
141142
protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, array $items, string $prefix = ' - ') {}
143+
protected function writeTableInOutputFormat(InputInterface $input, OutputInterface $output, array $items): void {}
142144
}
143145
}

0 commit comments

Comments
 (0)