mirror of https://github.com/nextcloud/server.git
feat: add command to scan external storages directly
the main use case of this over simply scanning through is the ability to provide a username and/or password for cases where login credentials are used Signed-off-by: Robin Appelman <robin@icewind.nl>pull/25109/head
parent
abfbe67ec9
commit
42e14cc4c7
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Command;
|
||||
|
||||
use OC\Files\Cache\Scanner;
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Scan extends StorageAuthBase {
|
||||
protected float $execTime = 0;
|
||||
protected int $foldersCounter = 0;
|
||||
protected int $filesCounter = 0;
|
||||
|
||||
public function __construct(
|
||||
GlobalStoragesService $globalService,
|
||||
IUserManager $userManager
|
||||
) {
|
||||
parent::__construct($globalService, $userManager);
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('files_external:scan')
|
||||
->setDescription('Scan an external storage for changed files')
|
||||
->addArgument(
|
||||
'mount_id',
|
||||
InputArgument::REQUIRED,
|
||||
'the mount id of the mount to scan'
|
||||
)->addOption(
|
||||
'user',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
|
||||
)->addOption(
|
||||
'password',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
|
||||
)->addOption(
|
||||
'path',
|
||||
'',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The path in the storage to scan',
|
||||
''
|
||||
);
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
[, $storage] = $this->createStorage($input, $output);
|
||||
if ($storage === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$path = $input->getOption('path');
|
||||
|
||||
$this->execTime = -microtime(true);
|
||||
|
||||
/** @var Scanner $scanner */
|
||||
$scanner = $storage->getScanner();
|
||||
|
||||
$scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output) {
|
||||
$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
|
||||
++$this->filesCounter;
|
||||
$this->abortIfInterrupted();
|
||||
});
|
||||
|
||||
$scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output) {
|
||||
$output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
|
||||
++$this->foldersCounter;
|
||||
$this->abortIfInterrupted();
|
||||
});
|
||||
|
||||
$scanner->scan($path);
|
||||
|
||||
$this->presentStats($output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
protected function presentStats(OutputInterface $output): void {
|
||||
// Stop the timer
|
||||
$this->execTime += microtime(true);
|
||||
|
||||
$headers = [
|
||||
'Folders', 'Files', 'Elapsed time'
|
||||
];
|
||||
|
||||
$this->showSummary($headers, [], $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a summary of operations
|
||||
*
|
||||
* @param string[] $headers
|
||||
* @param string[] $rows
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
protected function showSummary(array $headers, array $rows, OutputInterface $output): void {
|
||||
$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(): string {
|
||||
$secs = round($this->execTime);
|
||||
# convert seconds into HH:MM:SS form
|
||||
return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Command;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCA\Files_External\NotFoundException;
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
use OCP\Files\StorageNotAvailableException;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
abstract class StorageAuthBase extends Base {
|
||||
public function __construct(
|
||||
protected GlobalStoragesService $globalService,
|
||||
protected IUserManager $userManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
private function getUserOption(InputInterface $input): ?string {
|
||||
if ($input->getOption('user')) {
|
||||
return (string)$input->getOption('user');
|
||||
}
|
||||
|
||||
return $_ENV['NOTIFY_USER'] ?? $_SERVER['NOTIFY_USER'] ?? null;
|
||||
}
|
||||
|
||||
private function getPasswordOption(InputInterface $input): ?string {
|
||||
if ($input->getOption('password')) {
|
||||
return (string)$input->getOption('password');
|
||||
}
|
||||
|
||||
return $_ENV['NOTIFY_PASSWORD'] ?? $_SERVER['NOTIFY_PASSWORD'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return array
|
||||
* @psalm-return array{0: StorageConfig, 1: IStorage}|array{0: null, 1: null}
|
||||
*/
|
||||
protected function createStorage(InputInterface $input, OutputInterface $output): array {
|
||||
try {
|
||||
/** @var StorageConfig|null $mount */
|
||||
$mount = $this->globalService->getStorage($input->getArgument('mount_id'));
|
||||
} catch (NotFoundException $e) {
|
||||
$output->writeln('<error>Mount not found</error>');
|
||||
return [null, null];
|
||||
}
|
||||
if (is_null($mount)) {
|
||||
$output->writeln('<error>Mount not found</error>');
|
||||
return [null, null];
|
||||
}
|
||||
$noAuth = false;
|
||||
|
||||
$userOption = $this->getUserOption($input);
|
||||
$passwordOption = $this->getPasswordOption($input);
|
||||
|
||||
// if only the user is provided, we get the user object to pass along to the auth backend
|
||||
// this allows using saved user credentials
|
||||
$user = ($userOption && !$passwordOption) ? $this->userManager->get($userOption) : null;
|
||||
|
||||
try {
|
||||
$authBackend = $mount->getAuthMechanism();
|
||||
$authBackend->manipulateStorageConfig($mount, $user);
|
||||
} catch (InsufficientDataForMeaningfulAnswerException $e) {
|
||||
$noAuth = true;
|
||||
} catch (StorageNotAvailableException $e) {
|
||||
$noAuth = true;
|
||||
}
|
||||
|
||||
if ($userOption) {
|
||||
$mount->setBackendOption('user', $userOption);
|
||||
}
|
||||
if ($passwordOption) {
|
||||
$mount->setBackendOption('password', $passwordOption);
|
||||
}
|
||||
|
||||
try {
|
||||
$backend = $mount->getBackend();
|
||||
$backend->manipulateStorageConfig($mount, $user);
|
||||
} catch (InsufficientDataForMeaningfulAnswerException $e) {
|
||||
$noAuth = true;
|
||||
} catch (StorageNotAvailableException $e) {
|
||||
$noAuth = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$class = $mount->getBackend()->getStorageClass();
|
||||
/** @var IStorage $storage */
|
||||
$storage = new $class($mount->getBackendOptions());
|
||||
if (!$storage->test()) {
|
||||
throw new \Exception();
|
||||
}
|
||||
return [$mount, $storage];
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Error while trying to create storage</error>');
|
||||
if ($noAuth) {
|
||||
$output->writeln('<error>Username and/or password required</error>');
|
||||
}
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue