ScanAppData.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  7. * @author J0WI <J0WI@users.noreply.github.com>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Joel S <joel.devbox@protonmail.com>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Erik Wouters <6179932+EWouters@users.noreply.github.com>
  13. *
  14. * @license GNU AGPL version 3 or any later version
  15. *
  16. * This program is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License as
  18. * published by the Free Software Foundation, either version 3 of the
  19. * License, or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  28. *
  29. */
  30. namespace OCA\Files\Command;
  31. use OC\Core\Command\Base;
  32. use OC\Core\Command\InterruptedException;
  33. use OC\DB\Connection;
  34. use OC\DB\ConnectionAdapter;
  35. use OC\ForbiddenException;
  36. use OCP\EventDispatcher\IEventDispatcher;
  37. use OCP\Files\IRootFolder;
  38. use OCP\Files\Node;
  39. use OCP\Files\NotFoundException;
  40. use OCP\Files\StorageNotAvailableException;
  41. use OCP\IConfig;
  42. use Psr\Log\LoggerInterface;
  43. use Symfony\Component\Console\Helper\Table;
  44. use Symfony\Component\Console\Input\InputArgument;
  45. use Symfony\Component\Console\Input\InputInterface;
  46. use Symfony\Component\Console\Output\OutputInterface;
  47. class ScanAppData extends Base {
  48. protected float $execTime = 0;
  49. protected int $foldersCounter = 0;
  50. protected int $filesCounter = 0;
  51. public function __construct(
  52. protected IRootFolder $rootFolder,
  53. protected IConfig $config,
  54. ) {
  55. parent::__construct();
  56. }
  57. protected function configure(): void {
  58. parent::configure();
  59. $this
  60. ->setName('files:scan-app-data')
  61. ->setDescription('rescan the AppData folder');
  62. $this->addArgument('folder', InputArgument::OPTIONAL, 'The appdata subfolder to scan', '');
  63. }
  64. protected function scanFiles(OutputInterface $output, string $folder): int {
  65. try {
  66. /** @var \OCP\Files\Folder $appData */
  67. $appData = $this->getAppDataFolder();
  68. } catch (NotFoundException $e) {
  69. $output->writeln('<error>NoAppData folder found</error>');
  70. return self::FAILURE;
  71. }
  72. if ($folder !== '') {
  73. try {
  74. $appData = $appData->get($folder);
  75. } catch (NotFoundException $e) {
  76. $output->writeln('<error>Could not find folder: ' . $folder . '</error>');
  77. return self::FAILURE;
  78. }
  79. }
  80. $connection = $this->reconnectToDatabase($output);
  81. $scanner = new \OC\Files\Utils\Scanner(
  82. null,
  83. new ConnectionAdapter($connection),
  84. \OC::$server->query(IEventDispatcher::class),
  85. \OC::$server->get(LoggerInterface::class)
  86. );
  87. # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
  88. $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
  89. $output->writeln("\tFile <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
  90. ++$this->filesCounter;
  91. $this->abortIfInterrupted();
  92. });
  93. $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
  94. $output->writeln("\tFolder <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
  95. ++$this->foldersCounter;
  96. $this->abortIfInterrupted();
  97. });
  98. $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
  99. $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
  100. });
  101. $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
  102. $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
  103. });
  104. try {
  105. $scanner->scan($appData->getPath());
  106. } catch (ForbiddenException $e) {
  107. $output->writeln('<error>Storage not writable</error>');
  108. $output->writeln('<info>Make sure you\'re running the scan command only as the user the web server runs as</info>');
  109. return self::FAILURE;
  110. } catch (InterruptedException $e) {
  111. # exit the function if ctrl-c has been pressed
  112. $output->writeln('<info>Interrupted by user</info>');
  113. return self::FAILURE;
  114. } catch (NotFoundException $e) {
  115. $output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
  116. return self::FAILURE;
  117. } catch (\Exception $e) {
  118. $output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
  119. $output->writeln('<error>' . $e->getTraceAsString() . '</error>');
  120. return self::FAILURE;
  121. }
  122. return self::SUCCESS;
  123. }
  124. protected function execute(InputInterface $input, OutputInterface $output): int {
  125. # restrict the verbosity level to VERBOSITY_VERBOSE
  126. if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
  127. $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
  128. }
  129. $output->writeln('Scanning AppData for files');
  130. $output->writeln('');
  131. $folder = $input->getArgument('folder');
  132. $this->initTools();
  133. $exitCode = $this->scanFiles($output, $folder);
  134. if ($exitCode === 0) {
  135. $this->presentStats($output);
  136. }
  137. return $exitCode;
  138. }
  139. /**
  140. * Initialises some useful tools for the Command
  141. */
  142. protected function initTools(): void {
  143. // Start the timer
  144. $this->execTime = -microtime(true);
  145. // Convert PHP errors to exceptions
  146. set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
  147. }
  148. /**
  149. * Processes PHP errors as exceptions in order to be able to keep track of problems
  150. *
  151. * @see https://www.php.net/manual/en/function.set-error-handler.php
  152. *
  153. * @param int $severity the level of the error raised
  154. * @param string $message
  155. * @param string $file the filename that the error was raised in
  156. * @param int $line the line number the error was raised
  157. *
  158. * @throws \ErrorException
  159. */
  160. public function exceptionErrorHandler($severity, $message, $file, $line) {
  161. if (!(error_reporting() & $severity)) {
  162. // This error code is not included in error_reporting
  163. return;
  164. }
  165. throw new \ErrorException($message, 0, $severity, $file, $line);
  166. }
  167. protected function presentStats(OutputInterface $output): void {
  168. // Stop the timer
  169. $this->execTime += microtime(true);
  170. $headers = [
  171. 'Folders', 'Files', 'Elapsed time'
  172. ];
  173. $this->showSummary($headers, null, $output);
  174. }
  175. /**
  176. * Shows a summary of operations
  177. *
  178. * @param string[] $headers
  179. * @param string[] $rows
  180. */
  181. protected function showSummary($headers, $rows, OutputInterface $output): void {
  182. $niceDate = $this->formatExecTime();
  183. if (!$rows) {
  184. $rows = [
  185. $this->foldersCounter,
  186. $this->filesCounter,
  187. $niceDate,
  188. ];
  189. }
  190. $table = new Table($output);
  191. $table
  192. ->setHeaders($headers)
  193. ->setRows([$rows]);
  194. $table->render();
  195. }
  196. /**
  197. * Formats microtime into a human-readable format
  198. */
  199. protected function formatExecTime(): string {
  200. $secs = round($this->execTime);
  201. # convert seconds into HH:MM:SS form
  202. return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), (int)$secs % 60);
  203. }
  204. protected function reconnectToDatabase(OutputInterface $output): Connection {
  205. /** @var Connection $connection*/
  206. $connection = \OC::$server->get(Connection::class);
  207. try {
  208. $connection->close();
  209. } catch (\Exception $ex) {
  210. $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
  211. }
  212. while (!$connection->isConnected()) {
  213. try {
  214. $connection->connect();
  215. } catch (\Exception $ex) {
  216. $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
  217. sleep(60);
  218. }
  219. }
  220. return $connection;
  221. }
  222. /**
  223. * @throws NotFoundException
  224. */
  225. private function getAppDataFolder(): Node {
  226. $instanceId = $this->config->getSystemValue('instanceid', null);
  227. if ($instanceId === null) {
  228. throw new NotFoundException();
  229. }
  230. return $this->rootFolder->get('appdata_'.$instanceId);
  231. }
  232. }