diff --git a/appinfo/info.xml b/appinfo/info.xml
index 35a9b216ad75861c84ebdedff327b494c024ec54..c5d99471fc0a49890896e628e77fe4f10bba069a 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -28,5 +28,7 @@
OCA\EcloudAccounts\Command\Migrate2FASecrets
OCA\EcloudAccounts\Command\MigrateWebmailAddressbooks
OCA\EcloudAccounts\Command\MapActiveAttributetoLDAP
+ OCA\EcloudAccounts\Command\Scan
+ OCA\EcloudAccounts\Command\FixEncryptedVersion
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 1beceac88e28707aaeaa730130a3730520d5f194..a19e8eca13dfcee837ce8502adb28d0b4c820882 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -42,7 +42,9 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\Files\Storage\IStorage;
+use OCP\IGroupManager;
use OCP\IUserManager;
+use OCP\IUserSession;
use OCP\User\Events\BeforeUserDeletedEvent;
use OCP\User\Events\PasswordUpdatedEvent;
use OCP\User\Events\UserChangedEvent;
@@ -62,7 +64,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(UserChangedEvent::class, UserChangedListener::class);
$context->registerEventListener(StateChanged::class, TwoFactorStateChangedListener::class);
$context->registerEventListener(PasswordUpdatedEvent::class, PasswordUpdatedListener::class);
-
+
$context->registerMiddleware(AccountMiddleware::class);
}
@@ -89,6 +91,25 @@ class Application extends App implements IBootstrap {
* @return StorageWrapper|IStorage
*/
public function addStorageWrapperCallback($mountPoint, IStorage $storage) {
+ if (\OC::$CLI && \OC::$REQUESTEDAPP === self::APP_ID) {
+ return $storage;
+ }
+
+ $userSession = \OC::$server->get(IUserSession::class);
+ $currentUser = $userSession->getUser();
+ if ($currentUser !== null) {
+ $groupManager = \OC::$server->get(IGroupManager::class);
+ $groups = $groupManager->getUserGroups($currentUser);
+
+ if (!empty($groups)) {
+ foreach ($groups as $group) {
+ if ($group->getGID() === "recovery_done") {
+ return $storage;
+ }
+ }
+ }
+ }
+
$instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', '');
$appdataFolder = 'appdata_' . $instanceId;
if ($mountPoint !== '/' && strpos($mountPoint, '/' . $appdataFolder) !== 0) {
diff --git a/lib/Command/FixEncryptedVersion.php b/lib/Command/FixEncryptedVersion.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd6e75c7985ffe21700480e9c0f45c129704d46f
--- /dev/null
+++ b/lib/Command/FixEncryptedVersion.php
@@ -0,0 +1,334 @@
+
+ * @author Ilja Neumann
+ *
+ * @copyright Copyright (c) 2019, ownCloud GmbH
+ * @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
+ *
+ */
+
+declare(strict_types=1);
+
+namespace OCA\EcloudAccounts\Command;
+
+use OC\Files\Storage\Wrapper\Encryption;
+use OC\Files\View;
+use OC\ServerNotAvailableException;
+use OCA\EcloudAccounts\AppInfo\Application;
+use OCA\Encryption\Util;
+use OCP\Files\IRootFolder;
+use OCP\HintException;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+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;
+
+class FixEncryptedVersion extends Command {
+ private bool $supportLegacy;
+
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private IRootFolder $rootFolder,
+ private IUserManager $userManager,
+ private Util $util,
+ private View $view,
+ ) {
+ $this->supportLegacy = false;
+
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName(Application::APP_ID . ':fix-encrypted-version')
+ ->setDescription('Fix the encrypted version if the encrypted file(s) are not downloadable.')
+ ->addArgument(
+ 'user',
+ InputArgument::OPTIONAL,
+ 'The id of the user whose files need fixing'
+ )->addOption(
+ 'path',
+ 'p',
+ InputOption::VALUE_REQUIRED,
+ 'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.'
+ )->addOption(
+ 'all',
+ null,
+ InputOption::VALUE_NONE,
+ 'Run the fix for all users on the system, mutually exclusive with specifying a user id.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ \OC::$REQUESTEDAPP = Application::APP_ID;
+ $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
+ $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
+
+ if ($skipSignatureCheck) {
+ $output->writeln("Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.\n");
+ return 1;
+ }
+
+ if (!$this->util->isMasterKeyEnabled()) {
+ $output->writeln("Repairing only works with master key encryption.\n");
+ return 1;
+ }
+
+ $user = $input->getArgument('user');
+ $all = $input->getOption('all');
+ $pathOption = \trim(($input->getOption('path') ?? ''), '/');
+
+ if ($user) {
+ if ($all) {
+ $output->writeln("Specifying a user id and --all are mutually exclusive");
+ return 1;
+ }
+
+ if ($this->userManager->get($user) === null) {
+ $output->writeln("User id $user does not exist. Please provide a valid user id");
+ return 1;
+ }
+
+ return $this->runForUser($user, $pathOption, $output);
+ } elseif ($all) {
+ $result = 0;
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($pathOption, $output, &$result) {
+ $output->writeln("Processing files for " . $user->getUID());
+ $result = $this->runForUser($user->getUID(), $pathOption, $output);
+ return $result === 0;
+ });
+ return $result;
+ } else {
+ $output->writeln("Either a user id or --all needs to be provided");
+ return 1;
+ }
+ }
+
+ private function runForUser(string $user, string $pathOption, OutputInterface $output): int {
+ $pathToWalk = "/$user/files";
+ if ($pathOption !== "") {
+ $pathToWalk = "$pathToWalk/$pathOption";
+ }
+ return $this->walkPathOfUser($user, $pathToWalk, $output);
+ }
+
+ /**
+ * @return int 0 for success, 1 for error
+ */
+ private function walkPathOfUser(string $user, string $path, OutputInterface $output): int {
+ $this->setupUserFs($user);
+ if (!$this->view->file_exists($path)) {
+ $output->writeln("Path \"$path\" does not exist. Please provide a valid path.");
+ return 1;
+ }
+
+ if ($this->view->is_file($path)) {
+ $output->writeln("Verifying the content of file \"$path\"");
+ $this->verifyFileContent($path, $output);
+ return 0;
+ }
+ $directories = [];
+ $directories[] = $path;
+ while ($root = \array_pop($directories)) {
+ $directoryContent = $this->view->getDirectoryContent($root);
+ foreach ($directoryContent as $file) {
+ $path = $root . '/' . $file['name'];
+ if ($this->view->is_dir($path)) {
+ $directories[] = $path;
+ } else {
+ $output->writeln("Verifying the content of file \"$path\"");
+ $this->verifyFileContent($path, $output);
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @param bool $ignoreCorrectEncVersionCall, setting this variable to false avoids recursion
+ */
+ private function verifyFileContent(string $path, OutputInterface $output, bool $ignoreCorrectEncVersionCall = true): bool {
+ try {
+ // since we're manually poking around the encrypted state we need to ensure that this isn't cached in the encryption wrapper
+ $mount = $this->view->getMount($path);
+ $storage = $mount->getStorage();
+ if ($storage && $storage->instanceOfStorage(Encryption::class)) {
+ $storage->clearIsEncryptedCache();
+ }
+
+ /**
+ * In encryption, the files are read in a block size of 8192 bytes
+ * Read block size of 8192 and a bit more (808 bytes)
+ * If there is any problem, the first block should throw the signature
+ * mismatch error. Which as of now, is enough to proceed ahead to
+ * correct the encrypted version.
+ */
+ $handle = $this->view->fopen($path, 'rb');
+
+ if ($handle === false) {
+ $output->writeln("Failed to open file: \"$path\" skipping");
+ return true;
+ }
+
+ if (\fread($handle, 9001) !== false) {
+ $fileInfo = $this->view->getFileInfo($path);
+ if (!$fileInfo) {
+ $output->writeln("File info not found for file: \"$path\"");
+ return true;
+ }
+ $encryptedVersion = $fileInfo->getEncryptedVersion();
+ $stat = $this->view->stat($path);
+ if (($encryptedVersion == 0) && isset($stat['hasHeader']) && ($stat['hasHeader'] == true)) {
+ // The file has encrypted to false but has an encryption header
+ if ($ignoreCorrectEncVersionCall === true) {
+ // Lets rectify the file by correcting encrypted version
+ $output->writeln("Attempting to fix the path: \"$path\"");
+ return $this->correctEncryptedVersion($path, $output);
+ }
+ return false;
+ }
+ $output->writeln("The file \"$path\" is: OK");
+ }
+
+ \fclose($handle);
+
+ return true;
+ } catch (ServerNotAvailableException $e) {
+ // not a "bad signature" error and likely "legacy cipher" exception
+ // this could mean that the file is maybe not encrypted but the encrypted version is set
+ if (!$this->supportLegacy && $ignoreCorrectEncVersionCall === true) {
+ $output->writeln("Attempting to fix the path: \"$path\"");
+ return $this->correctEncryptedVersion($path, $output, true);
+ }
+ return false;
+ } catch (HintException $e) {
+ $this->logger->warning("Issue: " . $e->getMessage());
+ // If allowOnce is set to false, this becomes recursive.
+ if ($ignoreCorrectEncVersionCall === true) {
+ // Lets rectify the file by correcting encrypted version
+ $output->writeln("Attempting to fix the path: \"$path\"");
+ return $this->correctEncryptedVersion($path, $output);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * @param bool $includeZero whether to try zero version for unencrypted file
+ */
+ private function correctEncryptedVersion(string $path, OutputInterface $output, bool $includeZero = false): bool {
+ $fileInfo = $this->view->getFileInfo($path);
+ if (!$fileInfo) {
+ $output->writeln("File info not found for file: \"$path\"");
+ return true;
+ }
+ $fileId = $fileInfo->getId();
+ if ($fileId === null) {
+ $output->writeln("File info contains no id for file: \"$path\"");
+ return true;
+ }
+ $encryptedVersion = $fileInfo->getEncryptedVersion();
+ $wrongEncryptedVersion = $encryptedVersion;
+
+ $storage = $fileInfo->getStorage();
+
+ $cache = $storage->getCache();
+ $fileCache = $cache->get($fileId);
+ if (!$fileCache) {
+ $output->writeln("File cache entry not found for file: \"$path\"");
+ return true;
+ }
+
+ if ($storage->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) {
+ $output->writeln("The file: \"$path\" is a share. Please also run the script for the owner of the share");
+ return true;
+ }
+
+ // Save original encrypted version so we can restore it if decryption fails with all version
+ $originalEncryptedVersion = $encryptedVersion;
+ if ($encryptedVersion >= 0) {
+ if ($includeZero) {
+ // try with zero first
+ $cacheInfo = ['encryptedVersion' => 0, 'encrypted' => 0];
+ $cache->put($fileCache->getPath(), $cacheInfo);
+ $output->writeln("Set the encrypted version to 0 (unencrypted)");
+ if ($this->verifyFileContent($path, $output, false) === true) {
+ $output->writeln("Fixed the file: \"$path\" with version 0 (unencrypted)");
+ return true;
+ }
+ }
+
+ // Test by decrementing the value till 1 and if nothing works try incrementing
+ $encryptedVersion--;
+ while ($encryptedVersion > 0) {
+ $cacheInfo = ['encryptedVersion' => $encryptedVersion, 'encrypted' => $encryptedVersion];
+ $cache->put($fileCache->getPath(), $cacheInfo);
+ $output->writeln("Decrement the encrypted version to $encryptedVersion");
+ if ($this->verifyFileContent($path, $output, false) === true) {
+ $output->writeln("Fixed the file: \"$path\" with version " . $encryptedVersion . "");
+ return true;
+ }
+ $encryptedVersion--;
+ }
+
+ // So decrementing did not work. Now lets increment. Max increment is till 5
+ $increment = 1;
+ while ($increment <= 5) {
+ /**
+ * The wrongEncryptedVersion would not be incremented so nothing to worry about here.
+ * Only the newEncryptedVersion is incremented.
+ * For example if the wrong encrypted version is 4 then
+ * cycle1 -> newEncryptedVersion = 5 ( 4 + 1)
+ * cycle2 -> newEncryptedVersion = 6 ( 4 + 2)
+ * cycle3 -> newEncryptedVersion = 7 ( 4 + 3)
+ */
+ $newEncryptedVersion = $wrongEncryptedVersion + $increment;
+
+ $cacheInfo = ['encryptedVersion' => $newEncryptedVersion, 'encrypted' => $newEncryptedVersion];
+ $cache->put($fileCache->getPath(), $cacheInfo);
+ $output->writeln("Increment the encrypted version to $newEncryptedVersion");
+ if ($this->verifyFileContent($path, $output, false) === true) {
+ $output->writeln("Fixed the file: \"$path\" with version " . $newEncryptedVersion . "");
+ return true;
+ }
+ $increment++;
+ }
+ }
+
+ $cacheInfo = ['encryptedVersion' => $originalEncryptedVersion, 'encrypted' => $originalEncryptedVersion];
+ $cache->put($fileCache->getPath(), $cacheInfo);
+ $output->writeln("No fix found for \"$path\", restored version to original: $originalEncryptedVersion");
+
+ return false;
+ }
+
+ /**
+ * Setup user file system
+ */
+ private function setupUserFs(string $uid): void {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($uid);
+ }
+}
diff --git a/lib/Command/Scan.php b/lib/Command/Scan.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a4ea6bed49e430233f9c2b158db021417663b17
--- /dev/null
+++ b/lib/Command/Scan.php
@@ -0,0 +1,359 @@
+
+ * @author Blaok
+ * @author Christoph Wurst
+ * @author Daniel Kesselberg
+ * @author J0WI
+ * @author Joas Schilling
+ * @author Joel S
+ * @author Jörn Friedrich Dreyer
+ * @author martin.mattel@diemattels.at
+ * @author Maxence Lange
+ * @author Robin Appelman
+ * @author Roeland Jago Douma
+ * @author Thomas Müller
+ * @author Vincent Petry
+ *
+ * @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
+ *
+ */
+declare(strict_types=1);
+
+namespace OCA\EcloudAccounts\Command;
+
+use OC\Core\Command\Base;
+use OC\Core\Command\InterruptedException;
+use OC\DB\Connection;
+use OC\DB\ConnectionAdapter;
+use OC\FilesMetadata\FilesMetadataManager;
+use OC\ForbiddenException;
+use OCA\EcloudAccounts\AppInfo\Application;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\FileCacheUpdated;
+use OCP\Files\Events\NodeAddedToCache;
+use OCP\Files\Events\NodeRemovedFromCache;
+use OCP\Files\IRootFolder;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\NotFoundException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+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 Base {
+ protected float $execTime = 0;
+ protected int $foldersCounter = 0;
+ protected int $filesCounter = 0;
+ protected int $errorsCounter = 0;
+ protected int $newCounter = 0;
+ protected int $updatedCounter = 0;
+ protected int $removedCounter = 0;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ private FilesMetadataManager $filesMetadataManager,
+ private IEventDispatcher $eventDispatcher,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName(Application::APP_ID.':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(
+ 'generate-metadata',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Generate metadata for all scanned files; if specified only generate for named value',
+ ''
+ )
+ ->addOption(
+ 'all',
+ null,
+ InputOption::VALUE_NONE,
+ 'will rescan all files of all known users'
+ )->addOption(
+ 'unscanned',
+ null,
+ InputOption::VALUE_NONE,
+ 'only scan files which are marked as not fully scanned'
+ )->addOption(
+ 'shallow',
+ null,
+ InputOption::VALUE_NONE,
+ 'do not scan folders recursively'
+ )->addOption(
+ 'home-only',
+ null,
+ InputOption::VALUE_NONE,
+ 'only scan the home storage, ignoring any mounted external storage or share'
+ );
+ }
+
+ protected function scanFiles(string $user, string $path, ?string $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
+ $connection = $this->reconnectToDatabase($output);
+ $scanner = new \OC\Files\Utils\Scanner(
+ $user,
+ new ConnectionAdapter($connection),
+ \OC::$server->get(IEventDispatcher::class),
+ \OC::$server->get(LoggerInterface::class)
+ );
+
+ # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata) {
+ $output->writeln("\tFile\t$path", OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->filesCounter;
+ $this->abortIfInterrupted();
+ if ($scanMetadata !== null) {
+ $node = $this->rootFolder->get($path);
+ $this->filesMetadataManager->refreshMetadata(
+ $node,
+ ($scanMetadata !== '') ? IFilesMetadataManager::PROCESS_NAMED : IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND,
+ $scanMetadata
+ );
+ }
+ });
+
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
+ $output->writeln("\tFolder\t$path", OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->foldersCounter;
+ $this->abortIfInterrupted();
+ });
+
+ $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
+ $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->errorsCounter;
+ });
+
+ $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
+ $output->writeln("\tEntry \"" . $fullPath . '" will not be accessible due to incompatible encoding');
+ ++$this->errorsCounter;
+ });
+
+ $this->eventDispatcher->addListener(NodeAddedToCache::class, function () {
+ ++$this->newCounter;
+ });
+ $this->eventDispatcher->addListener(FileCacheUpdated::class, function () {
+ ++$this->updatedCounter;
+ });
+ $this->eventDispatcher->addListener(NodeRemovedFromCache::class, function () {
+ ++$this->removedCounter;
+ });
+
+ try {
+ if ($backgroundScan) {
+ $scanner->backgroundScan($path);
+ } else {
+ $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
+ }
+ } catch (ForbiddenException $e) {
+ $output->writeln("Home storage for user $user not writable or 'files' subdirectory missing");
+ $output->writeln(' ' . $e->getMessage());
+ $output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
+ ++$this->errorsCounter;
+ } catch (InterruptedException $e) {
+ # exit the function if ctrl-c has been pressed
+ $output->writeln('Interrupted by user');
+ } catch (NotFoundException $e) {
+ $output->writeln('Path not found: ' . $e->getMessage() . '');
+ ++$this->errorsCounter;
+ } catch (\Exception $e) {
+ $output->writeln('Exception during scan: ' . $e->getMessage() . '');
+ $output->writeln('' . $e->getTraceAsString() . '');
+ ++$this->errorsCounter;
+ }
+ }
+
+ public function filterHomeMount(IMountPoint $mountPoint): bool {
+ // any mountpoint inside '/$user/files/'
+ return substr_count($mountPoint->getMountPoint(), '/') <= 3;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ \OC::$REQUESTEDAPP = Application::APP_ID;
+ $inputPath = $input->getOption('path');
+ if ($inputPath) {
+ $inputPath = '/' . trim($inputPath, '/');
+ [, $user,] = explode('/', $inputPath, 3);
+ $users = [$user];
+ } elseif ($input->getOption('all')) {
+ $users = $this->userManager->search('');
+ } else {
+ $users = $input->getArgument('user_id');
+ }
+
+ # 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 self::FAILURE;
+ }
+
+ $this->initTools($output);
+
+ // getOption() logic on VALUE_OPTIONAL
+ $metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
+ if ($input->getOption('generate-metadata') !== '') {
+ $metadata = $input->getOption('generate-metadata') ?? '';
+ }
+
+ $user_count = 0;
+ foreach ($users as $user) {
+ if (is_object($user)) {
+ $user = $user->getUID();
+ }
+ $path = $inputPath ?: '/' . $user;
+ ++$user_count;
+ if ($this->userManager->userExists($user)) {
+ $output->writeln("Starting scan for user $user_count out of $users_total ($user)");
+ $this->scanFiles($user, $path, $metadata, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
+ $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
+ } else {
+ $output->writeln("Unknown user $user_count $user");
+ $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ try {
+ $this->abortIfInterrupted();
+ } catch (InterruptedException $e) {
+ break;
+ }
+ }
+
+ $this->presentStats($output);
+ return self::SUCCESS;
+ }
+
+ /**
+ * Initialises some useful tools for the Command
+ */
+ protected function initTools(OutputInterface $output): void {
+ // Start the timer
+ $this->execTime = -microtime(true);
+ // Convert PHP errors to exceptions
+ set_error_handler(
+ fn (int $severity, string $message, string $file, int $line): bool =>
+ $this->exceptionErrorHandler($output, $severity, $message, $file, $line),
+ E_ALL
+ );
+ }
+
+ /**
+ * Processes PHP errors in order to be able to show them in the output
+ *
+ * @see https://www.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
+ */
+ public function exceptionErrorHandler(OutputInterface $output, int $severity, string $message, string $file, int $line): bool {
+ if (($severity === E_DEPRECATED) || ($severity === E_USER_DEPRECATED)) {
+ // Do not show deprecation warnings
+ return false;
+ }
+ $e = new \ErrorException($message, 0, $severity, $file, $line);
+ $output->writeln('Error during scan: ' . $e->getMessage() . '');
+ $output->writeln('' . $e->getTraceAsString() . '', OutputInterface::VERBOSITY_VERY_VERBOSE);
+ ++$this->errorsCounter;
+ return true;
+ }
+
+ protected function presentStats(OutputInterface $output): void {
+ // Stop the timer
+ $this->execTime += microtime(true);
+
+ $this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
+
+ $headers = [
+ 'Folders',
+ 'Files',
+ 'New',
+ 'Updated',
+ 'Removed',
+ 'Errors',
+ 'Elapsed time',
+ ];
+ $niceDate = $this->formatExecTime();
+ $rows = [
+ $this->foldersCounter,
+ $this->filesCounter,
+ $this->newCounter,
+ $this->updatedCounter,
+ $this->removedCounter,
+ $this->errorsCounter,
+ $niceDate,
+ ];
+ $table = new Table($output);
+ $table
+ ->setHeaders($headers)
+ ->setRows([$rows]);
+ $table->render();
+ }
+
+
+ /**
+ * Formats microtime into a human-readable format
+ */
+ protected function formatExecTime(): string {
+ $secs = (int)round($this->execTime);
+ # convert seconds into HH:MM:SS form
+ return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
+ }
+
+ protected function reconnectToDatabase(OutputInterface $output): Connection {
+ /** @var Connection $connection */
+ $connection = \OC::$server->get(Connection::class);
+ try {
+ $connection->close();
+ } catch (\Exception $ex) {
+ $output->writeln("Error while disconnecting from database: {$ex->getMessage()}");
+ }
+ while (!$connection->isConnected()) {
+ try {
+ $connection->connect();
+ } catch (\Exception $ex) {
+ $output->writeln("Error while re-connecting to database: {$ex->getMessage()}");
+ sleep(60);
+ }
+ }
+ return $connection;
+ }
+}
diff --git a/lib/Filesystem/CacheWrapper.php b/lib/Filesystem/CacheWrapper.php
index da6e32537a1603509f851e699e253113ae32d3fc..f26cced783a583b1dbda433c14a6d6da43e16cc3 100644
--- a/lib/Filesystem/CacheWrapper.php
+++ b/lib/Filesystem/CacheWrapper.php
@@ -8,7 +8,6 @@ use OC\Files\Cache\Wrapper\CacheWrapper as Wrapper;
use OCP\Constants;
use OCP\Files\Cache\ICache;
use OCP\Files\Cache\ICacheEntry;
-use OCP\Files\ForbiddenException;
use OCP\Files\Search\ISearchQuery;
class CacheWrapper extends Wrapper {
@@ -25,26 +24,30 @@ class CacheWrapper extends Wrapper {
}
protected function formatCacheEntry($entry) {
- if (isset($entry['path']) && isset($entry['permissions'])) {
- try {
- throw new ForbiddenException('Access denied', false);
- } catch (ForbiddenException) {
- $entry['permissions'] &= $this->mask;
- }
- }
- return $entry;
+ throw new \OC\ServiceUnavailableException('Service unavailable');
+ // if (isset($entry['path']) && isset($entry['permissions'])) {
+ // try {
+ // throw new \Exception('Access denied', 503);
+ // } catch (\Exception) {
+ // $entry['permissions'] &= $this->mask;
+ // }
+ // }
+ // return $entry;
}
public function insert($file, $data) {
- throw new \Exception('User data cache insert is disabled.');
+ throw new \OC\ServiceUnavailableException('Service unavailable');
+ // throw new \Exception('User data cache insert is disabled.', 503);
}
public function update($id, $data) {
- throw new \Exception('User data cache update is disabled.');
+ throw new \OC\ServiceUnavailableException('Service unavailable');
+ // throw new \Exception('User data cache update is disabled.', 503);
}
public function remove($fileId) {
- throw new \Exception('User data cache removal is disabled.');
+ throw new \OC\ServiceUnavailableException('Service unavailable');
+ // throw new \Exception('User data cache removal is disabled.', 503);
}
public function searchQuery(ISearchQuery $searchQuery) {
diff --git a/lib/Filesystem/StorageWrapper.php b/lib/Filesystem/StorageWrapper.php
index f35a55105be582163fcfdbfbe44eab87292767be..4854aad1591d33e5e8c0e39ea2ef306a93161d65 100644
--- a/lib/Filesystem/StorageWrapper.php
+++ b/lib/Filesystem/StorageWrapper.php
@@ -7,9 +7,9 @@ namespace OCA\EcloudAccounts\Filesystem;
use OC\Files\Cache\Cache;
use OC\Files\Storage\Storage;
use OC\Files\Storage\Wrapper\Wrapper;
-use OCP\Files\ForbiddenException;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
+use OCP\Files\StorageNotAvailableException;
class StorageWrapper extends Wrapper implements IWriteStreamStorage {
/**
@@ -20,10 +20,10 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
}
/**
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
protected function checkFileAccess(string $path, bool $isDir = false): void {
- throw new ForbiddenException('Access denied', false);
+ throw new StorageNotAvailableException('Service unavailable');
}
/*
@@ -35,7 +35,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
*
* @param string $path
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function mkdir($path) {
$this->checkFileAccess($path, true);
@@ -46,7 +46,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
*
* @param string $path
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function rmdir($path) {
$this->checkFileAccess($path, true);
@@ -59,11 +59,8 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @return bool
*/
public function isCreatable($path) {
- try {
- $this->checkFileAccess($path);
- } catch (ForbiddenException $e) {
- return false;
- }
+ $this->checkFileAccess($path);
+ return false;
}
/**
@@ -73,11 +70,8 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @return bool
*/
public function isReadable($path) {
- try {
- $this->checkFileAccess($path);
- } catch (ForbiddenException $e) {
- return false;
- }
+ $this->checkFileAccess($path);
+ return false;
}
/**
@@ -87,11 +81,8 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @return bool
*/
public function isUpdatable($path) {
- try {
- $this->checkFileAccess($path);
- } catch (ForbiddenException $e) {
- return false;
- }
+ $this->checkFileAccess($path);
+ return false;
}
/**
@@ -101,19 +92,13 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @return bool
*/
public function isDeletable($path) {
- try {
- $this->checkFileAccess($path);
- } catch (ForbiddenException $e) {
- return false;
- }
+ $this->checkFileAccess($path);
+ return false;
}
public function getPermissions($path) {
- try {
- $this->checkFileAccess($path);
- } catch (ForbiddenException $e) {
- return $this->mask;
- }
+ $this->checkFileAccess($path);
+ return $this->mask;
}
/**
@@ -121,7 +106,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
*
* @param string $path
* @return string
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function file_get_contents($path) {
$this->checkFileAccess($path);
@@ -133,7 +118,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $path
* @param string $data
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function file_put_contents($path, $data) {
$this->checkFileAccess($path);
@@ -144,7 +129,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
*
* @param string $path
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function unlink($path) {
$this->checkFileAccess($path);
@@ -156,7 +141,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $path1
* @param string $path2
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function rename($path1, $path2) {
$this->checkFileAccess($path1);
@@ -169,7 +154,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $path1
* @param string $path2
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function copy($path1, $path2) {
$this->checkFileAccess($path1);
@@ -182,7 +167,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $path
* @param string $mode
* @return resource
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function fopen($path, $mode) {
$this->checkFileAccess($path);
@@ -195,7 +180,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $path
* @param int $mtime
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function touch($path, $mtime = null) {
$this->checkFileAccess($path);
@@ -223,7 +208,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
*
* @param string $path
* @return array
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function getDirectDownload($path) {
$this->checkFileAccess($path);
@@ -234,7 +219,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$this->checkFileAccess($targetInternalPath);
@@ -245,14 +230,14 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage {
* @param string $sourceInternalPath
* @param string $targetInternalPath
* @return bool
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
$this->checkFileAccess($targetInternalPath);
}
/**
- * @throws ForbiddenException
+ * @throws StorageNotAvailableException
*/
public function writeStream(string $path, $stream, ?int $size = null): int {
$this->checkFileAccess($path);