diff options
Diffstat (limited to 'core/Command/Preview/Repair.php')
-rw-r--r-- | core/Command/Preview/Repair.php | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/core/Command/Preview/Repair.php b/core/Command/Preview/Repair.php new file mode 100644 index 00000000000..a92a4cf8ed0 --- /dev/null +++ b/core/Command/Preview/Repair.php @@ -0,0 +1,293 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Preview; + +use bantu\IniGetWrapper\IniGetWrapper; +use OC\Preview\Storage\Root; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +use function pcntl_signal; + +class Repair extends Command { + private bool $stopSignalReceived = false; + private int $memoryLimit; + private int $memoryTreshold; + + public function __construct( + protected IConfig $config, + private IRootFolder $rootFolder, + private LoggerInterface $logger, + IniGetWrapper $phpIni, + private ILockingProvider $lockingProvider, + ) { + $this->memoryLimit = (int)$phpIni->getBytes('memory_limit'); + $this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024; + + parent::__construct(); + } + + protected function configure() { + $this + ->setName('preview:repair') + ->setDescription('distributes the existing previews into subfolders') + ->addOption('batch', 'b', InputOption::VALUE_NONE, 'Batch mode - will not ask to start the migration and start it right away.') + ->addOption('dry', 'd', InputOption::VALUE_NONE, 'Dry mode - will not create, move or delete any files - in combination with the verbose mode one could check the operations.') + ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete instead of migrating them. Usefull if too many entries to migrate.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->memoryLimit !== -1) { + $limitInMiB = round($this->memoryLimit / 1024 / 1024, 1); + $thresholdInMiB = round($this->memoryTreshold / 1024 / 1024, 1); + $output->writeln("Memory limit is $limitInMiB MiB"); + $output->writeln("Memory threshold is $thresholdInMiB MiB"); + $output->writeln(''); + $memoryCheckEnabled = true; + } else { + $output->writeln('No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit.'); + $output->writeln(''); + $memoryCheckEnabled = false; + } + + $dryMode = $input->getOption('dry'); + $deleteMode = $input->getOption('delete'); + + + if ($dryMode) { + $output->writeln('INFO: The migration is run in dry mode and will not modify anything.'); + $output->writeln(''); + } elseif ($deleteMode) { + $output->writeln('WARN: The migration will _DELETE_ old previews.'); + $output->writeln(''); + } + + $instanceId = $this->config->getSystemValueString('instanceid'); + + $output->writeln('This will migrate all previews from the old preview location to the new one.'); + $output->writeln(''); + + $output->writeln('Fetching previews that need to be migrated …'); + /** @var Folder $currentPreviewFolder */ + $currentPreviewFolder = $this->rootFolder->get("appdata_$instanceId/preview"); + + $directoryListing = $currentPreviewFolder->getDirectoryListing(); + + $total = count($directoryListing); + /** + * by default there could be 0-9 a-f and the old-multibucket folder which are all fine + */ + if ($total < 18) { + $directoryListing = array_filter($directoryListing, function ($dir) { + if ($dir->getName() === 'old-multibucket') { + return false; + } + + // a-f can't be a file ID -> removing from migration + if (preg_match('!^[a-f]$!', $dir->getName())) { + return false; + } + + if (preg_match('!^[0-9]$!', $dir->getName())) { + // ignore folders that only has folders in them + if ($dir instanceof Folder) { + foreach ($dir->getDirectoryListing() as $entry) { + if (!$entry instanceof Folder) { + return true; + } + } + return false; + } + } + return true; + }); + $total = count($directoryListing); + } + + if ($total === 0) { + $output->writeln('All previews are already migrated.'); + return 0; + } + + $output->writeln("A total of $total preview files need to be migrated."); + $output->writeln(''); + $output->writeln('The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This will finish the current batch and then stop the migration. This migration can then just be started and it will continue.'); + + if ($input->getOption('batch')) { + $output->writeln('Batch mode active: migration is started right away.'); + } else { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('<info>Should the migration be started? (y/[n]) </info>', false); + + if (!$helper->ask($input, $output, $question)) { + return 0; + } + } + + // register the SIGINT listener late in here to be able to exit in the early process of this command + pcntl_signal(SIGINT, [$this, 'sigIntHandler']); + + $output->writeln(''); + $output->writeln(''); + $section1 = $output->section(); + $section2 = $output->section(); + $progressBar = new ProgressBar($section2, $total); + $progressBar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%'); + $time = (new \DateTime())->format('H:i:s'); + $progressBar->setMessage("$time Starting …"); + $progressBar->maxSecondsBetweenRedraws(0.2); + $progressBar->start(); + + foreach ($directoryListing as $oldPreviewFolder) { + pcntl_signal_dispatch(); + $name = $oldPreviewFolder->getName(); + $time = (new \DateTime())->format('H:i:s'); + $section1->writeln("$time Migrating previews of file with fileId $name …"); + $progressBar->display(); + + if ($this->stopSignalReceived) { + $section1->writeln("$time Stopping migration …"); + return 0; + } + if (!$oldPreviewFolder instanceof Folder) { + $section1->writeln(" Skipping non-folder $name …"); + $progressBar->advance(); + continue; + } + if ($name === 'old-multibucket') { + $section1->writeln(" Skipping fallback mount point $name …"); + $progressBar->advance(); + continue; + } + if (in_array($name, ['a', 'b', 'c', 'd', 'e', 'f'])) { + $section1->writeln(" Skipping hex-digit folder $name …"); + $progressBar->advance(); + continue; + } + if (!preg_match('!^\d+$!', $name)) { + $section1->writeln(" Skipping non-numeric folder $name …"); + $progressBar->advance(); + continue; + } + + $newFoldername = Root::getInternalFolder($name); + + $memoryUsage = memory_get_usage(); + if ($memoryCheckEnabled && $memoryUsage > $this->memoryTreshold) { + $section1->writeln(''); + $section1->writeln(''); + $section1->writeln(''); + $section1->writeln(' Stopped process 25 MB before reaching the memory limit to avoid a hard crash.'); + $time = (new \DateTime())->format('H:i:s'); + $section1->writeln("$time Reached memory limit and stopped to avoid hard crash."); + return 1; + } + + $lockName = 'occ preview:repair lock ' . $oldPreviewFolder->getId(); + try { + $section1->writeln(" Locking \"$lockName\" …", OutputInterface::VERBOSITY_VERBOSE); + $this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE); + } catch (LockedException $e) { + $section1->writeln(' Skipping because it is locked - another process seems to work on this …'); + continue; + } + + $previews = $oldPreviewFolder->getDirectoryListing(); + if ($previews !== []) { + try { + $this->rootFolder->get("appdata_$instanceId/preview/$newFoldername"); + } catch (NotFoundException $e) { + $section1->writeln(" Create folder preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE); + if (!$dryMode) { + $this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername"); + } + } + + foreach ($previews as $preview) { + pcntl_signal_dispatch(); + $previewName = $preview->getName(); + + if ($preview instanceof Folder) { + $section1->writeln(" Skipping folder $name/$previewName …"); + $progressBar->advance(); + continue; + } + + // Execute process + if (!$dryMode) { + // Delete preview instead of moving + if ($deleteMode) { + try { + $section1->writeln(" Delete preview/$name/$previewName", OutputInterface::VERBOSITY_VERBOSE); + $preview->delete(); + } catch (\Exception $e) { + $this->logger->error("Failed to delete preview at preview/$name/$previewName", [ + 'app' => 'core', + 'exception' => $e, + ]); + } + } else { + try { + $section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE); + $preview->move("appdata_$instanceId/preview/$newFoldername/$previewName"); + } catch (\Exception $e) { + $this->logger->error("Failed to move preview from preview/$name/$previewName to preview/$newFoldername", [ + 'app' => 'core', + 'exception' => $e, + ]); + } + } + } + } + } + + if ($oldPreviewFolder->getDirectoryListing() === []) { + $section1->writeln(" Delete empty folder preview/$name", OutputInterface::VERBOSITY_VERBOSE); + if (!$dryMode) { + try { + $oldPreviewFolder->delete(); + } catch (\Exception $e) { + $this->logger->error("Failed to delete empty folder preview/$name", [ + 'app' => 'core', + 'exception' => $e, + ]); + } + } + } + + $this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE); + $section1->writeln(' Unlocked', OutputInterface::VERBOSITY_VERBOSE); + + $section1->writeln(" Finished migrating previews of file with fileId $name …"); + $progressBar->advance(); + } + + $progressBar->finish(); + $output->writeln(''); + return 0; + } + + protected function sigIntHandler() { + echo "\nSignal received - will finish the step and then stop the migration.\n\n\n"; + $this->stopSignalReceived = true; + } +} |