* @author Jörn Friedrich Dreyer * @author martin.mattel@diemattels.at * @author Morris Jobke * @author Robin Appelman * @author Vincent Petry * * @copyright Copyright (c) 2016, ownCloud, Inc. * @license AGPL-3.0 * * 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\Command; use OC\ForbiddenException; use OCP\Files\StorageNotAvailableException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Helper\Table; class Scan extends Command { /** * @var \OC\User\Manager $userManager */ private $userManager; /** @var float */ protected $execTime = 0; /** @var int */ protected $foldersCounter = 0; /** @var int */ protected $filesCounter = 0; /** @var bool */ protected $interrupted = false; /** @var bool */ protected $php_pcntl_signal = true; public function __construct(\OC\User\Manager $userManager) { $this->userManager = $userManager; parent::__construct(); } protected function configure() { $this ->setName('files:scan') ->setDescription('rescan filesystem') ->addArgument( 'user_id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'will rescan all files of the given user(s)' ) ->addOption( 'path', 'p', InputArgument::OPTIONAL, 'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored' ) ->addOption( 'quiet', 'q', InputOption::VALUE_NONE, 'suppress any output' ) ->addOption( 'verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'verbose the output' ) ->addOption( 'all', null, InputOption::VALUE_NONE, 'will rescan all files of all known users' ); } protected function scanFiles($user, $path, $verbose, OutputInterface $output) { $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger()); # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exeption # printout and count if ($verbose) { $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { $output->writeln("\tFile $path"); $this->filesCounter += 1; if ($this->hasBeenInterrupted()) { throw new \Exception('ctrl-c'); } }); $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { $output->writeln("\tFolder $path"); $this->foldersCounter += 1; if ($this->hasBeenInterrupted()) { throw new \Exception('ctrl-c'); } }); $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) { $output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")"); }); # count only } else { $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { $this->filesCounter += 1; if ($this->hasBeenInterrupted()) { throw new \Exception('ctrl-c'); } }); $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { $this->foldersCounter += 1; if ($this->hasBeenInterrupted()) { throw new \Exception('ctrl-c'); } }); } try { $scanner->scan($path); } catch (ForbiddenException $e) { $output->writeln("Home storage for user $user not writable"); $output->writeln("Make sure you're running the scan command only as the user the web server runs as"); } catch (\Exception $e) { # exit the function if ctrl-c has been pressed return; } } protected function execute(InputInterface $input, OutputInterface $output) { $inputPath = $input->getOption('path'); if ($inputPath) { $inputPath = '/' . trim($inputPath, '/'); list (, $user,) = explode('/', $inputPath, 3); $users = array($user); } else if ($input->getOption('all')) { $users = $this->userManager->search(''); } else { $users = $input->getArgument('user_id'); } # no messaging level option means: no full printout but statistics # $quiet means no print at all # $verbose means full printout including statistics # -q -v full stat # 0 0 no yes # 0 1 yes yes # 1 -- no no (quiet overrules verbose) $verbose = $input->getOption('verbose'); $quiet = $input->getOption('quiet'); # restrict the verbosity level to VERBOSITY_VERBOSE if ($output->getVerbosity()>OutputInterface::VERBOSITY_VERBOSE) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); } if ($quiet) { $verbose = false; } # check quantity of users to be process and show it on the command line $users_total = count($users); if ($users_total === 0) { $output->writeln("Please specify the user id to scan, \"--all\" to scan for all users or \"--path=...\""); return; } else { if ($users_total > 1) { $output->writeln("\nScanning files for $users_total users"); } } $this->initTools(); $user_count = 0; foreach ($users as $user) { if (is_object($user)) { $user = $user->getUID(); } $path = $inputPath ? $inputPath : '/' . $user; $user_count += 1; if ($this->userManager->userExists($user)) { # add an extra line when verbose is set to optical seperate users if ($verbose) {$output->writeln(""); } $output->writeln("Starting scan for user $user_count out of $users_total ($user)"); # full: printout data if $verbose was set $this->scanFiles($user, $path, $verbose, $output); } else { $output->writeln("Unknown user $user_count $user"); } # check on each user if there was a user interrupt (ctrl-c) and exit foreach if ($this->hasBeenInterrupted()) { break; } } # stat: printout statistics if $quiet was not set if (!$quiet) { $this->presentStats($output); } } /** * Initialises some useful tools for the Command */ protected function initTools() { // Start the timer $this->execTime = -microtime(true); // Convert PHP errors to exceptions set_error_handler([$this, 'exceptionErrorHandler'], E_ALL); // check if the php pcntl_signal functions are accessible if (function_exists('pcntl_signal')) { // Collect interrupts and notify the running command pcntl_signal(SIGTERM, [$this, 'cancelOperation']); pcntl_signal(SIGINT, [$this, 'cancelOperation']); } else { $this->php_pcntl_signal = false; } } /** * Changes the status of the command to "interrupted" if ctrl-c has been pressed * * Gives a chance to the command to properly terminate what it's doing */ private function cancelOperation() { $this->interrupted = true; } /** * @return bool */ protected function hasBeenInterrupted() { // return always false if pcntl_signal functions are not accessible if ($this->php_pcntl_signal) { pcntl_signal_dispatch(); if ($this->interrupted) { return true; } else { return false; } } else { return false; } } /** * Processes PHP errors as exceptions in order to be able to keep track of problems * * @see https://secure.php.net/manual/en/function.set-error-handler.php * * @param int $severity the level of the error raised * @param string $message * @param string $file the filename that the error was raised in * @param int $line the line number the error was raised * * @throws \ErrorException */ public function exceptionErrorHandler($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting return; } throw new \ErrorException($message, 0, $severity, $file, $line); } /** * @param OutputInterface $output */ protected function presentStats(OutputInterface $output) { // Stop the timer $this->execTime += microtime(true); $output->writeln(""); $headers = [ 'Folders', 'Files', 'Elapsed time' ]; $this->showSummary($headers, null, $output); } /** * Shows a summary of operations * * @param string[] $headers * @param string[] $rows * @param OutputInterface $output */ protected function showSummary($headers, $rows, OutputInterface $output) { $niceDate = $this->formatExecTime(); if (!$rows) { $rows = [ $this->foldersCounter, $this->filesCounter, $niceDate, ]; } $table = new Table($output); $table ->setHeaders($headers) ->setRows([$rows]); $table->render(); } /** * Formats microtime into a human readable format * * @return string */ protected function formatExecTime() { list($secs, $tens) = explode('.', sprintf("%.1f", ($this->execTime))); # add the following to $niceDate if you want to have microsecons added: . '.' . $tens; $niceDate = date('H:i:s', $secs); return $niceDate; } }