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);