Skip to content

Commit cf7a539

Browse files
committed
Add command to scan trashbin for database inconsistencies
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
1 parent eddc6f2 commit cf7a539

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

apps/files_trashbin/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ To prevent a user from running out of disk space, the Deleted files app will not
3535
<command>OCA\Files_Trashbin\Command\ExpireTrash</command>
3636
<command>OCA\Files_Trashbin\Command\Size</command>
3737
<command>OCA\Files_Trashbin\Command\RestoreAllFiles</command>
38+
<command>OCA\Files_Trashbin\Command\ScanFileSystem</command>
3839
</commands>
3940

4041
<sabre>

apps/files_trashbin/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
'OCA\\Files_Trashbin\\Command\\Expire' => $baseDir . '/../lib/Command/Expire.php',
1515
'OCA\\Files_Trashbin\\Command\\ExpireTrash' => $baseDir . '/../lib/Command/ExpireTrash.php',
1616
'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => $baseDir . '/../lib/Command/RestoreAllFiles.php',
17+
'OCA\\Files_Trashbin\\Command\\ScanFileSystem' => $baseDir . '/../lib/Command/ScanFileSystem.php',
1718
'OCA\\Files_Trashbin\\Command\\Size' => $baseDir . '/../lib/Command/Size.php',
1819
'OCA\\Files_Trashbin\\Controller\\PreviewController' => $baseDir . '/../lib/Controller/PreviewController.php',
1920
'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => $baseDir . '/../lib/Events/MoveToTrashEvent.php',

