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."); + } +}