diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 00a92a932d..1b2ae9655a 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2989,6 +2989,11 @@ section + + + [] + + 0 diff --git a/core/Command/Preview/ResetRenderedTexts.php b/core/Command/Preview/ResetRenderedTexts.php new file mode 100644 index 0000000000..7881a21d27 --- /dev/null +++ b/core/Command/Preview/ResetRenderedTexts.php @@ -0,0 +1,202 @@ + + * + * @author Daniel Calviño Sánchez + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Core\Command\Preview; + +use OC\Preview\Storage\Root; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IAvatarManager; +use OCP\IDBConnection; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ResetRenderedTexts extends Command { + + /** @var IDBConnection */ + protected $connection; + + /** @var IUserManager */ + protected $userManager; + + /** @var IAvatarManager */ + protected $avatarManager; + + /** @var Root */ + private $previewFolder; + + /** @var IMimeTypeLoader */ + private $mimeTypeLoader; + + public function __construct(IDBConnection $connection, + IUserManager $userManager, + IAvatarManager $avatarManager, + Root $previewFolder, + IMimeTypeLoader $mimeTypeLoader) { + parent::__construct(); + + $this->connection = $connection; + $this->userManager = $userManager; + $this->avatarManager = $avatarManager; + $this->previewFolder = $previewFolder; + $this->mimeTypeLoader = $mimeTypeLoader; + } + + protected function configure() { + $this + ->setName('preview:reset-rendered-texts') + ->setDescription('Deletes all generated avatars and previews of text and md files') + ->addOption('dry', 'd', InputOption::VALUE_NONE, 'Dry mode - will not delete any files - in combination with the verbose mode one could check the operations.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $dryMode = $input->getOption('dry'); + + if ($dryMode) { + $output->writeln('INFO: The command is run in dry mode and will not modify anything.'); + $output->writeln(''); + } + + $this->deleteAvatars($output, $dryMode); + $this->deletePreviews($output, $dryMode); + + return 0; + } + + private function deleteAvatars(OutputInterface $output, bool $dryMode): void { + $avatarsToDeleteCount = 0; + + foreach ($this->getAvatarsToDelete() as [$userId, $avatar]) { + $output->writeln('Deleting avatar for ' . $userId, OutputInterface::VERBOSITY_VERBOSE); + + $avatarsToDeleteCount++; + + if ($dryMode) { + continue; + } + + try { + $avatar->remove(); + } catch (NotFoundException $e) { + // continue + } catch (NotPermittedException $e) { + // continue + } + } + + $output->writeln('Deleted ' . $avatarsToDeleteCount . ' avatars'); + $output->writeln(''); + } + + private function getAvatarsToDelete(): \Iterator { + foreach ($this->userManager->search('') as $user) { + $avatar = $this->avatarManager->getAvatar($user->getUID()); + + if (!$avatar->isCustomAvatar()) { + yield [$user->getUID(), $avatar]; + } + } + } + + private function deletePreviews(OutputInterface $output, bool $dryMode): void { + $previewsToDeleteCount = 0; + + foreach ($this->getPreviewsToDelete() as ['name' => $previewFileId, 'path' => $filePath]) { + $output->writeln('Deleting previews for ' . $filePath, OutputInterface::VERBOSITY_VERBOSE); + + $previewsToDeleteCount++; + + if ($dryMode) { + continue; + } + + try { + $preview = $this->previewFolder->getFolder((string)$previewFileId); + $preview->delete(); + } catch (NotFoundException $e) { + // continue + } catch (NotPermittedException $e) { + // continue + } + } + + $output->writeln('Deleted ' . $previewsToDeleteCount . ' previews'); + } + + // Copy pasted and adjusted from + // "lib/private/Preview/BackgroundCleanupJob.php". + private function getPreviewsToDelete(): \Iterator { + $qb = $this->connection->getQueryBuilder(); + $qb->select('path', 'mimetype') + ->from('filecache') + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($this->previewFolder->getId()))); + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === null) { + return []; + } + + /* + * This lovely like is the result of the way the new previews are stored + * We take the md5 of the name (fileid) and split the first 7 chars. That way + * there are not a gazillion files in the root of the preview appdata. + */ + $like = $this->connection->escapeLikeParameter($data['path']) . '/_/_/_/_/_/_/_/%'; + + $qb = $this->connection->getQueryBuilder(); + $qb->select('a.name', 'b.path') + ->from('filecache', 'a') + ->leftJoin('a', 'filecache', 'b', $qb->expr()->eq( + $qb->expr()->castColumn('a.name', IQueryBuilder::PARAM_INT), 'b.fileid' + )) + ->where( + $qb->expr()->andX( + $qb->expr()->like('a.path', $qb->createNamedParameter($like)), + $qb->expr()->eq('a.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))), + $qb->expr()->orX( + $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/plain'))), + $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/markdown'))), + $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/x-markdown'))) + ) + ) + ); + + $cursor = $qb->execute(); + + while ($row = $cursor->fetch()) { + yield $row; + } + + $cursor->closeCursor(); + } +} diff --git a/core/register_command.php b/core/register_command.php index 507dcfc3d4..dc3aa7c8c0 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -166,6 +166,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { )); $application->add(\OC::$server->query(\OC\Core\Command\Preview\Repair::class)); + $application->add(\OC::$server->query(\OC\Core\Command\Preview\ResetRenderedTexts::class)); $application->add(new OC\Core\Command\User\Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager())); $application->add(new OC\Core\Command\User\Delete(\OC::$server->getUserManager())); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c4ed69476c..0e64da4784 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -839,6 +839,7 @@ return array( 'OC\\Core\\Command\\Maintenance\\UpdateHtaccess' => $baseDir . '/core/Command/Maintenance/UpdateHtaccess.php', 'OC\\Core\\Command\\Maintenance\\UpdateTheme' => $baseDir . '/core/Command/Maintenance/UpdateTheme.php', 'OC\\Core\\Command\\Preview\\Repair' => $baseDir . '/core/Command/Preview/Repair.php', + 'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php', 'OC\\Core\\Command\\Security\\ImportCertificate' => $baseDir . '/core/Command/Security/ImportCertificate.php', 'OC\\Core\\Command\\Security\\ListCertificates' => $baseDir . '/core/Command/Security/ListCertificates.php', 'OC\\Core\\Command\\Security\\RemoveCertificate' => $baseDir . '/core/Command/Security/RemoveCertificate.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index edd8e1c681..13fb59ddfc 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -868,6 +868,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\Maintenance\\UpdateHtaccess' => __DIR__ . '/../../..' . '/core/Command/Maintenance/UpdateHtaccess.php', 'OC\\Core\\Command\\Maintenance\\UpdateTheme' => __DIR__ . '/../../..' . '/core/Command/Maintenance/UpdateTheme.php', 'OC\\Core\\Command\\Preview\\Repair' => __DIR__ . '/../../..' . '/core/Command/Preview/Repair.php', + 'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php', 'OC\\Core\\Command\\Security\\ImportCertificate' => __DIR__ . '/../../..' . '/core/Command/Security/ImportCertificate.php', 'OC\\Core\\Command\\Security\\ListCertificates' => __DIR__ . '/../../..' . '/core/Command/Security/ListCertificates.php', 'OC\\Core\\Command\\Security\\RemoveCertificate' => __DIR__ . '/../../..' . '/core/Command/Security/RemoveCertificate.php',