apps/files_trashbin/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class ComposerStaticInitFiles_Trashbin
2929
'OCA\\Files_Trashbin\\Command\\Expire' => __DIR__ . '/..' . '/../lib/Command/Expire.php',
3030
'OCA\\Files_Trashbin\\Command\\ExpireTrash' => __DIR__ . '/..' . '/../lib/Command/ExpireTrash.php',
3131
'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => __DIR__ . '/..' . '/../lib/Command/RestoreAllFiles.php',
32+
'OCA\\Files_Trashbin\\Command\\ScanFileSystem' => __DIR__ . '/..' . '/../lib/Command/ScanFileSystem.php',
3233
'OCA\\Files_Trashbin\\Command\\Size' => __DIR__ . '/..' . '/../lib/Command/Size.php',
3334
'OCA\\Files_Trashbin\\Controller\\PreviewController' => __DIR__ . '/..' . '/../lib/Controller/PreviewController.php',
3435
'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/MoveToTrashEvent.php',
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023, Nextcloud, GmbH.
7+
*
8+
* @author Côme Chilliet <come.chilliet@nextcloud.com>
9+
*
10+
* @license AGPL-3.0-or-later
11+
*
12+
* This code is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License, version 3,
14+
* as published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License, version 3,
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>
23+
*
24+
*/
25+
26+
namespace OCA\Files_Trashbin\Command;
27+
28+
use OC\Core\Command\Base;
29+
use OCP\Files\IRootFolder;
30+
use OCP\IDBConnection;
31+
use OCP\IL10N;
32+
use OCP\IUserBackend;
33+
use OCA\Files_Trashbin\Trashbin;
34+
use OCA\Files_Trashbin\Helper;
35+
use OCP\IUserManager;
36+
use OCP\L10N\IFactory;
37+
use Symfony\Component\Console\Exception\InvalidOptionException;
38+
use Symfony\Component\Console\Input\InputArgument;
39+
use Symfony\Component\Console\Input\InputInterface;
40+
use Symfony\Component\Console\Input\InputOption;
41+
use Symfony\Component\Console\Output\OutputInterface;
42+
43+
class ScanFileSystem extends Base {
44+
protected IUserManager $userManager;
45+
protected IRootFolder $rootFolder;
46+
protected IDBConnection $dbConnection;
47+
protected IL10N $l10n;
48+
49+
public function __construct(
50+
IRootFolder $rootFolder,
51+
IUserManager $userManager,
52+
IDBConnection $dbConnection,
53+
IFactory $l10nFactory,
54+
) {
55+
parent::__construct();
56+
$this->userManager = $userManager;
57+
$this->rootFolder = $rootFolder;
58+
$this->dbConnection = $dbConnection;
59+
$this->l10n = $l10nFactory->get('files_trashbin');
60+
}
61+
62+
protected function configure(): void {
63+
parent::configure();
64+
$this
65+
->setName('trashbin:scan')
66+
->setDescription('Rescan trashbin for a user, and fix inconsistencies if needed')
67+
->addArgument(
68+
'user_id',
69+
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
70+
'scan all deleted files of the given user(s)'
71+
)
72+
->addOption(
73+
'all-users',
74+
null,
75+
InputOption::VALUE_NONE,
76+
'run action on all users'
77+
);
78+
}
79+
80+
protected function execute(InputInterface $input, OutputInterface $output): int {
81+
/** @var string[] $users */
82+
$users = $input->getArgument('user_id');
83+
if ((!empty($users)) and ($input->getOption('all-users'))) {
84+
throw new InvalidOptionException('Either specify a user_id or --all-users');
85+
} elseif (!empty($users)) {
86+
foreach ($users as $user) {
87+
if ($this->userManager->userExists($user)) {
88+
$output->writeln("Restoring deleted files for user <info>$user</info>");
89+
$this->scanDeletedFiles($user, $output);
90+
} else {
91+
$output->writeln("<error>Unknown user $user</error>");
92+
return 1;
93+
}
94+
}
95+
} elseif ($input->getOption('all-users')) {
96+
$output->writeln('Restoring deleted files for all users');
97+
foreach ($this->userManager->getBackends() as $backend) {
98+
$name = get_class($backend);
99+
if ($backend instanceof IUserBackend) {
100+
$name = $backend->getBackendName();
101+
}
102+
$output->writeln("Restoring deleted files for users on backend <info>$name</info>");
103+
$limit = 500;
104+
$offset = 0;
105+
do {
106+
$users = $backend->getUsers('', $limit, $offset);
107+
foreach ($users as $user) {
108+
$output->writeln("<info>$user</info>");
109+
$this->scanDeletedFiles($user, $output);
110+
}
111+
$offset += $limit;
112+
} while (count($users) >= $limit);
113+
}
114+
} else {
115+
throw new InvalidOptionException('Either specify a user_id or --all-users');
116+
}
117+
return 0;
118+
}
119+
120+
/**
121+
* Scan deleted files for the given user
122+
*/
123+
protected function scanDeletedFiles(string $uid, OutputInterface $output): void {
124+
\OC_Util::tearDownFS();
125+
\OC_Util::setupFS($uid);
126+
\OC_User::setUserId($uid);
127+
128+
$filesInTrash = Helper::getTrashFiles('/', $uid);
129+
$filesInTrashDatabase = Trashbin::getLocations($uid);
130+
131+
var_dump($filesInTrashDatabase);
132+
133+
$trashCount = count($filesInTrash);
134+
$trashCountDatabase = count($filesInTrashDatabase);
135+
if ($trashCount === 0 && $trashCountDatabase === 0) {
136+
$output->writeln("User has no deleted files in the trashbin");
137+
return;
138+
}
139+
$output->writeln("Preparing to scan <info>$trashCount</info> files...");
140+
$count = 0;
141+
foreach ($filesInTrash as $trashFile) {
142+
$filename = $trashFile->getName();
143+
$timestamp = $trashFile->getMtime();
144+
$humanTime = $this->l10n->l('datetime', $timestamp);
145+
if (isset($filesInTrashDatabase[$filename][$timestamp])) {
146+
$count++;
147+
$output->writeln("File <info>$filename</info> originally deleted at <info>$humanTime</info> is clean");
148+
unset($filesInTrashDatabase[$filename][$timestamp]);
149+
} else {
150+
$output->writeln("<error>File <info>$filename</info> originally deleted at <info>$humanTime</info> is missing from database</error>");
151+
}
152+
}
153+
154+
$output->writeln("Found <info>$count</info> clean files out of <info>$trashCount</info> files.");
155+
156+
$filesInTrashDatabase = array_filter($filesInTrashDatabase);
157+
158+
$trashCountDatabase = count($filesInTrashDatabase);
159+
160+
$output->writeln("Found <info>$trashCountDatabase</info> files in database missing from storage.");
161+
}
162+
}

0 commit comments

Comments
 (0)