diff --git a/apps/files_trashbin/appinfo/info.xml b/apps/files_trashbin/appinfo/info.xml
index 0c690c1558ea8..e8581f34db706 100644
--- a/apps/files_trashbin/appinfo/info.xml
+++ b/apps/files_trashbin/appinfo/info.xml
@@ -35,6 +35,7 @@ To prevent a user from running out of disk space, the Deleted files app will not
OCA\Files_Trashbin\Command\ExpireTrash
OCA\Files_Trashbin\Command\Size
OCA\Files_Trashbin\Command\RestoreAllFiles
+ OCA\Files_Trashbin\Command\ScanFileSystem
diff --git a/apps/files_trashbin/composer/composer/autoload_classmap.php b/apps/files_trashbin/composer/composer/autoload_classmap.php
index 760044d4f87cb..f286036e59990 100644
--- a/apps/files_trashbin/composer/composer/autoload_classmap.php
+++ b/apps/files_trashbin/composer/composer/autoload_classmap.php
@@ -14,6 +14,7 @@
'OCA\\Files_Trashbin\\Command\\Expire' => $baseDir . '/../lib/Command/Expire.php',
'OCA\\Files_Trashbin\\Command\\ExpireTrash' => $baseDir . '/../lib/Command/ExpireTrash.php',
'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => $baseDir . '/../lib/Command/RestoreAllFiles.php',
+ 'OCA\\Files_Trashbin\\Command\\ScanFileSystem' => $baseDir . '/../lib/Command/ScanFileSystem.php',
'OCA\\Files_Trashbin\\Command\\Size' => $baseDir . '/../lib/Command/Size.php',
'OCA\\Files_Trashbin\\Controller\\PreviewController' => $baseDir . '/../lib/Controller/PreviewController.php',
'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => $baseDir . '/../lib/Events/MoveToTrashEvent.php',
diff --git a/apps/files_trashbin/composer/composer/autoload_static.php b/apps/files_trashbin/composer/composer/autoload_static.php
index ef52ac0e1e76d..5161b7a6be8d0 100644
--- a/apps/files_trashbin/composer/composer/autoload_static.php
+++ b/apps/files_trashbin/composer/composer/autoload_static.php
@@ -29,6 +29,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Command\\Expire' => __DIR__ . '/..' . '/../lib/Command/Expire.php',
'OCA\\Files_Trashbin\\Command\\ExpireTrash' => __DIR__ . '/..' . '/../lib/Command/ExpireTrash.php',
'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => __DIR__ . '/..' . '/../lib/Command/RestoreAllFiles.php',
+ 'OCA\\Files_Trashbin\\Command\\ScanFileSystem' => __DIR__ . '/..' . '/../lib/Command/ScanFileSystem.php',
'OCA\\Files_Trashbin\\Command\\Size' => __DIR__ . '/..' . '/../lib/Command/Size.php',
'OCA\\Files_Trashbin\\Controller\\PreviewController' => __DIR__ . '/..' . '/../lib/Controller/PreviewController.php',
'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/MoveToTrashEvent.php',
diff --git a/apps/files_trashbin/lib/Command/ScanFileSystem.php b/apps/files_trashbin/lib/Command/ScanFileSystem.php
new file mode 100644
index 0000000000000..e49e64b8494d4
--- /dev/null
+++ b/apps/files_trashbin/lib/Command/ScanFileSystem.php
@@ -0,0 +1,162 @@
+
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Files_Trashbin\Command;
+
+use OC\Core\Command\Base;
+use OCP\Files\IRootFolder;
+use OCP\IDBConnection;
+use OCP\IL10N;
+use OCP\IUserBackend;
+use OCA\Files_Trashbin\Trashbin;
+use OCA\Files_Trashbin\Helper;
+use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use Symfony\Component\Console\Exception\InvalidOptionException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ScanFileSystem extends Base {
+ protected IUserManager $userManager;
+ protected IRootFolder $rootFolder;
+ protected IDBConnection $dbConnection;
+ protected IL10N $l10n;
+
+ public function __construct(
+ IRootFolder $rootFolder,
+ IUserManager $userManager,
+ IDBConnection $dbConnection,
+ IFactory $l10nFactory,
+ ) {
+ parent::__construct();
+ $this->userManager = $userManager;
+ $this->rootFolder = $rootFolder;
+ $this->dbConnection = $dbConnection;
+ $this->l10n = $l10nFactory->get('files_trashbin');
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('trashbin:scan')
+ ->setDescription('Rescan trashbin for a user, and fix inconsistencies if needed')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'scan all deleted files of the given user(s)'
+ )
+ ->addOption(
+ 'all-users',
+ null,
+ InputOption::VALUE_NONE,
+ 'run action on all users'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ /** @var string[] $users */
+ $users = $input->getArgument('user_id');
+ if ((!empty($users)) and ($input->getOption('all-users'))) {
+ throw new InvalidOptionException('Either specify a user_id or --all-users');
+ } elseif (!empty($users)) {
+ foreach ($users as $user) {
+ if ($this->userManager->userExists($user)) {
+ $output->writeln("Restoring deleted files for user $user");
+ $this->scanDeletedFiles($user, $output);
+ } else {
+ $output->writeln("Unknown user $user");
+ return 1;
+ }
+ }
+ } elseif ($input->getOption('all-users')) {
+ $output->writeln('Restoring deleted files for all users');
+ foreach ($this->userManager->getBackends() as $backend) {
+ $name = get_class($backend);
+ if ($backend instanceof IUserBackend) {
+ $name = $backend->getBackendName();
+ }
+ $output->writeln("Restoring deleted files for users on backend $name");
+ $limit = 500;
+ $offset = 0;
+ do {
+ $users = $backend->getUsers('', $limit, $offset);
+ foreach ($users as $user) {
+ $output->writeln("$user");
+ $this->scanDeletedFiles($user, $output);
+ }
+ $offset += $limit;
+ } while (count($users) >= $limit);
+ }
+ } else {
+ throw new InvalidOptionException('Either specify a user_id or --all-users');
+ }
+ return 0;
+ }
+
+ /**
+ * Scan deleted files for the given user
+ */
+ protected function scanDeletedFiles(string $uid, OutputInterface $output): void {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($uid);
+ \OC_User::setUserId($uid);
+
+ $filesInTrash = Helper::getTrashFiles('/', $uid);
+ $filesInTrashDatabase = Trashbin::getLocations($uid);
+
+ var_dump($filesInTrashDatabase);
+
+ $trashCount = count($filesInTrash);
+ $trashCountDatabase = count($filesInTrashDatabase);
+ if ($trashCount === 0 && $trashCountDatabase === 0) {
+ $output->writeln("User has no deleted files in the trashbin");
+ return;
+ }
+ $output->writeln("Preparing to scan $trashCount files...");
+ $count = 0;
+ foreach ($filesInTrash as $trashFile) {
+ $filename = $trashFile->getName();
+ $timestamp = $trashFile->getMtime();
+ $humanTime = $this->l10n->l('datetime', $timestamp);
+ if (isset($filesInTrashDatabase[$filename][$timestamp])) {
+ $count++;
+ $output->writeln("File $filename originally deleted at $humanTime is clean");
+ unset($filesInTrashDatabase[$filename][$timestamp]);
+ } else {
+ $output->writeln("File $filename originally deleted at $humanTime is missing from database");
+ }
+ }
+
+ $output->writeln("Found $count clean files out of $trashCount files.");
+
+ $filesInTrashDatabase = array_filter($filesInTrashDatabase);
+
+ $trashCountDatabase = count($filesInTrashDatabase);
+
+ $output->writeln("Found $trashCountDatabase files in database missing from storage.");
+ }
+}