Repair.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
  5. *
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  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
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OC\Core\Command\Preview;
  26. use bantu\IniGetWrapper\IniGetWrapper;
  27. use OC\Preview\Storage\Root;
  28. use OCP\Files\Folder;
  29. use OCP\Files\IRootFolder;
  30. use OCP\Files\NotFoundException;
  31. use OCP\IConfig;
  32. use OCP\ILogger;
  33. use OCP\Lock\ILockingProvider;
  34. use OCP\Lock\LockedException;
  35. use Symfony\Component\Console\Command\Command;
  36. use Symfony\Component\Console\Helper\ProgressBar;
  37. use Symfony\Component\Console\Input\InputInterface;
  38. use Symfony\Component\Console\Input\InputOption;
  39. use Symfony\Component\Console\Output\OutputInterface;
  40. use Symfony\Component\Console\Question\ConfirmationQuestion;
  41. class Repair extends Command {
  42. /** @var IConfig */
  43. protected $config;
  44. /** @var IRootFolder */
  45. private $rootFolder;
  46. /** @var ILogger */
  47. private $logger;
  48. /** @var bool */
  49. private $stopSignalReceived = false;
  50. /** @var int */
  51. private $memoryLimit;
  52. /** @var int */
  53. private $memoryTreshold;
  54. /** @var ILockingProvider */
  55. private $lockingProvider;
  56. public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni, ILockingProvider $lockingProvider) {
  57. $this->config = $config;
  58. $this->rootFolder = $rootFolder;
  59. $this->logger = $logger;
  60. $this->lockingProvider = $lockingProvider;
  61. $this->memoryLimit = $phpIni->getBytes('memory_limit');
  62. $this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024;
  63. parent::__construct();
  64. }
  65. protected function configure() {
  66. $this
  67. ->setName('preview:repair')
  68. ->setDescription('distributes the existing previews into subfolders')
  69. ->addOption('batch', 'b', InputOption::VALUE_NONE, 'Batch mode - will not ask to start the migration and start it right away.')
  70. ->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.');
  71. }
  72. protected function execute(InputInterface $input, OutputInterface $output): int {
  73. if ($this->memoryLimit !== -1) {
  74. $limitInMiB = round($this->memoryLimit / 1024 / 1024, 1);
  75. $thresholdInMiB = round($this->memoryTreshold / 1024 / 1024, 1);
  76. $output->writeln("Memory limit is $limitInMiB MiB");
  77. $output->writeln("Memory threshold is $thresholdInMiB MiB");
  78. $output->writeln("");
  79. $memoryCheckEnabled = true;
  80. } else {
  81. $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.");
  82. $output->writeln("");
  83. $memoryCheckEnabled = false;
  84. }
  85. $dryMode = $input->getOption('dry');
  86. if ($dryMode) {
  87. $output->writeln("INFO: The migration is run in dry mode and will not modify anything.");
  88. $output->writeln("");
  89. }
  90. $instanceId = $this->config->getSystemValueString('instanceid');
  91. $output->writeln("This will migrate all previews from the old preview location to the new one.");
  92. $output->writeln('');
  93. $output->writeln('Fetching previews that need to be migrated …');
  94. /** @var \OCP\Files\Folder $currentPreviewFolder */
  95. $currentPreviewFolder = $this->rootFolder->get("appdata_$instanceId/preview");
  96. $directoryListing = $currentPreviewFolder->getDirectoryListing();
  97. $total = count($directoryListing);
  98. /**
  99. * by default there could be 0-9 a-f and the old-multibucket folder which are all fine
  100. */
  101. if ($total < 18) {
  102. $directoryListing = array_filter($directoryListing, function ($dir) {
  103. if ($dir->getName() === 'old-multibucket') {
  104. return false;
  105. }
  106. // a-f can't be a file ID -> removing from migration
  107. if (preg_match('!^[a-f]$!', $dir->getName())) {
  108. return false;
  109. }
  110. if (preg_match('!^[0-9]$!', $dir->getName())) {
  111. // ignore folders that only has folders in them
  112. if ($dir instanceof Folder) {
  113. foreach ($dir->getDirectoryListing() as $entry) {
  114. if (!$entry instanceof Folder) {
  115. return true;
  116. }
  117. }
  118. return false;
  119. }
  120. }
  121. return true;
  122. });
  123. $total = count($directoryListing);
  124. }
  125. if ($total === 0) {
  126. $output->writeln("All previews are already migrated.");
  127. return 0;
  128. }
  129. $output->writeln("A total of $total preview files need to be migrated.");
  130. $output->writeln("");
  131. $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 fill finish the current batch and then stop the migration. This migration can then just be started and it will continue.");
  132. if ($input->getOption('batch')) {
  133. $output->writeln('Batch mode active: migration is started right away.');
  134. } else {
  135. $helper = $this->getHelper('question');
  136. $question = new ConfirmationQuestion('<info>Should the migration be started? (y/[n]) </info>', false);
  137. if (!$helper->ask($input, $output, $question)) {
  138. return 0;
  139. }
  140. }
  141. // register the SIGINT listener late in here to be able to exit in the early process of this command
  142. pcntl_signal(SIGINT, [$this, 'sigIntHandler']);
  143. $output->writeln("");
  144. $output->writeln("");
  145. $section1 = $output->section();
  146. $section2 = $output->section();
  147. $progressBar = new ProgressBar($section2, $total);
  148. $progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%");
  149. $time = (new \DateTime())->format('H:i:s');
  150. $progressBar->setMessage("$time Starting …");
  151. $progressBar->maxSecondsBetweenRedraws(0.2);
  152. $progressBar->start();
  153. foreach ($directoryListing as $oldPreviewFolder) {
  154. pcntl_signal_dispatch();
  155. $name = $oldPreviewFolder->getName();
  156. $time = (new \DateTime())->format('H:i:s');
  157. $section1->writeln("$time Migrating previews of file with fileId $name …");
  158. $progressBar->display();
  159. if ($this->stopSignalReceived) {
  160. $section1->writeln("$time Stopping migration …");
  161. return 0;
  162. }
  163. if (!$oldPreviewFolder instanceof Folder) {
  164. $section1->writeln(" Skipping non-folder $name …");
  165. $progressBar->advance();
  166. continue;
  167. }
  168. if ($name === 'old-multibucket') {
  169. $section1->writeln(" Skipping fallback mount point $name …");
  170. $progressBar->advance();
  171. continue;
  172. }
  173. if (in_array($name, ['a', 'b', 'c', 'd', 'e', 'f'])) {
  174. $section1->writeln(" Skipping hex-digit folder $name …");
  175. $progressBar->advance();
  176. continue;
  177. }
  178. if (!preg_match('!^\d+$!', $name)) {
  179. $section1->writeln(" Skipping non-numeric folder $name …");
  180. $progressBar->advance();
  181. continue;
  182. }
  183. $newFoldername = Root::getInternalFolder($name);
  184. $memoryUsage = memory_get_usage();
  185. if ($memoryCheckEnabled && $memoryUsage > $this->memoryTreshold) {
  186. $section1->writeln("");
  187. $section1->writeln("");
  188. $section1->writeln("");
  189. $section1->writeln(" Stopped process 25 MB before reaching the memory limit to avoid a hard crash.");
  190. $time = (new \DateTime())->format('H:i:s');
  191. $section1->writeln("$time Reached memory limit and stopped to avoid hard crash.");
  192. return 1;
  193. }
  194. $lockName = 'occ preview:repair lock ' . $oldPreviewFolder->getId();
  195. try {
  196. $section1->writeln(" Locking \"$lockName\" …", OutputInterface::VERBOSITY_VERBOSE);
  197. $this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
  198. } catch (LockedException $e) {
  199. $section1->writeln(" Skipping because it is locked - another process seems to work on this …");
  200. continue;
  201. }
  202. $previews = $oldPreviewFolder->getDirectoryListing();
  203. if ($previews !== []) {
  204. try {
  205. $this->rootFolder->get("appdata_$instanceId/preview/$newFoldername");
  206. } catch (NotFoundException $e) {
  207. $section1->writeln(" Create folder preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
  208. if (!$dryMode) {
  209. $this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername");
  210. }
  211. }
  212. foreach ($previews as $preview) {
  213. pcntl_signal_dispatch();
  214. $previewName = $preview->getName();
  215. if ($preview instanceof Folder) {
  216. $section1->writeln(" Skipping folder $name/$previewName …");
  217. $progressBar->advance();
  218. continue;
  219. }
  220. $section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
  221. if (!$dryMode) {
  222. try {
  223. $preview->move("appdata_$instanceId/preview/$newFoldername/$previewName");
  224. } catch (\Exception $e) {
  225. $this->logger->logException($e, ['app' => 'core', 'message' => "Failed to move preview from preview/$name/$previewName to preview/$newFoldername"]);
  226. }
  227. }
  228. }
  229. }
  230. if ($oldPreviewFolder->getDirectoryListing() === []) {
  231. $section1->writeln(" Delete empty folder preview/$name", OutputInterface::VERBOSITY_VERBOSE);
  232. if (!$dryMode) {
  233. try {
  234. $oldPreviewFolder->delete();
  235. } catch (\Exception $e) {
  236. $this->logger->logException($e, ['app' => 'core', 'message' => "Failed to delete empty folder preview/$name"]);
  237. }
  238. }
  239. }
  240. $this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
  241. $section1->writeln(" Unlocked", OutputInterface::VERBOSITY_VERBOSE);
  242. $section1->writeln(" Finished migrating previews of file with fileId $name …");
  243. $progressBar->advance();
  244. }
  245. $progressBar->finish();
  246. $output->writeln("");
  247. return 0;
  248. }
  249. protected function sigIntHandler() {
  250. echo "\nSignal received - will finish the step and then stop the migration.\n\n\n";
  251. $this->stopSignalReceived = true;
  252. }
  253. }