diff options
Diffstat (limited to 'apps/files_external/lib/Command')
-rw-r--r-- | apps/files_external/lib/Command/Applicable.php | 65 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Backends.php | 47 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Config.php | 53 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Create.php | 111 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Delete.php | 68 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Dependencies.php | 57 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Export.php | 28 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Import.php | 126 | ||||
-rw-r--r-- | apps/files_external/lib/Command/ListCommand.php | 97 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Notify.php | 283 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Option.php | 36 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Scan.php | 166 | ||||
-rw-r--r-- | apps/files_external/lib/Command/StorageAuthBase.php | 114 | ||||
-rw-r--r-- | apps/files_external/lib/Command/Verify.php | 60 |
14 files changed, 624 insertions, 687 deletions
diff --git a/apps/files_external/lib/Command/Applicable.php b/apps/files_external/lib/Command/Applicable.php index bec312bdcb2..4d5e264bfaf 100644 --- a/apps/files_external/lib/Command/Applicable.php +++ b/apps/files_external/lib/Command/Applicable.php @@ -1,34 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\NotFoundException; use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; use OCP\IGroupManager; use OCP\IUserManager; use Symfony\Component\Console\Input\InputArgument; @@ -37,33 +20,15 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Applicable extends Base { - /** - * @var GlobalStoragesService - */ - protected $globalService; - - /** - * @var IUserManager - */ - private $userManager; - - /** - * @var IGroupManager - */ - private $groupManager; - public function __construct( - GlobalStoragesService $globalService, - IUserManager $userManager, - IGroupManager $groupManager + protected GlobalStoragesService $globalService, + private IUserManager $userManager, + private IGroupManager $groupManager, ) { parent::__construct(); - $this->globalService = $globalService; - $this->userManager = $userManager; - $this->groupManager = $groupManager; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:applicable') ->setDescription('Manage applicable users and groups for a mount') @@ -106,12 +71,12 @@ class Applicable extends Base { $mount = $this->globalService->getStorage($mountId); } catch (NotFoundException $e) { $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } - if ($mount->getType() === StorageConfig::MOUNT_TYPE_PERSONAl) { + if ($mount->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) { $output->writeln('<error>Can\'t change applicables on personal mounts</error>'); - return 1; + return self::FAILURE; } $addUsers = $input->getOption('add-user'); @@ -126,13 +91,13 @@ class Applicable extends Base { foreach ($addUsers as $addUser) { if (!$this->userManager->userExists($addUser)) { $output->writeln('<error>User "' . $addUser . '" not found</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } } foreach ($addGroups as $addGroup) { if (!$this->groupManager->groupExists($addGroup)) { $output->writeln('<error>Group "' . $addGroup . '" not found</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } } @@ -154,6 +119,6 @@ class Applicable extends Base { 'users' => $applicableUsers, 'groups' => $applicableGroups ]); - return 0; + return self::SUCCESS; } } diff --git a/apps/files_external/lib/Command/Backends.php b/apps/files_external/lib/Command/Backends.php index 0046ad72a4c..7fab0477adf 100644 --- a/apps/files_external/lib/Command/Backends.php +++ b/apps/files_external/lib/Command/Backends.php @@ -1,27 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; @@ -34,17 +17,13 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Backends extends Base { - /** @var BackendService */ - private $backendService; - - public function __construct(BackendService $backendService + public function __construct( + private BackendService $backendService, ) { parent::__construct(); - - $this->backendService = $backendService; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:backends') ->setDescription('Show available authentication and storage backends') @@ -74,24 +53,24 @@ class Backends extends Base { if ($type) { if (!isset($data[$type])) { $output->writeln('<error>Invalid type "' . $type . '". Possible values are "authentication" or "storage"</error>'); - return 1; + return self::FAILURE; } $data = $data[$type]; if ($backend) { if (!isset($data[$backend])) { $output->writeln('<error>Unknown backend "' . $backend . '" of type "' . $type . '"</error>'); - return 1; + return self::FAILURE; } $data = $data[$backend]; } } $this->writeArrayInOutputFormat($input, $output, $data); - return 0; + return self::SUCCESS; } - private function serializeAuthBackend(\JsonSerializable $backend) { + private function serializeAuthBackend(\JsonSerializable $backend): array { $data = $backend->jsonSerialize(); $result = [ 'name' => $data['name'], @@ -114,9 +93,9 @@ class Backends extends Base { * @param DefinitionParameter[] $parameters * @return string[] */ - private function formatConfiguration(array $parameters) { + private function formatConfiguration(array $parameters): array { $configuration = array_filter($parameters, function (DefinitionParameter $parameter) { - return $parameter->getType() !== DefinitionParameter::VALUE_HIDDEN; + return $parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN); }); return array_map(function (DefinitionParameter $parameter) { return $parameter->getTypeName(); diff --git a/apps/files_external/lib/Command/Config.php b/apps/files_external/lib/Command/Config.php index 467f421e730..883b4a2f3e7 100644 --- a/apps/files_external/lib/Command/Config.php +++ b/apps/files_external/lib/Command/Config.php @@ -1,50 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Ardinis <Ardinis@users.noreply.github.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\NotFoundException; use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Config extends Base { - /** - * @var GlobalStoragesService - */ - protected $globalService; - - public function __construct(GlobalStoragesService $globalService) { + public function __construct( + protected GlobalStoragesService $globalService, + ) { parent::__construct(); - $this->globalService = $globalService; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:config') ->setDescription('Manage backend configuration for a mount') @@ -71,7 +50,7 @@ class Config extends Base { $mount = $this->globalService->getStorage($mountId); } catch (NotFoundException $e) { $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } $value = $input->getArgument('value'); @@ -80,15 +59,13 @@ class Config extends Base { } else { $this->getOption($mount, $key, $output); } - return 0; + return self::SUCCESS; } /** - * @param StorageConfig $mount * @param string $key - * @param OutputInterface $output */ - protected function getOption(StorageConfig $mount, $key, OutputInterface $output) { + protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void { if ($key === 'mountpoint' || $key === 'mount_point') { $value = $mount->getMountPoint(); } else { @@ -97,16 +74,14 @@ class Config extends Base { if (!is_string($value) && json_decode(json_encode($value)) === $value) { // show bools and objects correctly $value = json_encode($value); } - $output->writeln($value); + $output->writeln((string)$value); } /** - * @param StorageConfig $mount * @param string $key * @param string $value - * @param OutputInterface $output */ - protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output) { + protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void { $decoded = json_decode($value, true); if (!is_null($decoded) && json_encode($decoded) === $value) { $value = $decoded; diff --git a/apps/files_external/lib/Command/Create.php b/apps/files_external/lib/Command/Create.php index 654c7357023..3307015518a 100644 --- a/apps/files_external/lib/Command/Create.php +++ b/apps/files_external/lib/Command/Create.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; @@ -34,7 +16,9 @@ use OCA\Files_External\Lib\DefinitionParameter; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\BackendService; use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\StoragesService; use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; use OCP\IUserManager; use OCP\IUserSession; use Symfony\Component\Console\Input\ArrayInput; @@ -44,42 +28,17 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Create extends Base { - /** - * @var GlobalStoragesService - */ - private $globalService; - - /** - * @var UserStoragesService - */ - private $userService; - - /** - * @var IUserManager - */ - private $userManager; - - /** @var BackendService */ - private $backendService; - - /** @var IUserSession */ - private $userSession; - - public function __construct(GlobalStoragesService $globalService, - UserStoragesService $userService, - IUserManager $userManager, - IUserSession $userSession, - BackendService $backendService + public function __construct( + private GlobalStoragesService $globalService, + private UserStoragesService $userService, + private IUserManager $userManager, + private IUserSession $userSession, + private BackendService $backendService, ) { parent::__construct(); - $this->globalService = $globalService; - $this->userService = $userService; - $this->userManager = $userManager; - $this->userSession = $userSession; - $this->backendService = $backendService; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:create') ->setDescription('Create a new mount configuration') @@ -120,7 +79,7 @@ class Create extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $input->getOption('user'); + $user = (string)$input->getOption('user'); $mountPoint = $input->getArgument('mount_point'); $storageIdentifier = $input->getArgument('storage_backend'); $authIdentifier = $input->getArgument('authentication_backend'); @@ -131,32 +90,32 @@ class Create extends Base { if (!Filesystem::isValidPath($mountPoint)) { $output->writeln('<error>Invalid mountpoint "' . $mountPoint . '"</error>'); - return 1; + return self::FAILURE; } if (is_null($storageBackend)) { $output->writeln('<error>Storage backend with identifier "' . $storageIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } if (is_null($authBackend)) { $output->writeln('<error>Authentication backend with identifier "' . $authIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } $supportedSchemes = array_keys($storageBackend->getAuthSchemes()); if (!in_array($authBackend->getScheme(), $supportedSchemes)) { $output->writeln('<error>Authentication backend "' . $authIdentifier . '" not valid for storage backend "' . $storageIdentifier . '" (see `occ files_external:backends storage ' . $storageIdentifier . '` for possible values)</error>'); - return 1; + return self::FAILURE; } $config = []; foreach ($configInput as $configOption) { - if (!strpos($configOption, '=')) { + if (!str_contains($configOption, '=')) { $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>'); - return 1; + return self::FAILURE; } - list($key, $value) = explode('=', $configOption, 2); + [$key, $value] = explode('=', $configOption, 2); if (!$this->validateParam($key, $value, $storageBackend, $authBackend)) { $output->writeln('<error>Unknown configuration for backends "' . $key . '"</error>'); - return 1; + return self::FAILURE; } $config[$key] = $value; } @@ -170,7 +129,7 @@ class Create extends Base { if ($user) { if (!$this->userManager->userExists($user)) { $output->writeln('<error>User "' . $user . '" not found</error>'); - return 1; + return self::FAILURE; } $mount->setApplicableUsers([$user]); } @@ -185,10 +144,10 @@ class Create extends Base { $output->writeln((string)$mount->getId()); } } - return 0; + return self::SUCCESS; } - private function validateParam($key, &$value, Backend $storageBackend, AuthMechanism $authBackend) { + private function validateParam(string $key, &$value, Backend $storageBackend, AuthMechanism $authBackend): bool { $params = array_merge($storageBackend->getParameters(), $authBackend->getParameters()); foreach ($params as $param) { /** @var DefinitionParameter $param */ @@ -202,7 +161,7 @@ class Create extends Base { return false; } - private function showMount($user, StorageConfig $mount, InputInterface $input, OutputInterface $output) { + private function showMount(string $user, StorageConfig $mount, InputInterface $input, OutputInterface $output): void { $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); $listInput = new ArrayInput([], $listCommand->getDefinition()); $listInput->setOption('output', $input->getOption('output')); @@ -210,16 +169,16 @@ class Create extends Base { $listCommand->listMounts($user, [$mount], $listInput, $output); } - protected function getStorageService($userId) { - if (!empty($userId)) { - $user = $this->userManager->get($userId); - if (is_null($user)) { - throw new NoUserException("user $userId not found"); - } - $this->userSession->setUser($user); - return $this->userService; - } else { + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { return $this->globalService; } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; } } diff --git a/apps/files_external/lib/Command/Delete.php b/apps/files_external/lib/Command/Delete.php index 3a05fed8d08..9f121250f7d 100644 --- a/apps/files_external/lib/Command/Delete.php +++ b/apps/files_external/lib/Command/Delete.php @@ -1,35 +1,20 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; use OCA\Files_External\NotFoundException; use OCA\Files_External\Service\GlobalStoragesService; use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; use OCP\IUserManager; use OCP\IUserSession; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -38,35 +23,17 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class Delete extends Base { - /** - * @var GlobalStoragesService - */ - protected $globalService; - - /** - * @var UserStoragesService - */ - protected $userService; - - /** - * @var IUserSession - */ - protected $userSession; - - /** - * @var IUserManager - */ - protected $userManager; - - public function __construct(GlobalStoragesService $globalService, UserStoragesService $userService, IUserSession $userSession, IUserManager $userManager) { + public function __construct( + protected GlobalStoragesService $globalService, + protected UserStoragesService $userService, + protected IUserSession $userSession, + protected IUserManager $userManager, + protected QuestionHelper $questionHelper, + ) { parent::__construct(); - $this->globalService = $globalService; - $this->userService = $userService; - $this->userSession = $userSession; - $this->userManager = $userManager; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:delete') ->setDescription('Delete an external mount') @@ -89,7 +56,7 @@ class Delete extends Base { $mount = $this->globalService->getStorage($mountId); } catch (NotFoundException $e) { $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } $noConfirm = $input->getOption('yes'); @@ -100,15 +67,16 @@ class Delete extends Base { $listInput->setOption('output', $input->getOption('output')); $listCommand->listMounts(null, [$mount], $listInput, $output); + /** @var QuestionHelper $questionHelper */ $questionHelper = $this->getHelper('question'); $question = new ConfirmationQuestion('Delete this mount? [y/N] ', false); if (!$questionHelper->ask($input, $output, $question)) { - return 1; + return self::FAILURE; } } $this->globalService->removeStorage($mountId); - return 0; + return self::SUCCESS; } } diff --git a/apps/files_external/lib/Command/Dependencies.php b/apps/files_external/lib/Command/Dependencies.php new file mode 100644 index 00000000000..d2db8a8c9af --- /dev/null +++ b/apps/files_external/lib/Command/Dependencies.php @@ -0,0 +1,57 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_External\Command; + +use OC\Core\Command\Base; +use OCA\Files_External\Service\BackendService; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Dependencies extends Base { + public function __construct( + private readonly BackendService $backendService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('files_external:dependencies') + ->setDescription('Show information about the backend dependencies'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $storageBackends = $this->backendService->getBackends(); + + $anyMissing = false; + + foreach ($storageBackends as $backend) { + if ($backend->getDeprecateTo() !== null) { + continue; + } + $missingDependencies = $backend->checkDependencies(); + if ($missingDependencies) { + $anyMissing = true; + $output->writeln($backend->getText() . ':'); + foreach ($missingDependencies as $missingDependency) { + if ($missingDependency->getMessage()) { + $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>: {$missingDependency->getMessage()}"); + } else { + $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>"); + } + } + } + } + + if (!$anyMissing) { + $output->writeln('<info>All dependencies are met</info>'); + } + + return self::SUCCESS; + } +} diff --git a/apps/files_external/lib/Command/Export.php b/apps/files_external/lib/Command/Export.php index 1f5dc3f4afc..59484d0e788 100644 --- a/apps/files_external/lib/Command/Export.php +++ b/apps/files_external/lib/Command/Export.php @@ -1,26 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use Symfony\Component\Console\Input\ArrayInput; @@ -30,7 +14,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Export extends ListCommand { - protected function configure() { + protected function configure(): void { $this ->setName('files_external:export') ->setDescription('Export mount configurations') @@ -55,6 +39,6 @@ class Export extends ListCommand { $listInput->setOption('show-password', true); $listInput->setOption('full', true); $listCommand->execute($listInput, $output); - return 0; + return self::SUCCESS; } } diff --git a/apps/files_external/lib/Command/Import.php b/apps/files_external/lib/Command/Import.php index bbaeea91c3e..a9ed76fbe40 100644 --- a/apps/files_external/lib/Command/Import.php +++ b/apps/files_external/lib/Command/Import.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; @@ -31,6 +13,7 @@ use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\BackendService; use OCA\Files_External\Service\GlobalStoragesService; use OCA\Files_External\Service\ImportLegacyStoragesService; +use OCA\Files_External\Service\StoragesService; use OCA\Files_External\Service\UserStoragesService; use OCP\IUserManager; use OCP\IUserSession; @@ -41,49 +24,18 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Import extends Base { - /** - * @var GlobalStoragesService - */ - private $globalService; - - /** - * @var UserStoragesService - */ - private $userService; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * @var IUserManager - */ - private $userManager; - - /** @var ImportLegacyStoragesService */ - private $importLegacyStorageService; - - /** @var BackendService */ - private $backendService; - - public function __construct(GlobalStoragesService $globalService, - UserStoragesService $userService, - IUserSession $userSession, - IUserManager $userManager, - ImportLegacyStoragesService $importLegacyStorageService, - BackendService $backendService + public function __construct( + private GlobalStoragesService $globalService, + private UserStoragesService $userService, + private IUserSession $userSession, + private IUserManager $userManager, + private ImportLegacyStoragesService $importLegacyStorageService, + private BackendService $backendService, ) { parent::__construct(); - $this->globalService = $globalService; - $this->userService = $userService; - $this->userSession = $userSession; - $this->userManager = $userManager; - $this->importLegacyStorageService = $importLegacyStorageService; - $this->backendService = $backendService; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:import') ->setDescription('Import mount configurations') @@ -108,25 +60,25 @@ class Import extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $input->getOption('user'); + $user = (string)$input->getOption('user'); $path = $input->getArgument('path'); if ($path === '-') { $json = file_get_contents('php://stdin'); } else { if (!file_exists($path)) { $output->writeln('<error>File not found: ' . $path . '</error>'); - return 1; + return self::FAILURE; } $json = file_get_contents($path); } if (!is_string($json) || strlen($json) < 2) { $output->writeln('<error>Error while reading json</error>'); - return 1; + return self::FAILURE; } $data = json_decode($json, true); if (!is_array($data)) { $output->writeln('<error>Error while parsing json</error>'); - return 1; + return self::FAILURE; } $isLegacy = isset($data['user']) || isset($data['group']); @@ -136,7 +88,7 @@ class Import extends Base { foreach ($mounts as $mount) { if ($mount->getBackendOption('password') === false) { $output->writeln('<error>Failed to decrypt password</error>'); - return 1; + return self::FAILURE; } } } else { @@ -161,13 +113,13 @@ class Import extends Base { foreach ($mounts as $mount) { foreach ($existingMounts as $existingMount) { if ( - $existingMount->getMountPoint() === $mount->getMountPoint() && - $existingMount->getApplicableGroups() === $mount->getApplicableGroups() && - $existingMount->getApplicableUsers() === $mount->getApplicableUsers() && - $existingMount->getBackendOptions() === $mount->getBackendOptions() + $existingMount->getMountPoint() === $mount->getMountPoint() + && $existingMount->getApplicableGroups() === $mount->getApplicableGroups() + && $existingMount->getApplicableUsers() === $mount->getApplicableUsers() + && $existingMount->getBackendOptions() === $mount->getBackendOptions() ) { - $output->writeln("<error>Duplicate mount (" . $mount->getMountPoint() . ")</error>"); - return 1; + $output->writeln('<error>Duplicate mount (' . $mount->getMountPoint() . ')</error>'); + return self::FAILURE; } } } @@ -175,7 +127,7 @@ class Import extends Base { if ($input->getOption('dry')) { if (count($mounts) === 0) { $output->writeln('<error>No mounts to be imported</error>'); - return 1; + return self::FAILURE; } $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager); $listInput = new ArrayInput([], $listCommand->getDefinition()); @@ -187,10 +139,10 @@ class Import extends Base { $storageService->addStorage($mount); } } - return 0; + return self::SUCCESS; } - private function parseData(array $data) { + private function parseData(array $data): StorageConfig { $mount = new StorageConfig($data['mount_id']); $mount->setMountPoint($data['mount_point']); $mount->setBackend($this->getBackendByClass($data['storage'])); @@ -198,12 +150,12 @@ class Import extends Base { $mount->setAuthMechanism($authBackend); $mount->setBackendOptions($data['configuration']); $mount->setMountOptions($data['options']); - $mount->setApplicableUsers(isset($data['applicable_users']) ? $data['applicable_users'] : []); - $mount->setApplicableGroups(isset($data['applicable_groups']) ? $data['applicable_groups'] : []); + $mount->setApplicableUsers($data['applicable_users'] ?? []); + $mount->setApplicableGroups($data['applicable_groups'] ?? []); return $mount; } - private function getBackendByClass($className) { + private function getBackendByClass(string $className) { $backends = $this->backendService->getBackends(); foreach ($backends as $backend) { if ($backend->getStorageClass() === $className) { @@ -212,16 +164,16 @@ class Import extends Base { } } - protected function getStorageService($userId) { - if (!empty($userId)) { - $user = $this->userManager->get($userId); - if (is_null($user)) { - throw new NoUserException("user $userId not found"); - } - $this->userSession->setUser($user); - return $this->userService; - } else { + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { return $this->globalService; } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; } } diff --git a/apps/files_external/lib/Command/ListCommand.php b/apps/files_external/lib/Command/ListCommand.php index d415d7718d4..350916b6c2c 100644 --- a/apps/files_external/lib/Command/ListCommand.php +++ b/apps/files_external/lib/Command/ListCommand.php @@ -1,35 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; use OC\User\NoUserException; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\StoragesService; use OCA\Files_External\Service\UserStoragesService; use OCP\IUserManager; use OCP\IUserSession; @@ -40,37 +22,18 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListCommand extends Base { - /** - * @var GlobalStoragesService - */ - protected $globalService; - - /** - * @var UserStoragesService - */ - protected $userService; - - /** - * @var IUserSession - */ - protected $userSession; - - /** - * @var IUserManager - */ - protected $userManager; - public const ALL = -1; - public function __construct(GlobalStoragesService $globalService, UserStoragesService $userService, IUserSession $userSession, IUserManager $userManager) { + public function __construct( + protected GlobalStoragesService $globalService, + protected UserStoragesService $userService, + protected IUserSession $userSession, + protected IUserManager $userManager, + ) { parent::__construct(); - $this->globalService = $globalService; - $this->userService = $userService; - $this->userSession = $userSession; - $this->userManager = $userManager; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:list') ->setDescription('List configured admin or personal mounts') @@ -103,33 +66,31 @@ class ListCommand extends Base { $mounts = $this->globalService->getStorageForAllUsers(); $userId = self::ALL; } else { - $userId = $input->getArgument('user_id'); + $userId = (string)$input->getArgument('user_id'); $storageService = $this->getStorageService($userId); $mounts = $storageService->getAllStorages(); } $this->listMounts($userId, $mounts, $input, $output); - return 0; + return self::SUCCESS; } /** - * @param string $userId + * @param ?string|ListCommand::ALL $userId * @param StorageConfig[] $mounts - * @param InputInterface $input - * @param OutputInterface $output */ - public function listMounts($userId, array $mounts, InputInterface $input, OutputInterface $output) { + public function listMounts($userId, array $mounts, InputInterface $input, OutputInterface $output): void { $outputType = $input->getOption('output'); if (count($mounts) === 0) { if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { $output->writeln('[]'); } else { if ($userId === self::ALL) { - $output->writeln("<info>No mounts configured</info>"); + $output->writeln('<info>No mounts configured</info>'); } elseif ($userId) { $output->writeln("<info>No mounts configured by $userId</info>"); } else { - $output->writeln("<info>No admin mounts configured</info>"); + $output->writeln('<info>No admin mounts configured</info>'); } } return; @@ -146,12 +107,12 @@ class ListCommand extends Base { } if (!$input->getOption('show-password')) { - $hideKeys = ['password', 'refresh_token', 'token', 'client_secret', 'public_key', 'private_key']; + $hideKeys = ['key', 'bucket', 'secret', 'password', 'refresh_token', 'token', 'client_secret', 'public_key', 'private_key']; foreach ($mounts as $mount) { $config = $mount->getBackendOptions(); foreach ($config as $key => $value) { if (in_array($key, $hideKeys)) { - $mount->setBackendOption($key, '***'); + $mount->setBackendOption($key, '***REMOVED SENSITIVE VALUE***'); } } } @@ -263,16 +224,16 @@ class ListCommand extends Base { } } - protected function getStorageService($userId) { - if (!empty($userId)) { - $user = $this->userManager->get($userId); - if (is_null($user)) { - throw new NoUserException("user $userId not found"); - } - $this->userSession->setUser($user); - return $this->userService; - } else { + protected function getStorageService(string $userId): StoragesService { + if (empty($userId)) { return $this->globalService; } + + $user = $this->userManager->get($userId); + if (is_null($user)) { + throw new NoUserException("user $userId not found"); + } + $this->userSession->setUser($user); + return $this->userService; } } diff --git a/apps/files_external/lib/Command/Notify.php b/apps/files_external/lib/Command/Notify.php index 31919ba2d2a..0982aa5598b 100644 --- a/apps/files_external/lib/Command/Notify.php +++ b/apps/files_external/lib/Command/Notify.php @@ -1,36 +1,14 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Ari Selseng <ari@selseng.net> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.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/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Files_External\Command; use Doctrine\DBAL\Exception\DriverException; -use OC\Core\Command\Base; -use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; -use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\GlobalStoragesService; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Notify\IChange; @@ -38,39 +16,25 @@ use OCP\Files\Notify\INotifyHandler; use OCP\Files\Notify\IRenameChange; use OCP\Files\Storage\INotifyStorage; use OCP\Files\Storage\IStorage; -use OCP\Files\StorageNotAvailableException; use OCP\IDBConnection; -use OCP\ILogger; use OCP\IUserManager; +use Psr\Log\LoggerInterface; 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 Notify extends Base { - /** @var GlobalStoragesService */ - private $globalService; - /** @var IDBConnection */ - private $connection; - /** @var ILogger */ - private $logger; - /** @var IUserManager */ - private $userManager; - +class Notify extends StorageAuthBase { public function __construct( + private IDBConnection $connection, + private LoggerInterface $logger, GlobalStoragesService $globalService, - IDBConnection $connection, - ILogger $logger, - IUserManager $userManager + IUserManager $userManager, ) { - parent::__construct(); - $this->globalService = $globalService; - $this->connection = $connection; - $this->logger = $logger; - $this->userManager = $userManager; + parent::__construct($globalService, $userManager); } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:notify') ->setDescription('Listen for active update notifications for a configured external mount') @@ -99,156 +63,100 @@ class Notify extends Base { '', InputOption::VALUE_NONE, 'Disable self check on startup' + )->addOption( + 'dry-run', + '', + InputOption::VALUE_NONE, + 'Don\'t make any changes, only log detected changes' ); parent::configure(); } - private function getUserOption(InputInterface $input): ?string { - if ($input->getOption('user')) { - return (string)$input->getOption('user'); - } elseif (isset($_ENV['NOTIFY_USER'])) { - return (string)$_ENV['NOTIFY_USER']; - } elseif (isset($_SERVER['NOTIFY_USER'])) { - return (string)$_SERVER['NOTIFY_USER']; - } else { - return null; - } - } - - private function getPasswordOption(InputInterface $input): ?string { - if ($input->getOption('password')) { - return (string)$input->getOption('password'); - } elseif (isset($_ENV['NOTIFY_PASSWORD'])) { - return (string)$_ENV['NOTIFY_PASSWORD']; - } elseif (isset($_SERVER['NOTIFY_PASSWORD'])) { - return (string)$_SERVER['NOTIFY_PASSWORD']; - } else { - return null; - } - } - protected function execute(InputInterface $input, OutputInterface $output): int { - $mount = $this->globalService->getStorage($input->getArgument('mount_id')); - if (is_null($mount)) { - $output->writeln('<error>Mount not found</error>'); - return 1; - } - $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; + [$mount, $storage] = $this->createStorage($input, $output); + if ($storage === null) { + return self::FAILURE; } - try { - $storage = $this->createStorage($mount); - } 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 1; - } if (!$storage instanceof INotifyStorage) { $output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>'); - return 1; + return self::FAILURE; } - $verbose = $input->getOption('verbose'); + $dryRun = $input->getOption('dry-run'); + if ($dryRun && $output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + } $path = trim($input->getOption('path'), '/'); $notifyHandler = $storage->notify($path); if (!$input->getOption('no-self-check')) { - $this->selfTest($storage, $notifyHandler, $verbose, $output); + $this->selfTest($storage, $notifyHandler, $output); } - $notifyHandler->listen(function (IChange $change) use ($mount, $verbose, $output) { - if ($verbose) { - $this->logUpdate($change, $output); - } + $notifyHandler->listen(function (IChange $change) use ($mount, $output, $dryRun): void { + $this->logUpdate($change, $output); if ($change instanceof IRenameChange) { - $this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output); + $this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output, $dryRun); } - $this->markParentAsOutdated($mount->getId(), $change->getPath(), $output); + $this->markParentAsOutdated($mount->getId(), $change->getPath(), $output, $dryRun); }); - return 0; + return self::SUCCESS; } - private function createStorage(StorageConfig $mount) { - $class = $mount->getBackend()->getStorageClass(); - return new $class($mount->getBackendOptions()); - } - - private function markParentAsOutdated($mountId, $path, OutputInterface $output) { + private function markParentAsOutdated($mountId, $path, OutputInterface $output, bool $dryRun): void { $parent = ltrim(dirname($path), '/'); if ($parent === '.') { $parent = ''; } try { - $storageIds = $this->getStorageIds($mountId); + $storages = $this->getStorageIds($mountId, $parent); } catch (DriverException $ex) { - $this->logger->logException($ex, ['message' => 'Error while trying to find correct storage ids.', 'level' => ILogger::WARN]); + $this->logger->warning('Error while trying to find correct storage ids.', ['exception' => $ex]); $this->connection = $this->reconnectToDatabase($this->connection, $output); $output->writeln('<info>Needed to reconnect to the database</info>'); - $storageIds = $this->getStorageIds($mountId); + $storages = $this->getStorageIds($mountId, $path); } - if (count($storageIds) === 0) { - throw new StorageNotAvailableException('No storages found by mount ID ' . $mountId); + if (count($storages) === 0) { + $output->writeln(" no users found with access to '$parent', skipping", OutputInterface::VERBOSITY_VERBOSE); + return; } - $storageIds = array_map('intval', $storageIds); - $result = $this->updateParent($storageIds, $parent); - if ($result === 0) { - //TODO: Find existing parent further up the tree in the database and register that folder instead. - $this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.'); + $users = array_map(function (array $storage) { + return $storage['user_id']; + }, $storages); + + $output->writeln(" marking '$parent' as outdated for " . implode(', ', $users), OutputInterface::VERBOSITY_VERBOSE); + + $storageIds = array_map(function (array $storage) { + return intval($storage['storage_id']); + }, $storages); + $storageIds = array_values(array_unique($storageIds)); + + if ($dryRun) { + $output->writeln(' dry-run: skipping database write'); + } else { + $result = $this->updateParent($storageIds, $parent); + if ($result === 0) { + //TODO: Find existing parent further up the tree in the database and register that folder instead. + $this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.'); + } } } - private function logUpdate(IChange $change, OutputInterface $output) { - switch ($change->getType()) { - case INotifyStorage::NOTIFY_ADDED: - $text = 'added'; - break; - case INotifyStorage::NOTIFY_MODIFIED: - $text = 'modified'; - break; - case INotifyStorage::NOTIFY_REMOVED: - $text = 'removed'; - break; - case INotifyStorage::NOTIFY_RENAMED: - $text = 'renamed'; - break; - default: - return; + private function logUpdate(IChange $change, OutputInterface $output): void { + $text = match ($change->getType()) { + INotifyStorage::NOTIFY_ADDED => 'added', + INotifyStorage::NOTIFY_MODIFIED => 'modified', + INotifyStorage::NOTIFY_REMOVED => 'removed', + INotifyStorage::NOTIFY_RENAMED => 'renamed', + default => '', + }; + + if ($text === '') { + return; } $text .= ' ' . $change->getPath(); @@ -256,29 +164,23 @@ class Notify extends Base { $text .= ' to ' . $change->getTargetPath(); } - $output->writeln($text); + $output->writeln($text, OutputInterface::VERBOSITY_VERBOSE); } - /** - * @param int $mountId - * @return array - */ - private function getStorageIds($mountId) { + private function getStorageIds(int $mountId, string $path): array { + $pathHash = md5(trim(\OC_Util::normalizeUnicode($path), '/')); $qb = $this->connection->getQueryBuilder(); return $qb - ->select('storage_id') - ->from('mounts') + ->select('storage_id', 'user_id') + ->from('mounts', 'm') + ->innerJoin('m', 'filecache', 'f', $qb->expr()->eq('m.storage_id', 'f.storage')) ->where($qb->expr()->eq('mount_id', $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR))) ->execute() - ->fetchAll(\PDO::FETCH_COLUMN); + ->fetchAll(); } - /** - * @param array $storageIds - * @param string $parent - * @return int - */ - private function updateParent($storageIds, $parent) { + private function updateParent(array $storageIds, string $parent): int { $pathHash = md5(trim(\OC_Util::normalizeUnicode($parent), '/')); $qb = $this->connection->getQueryBuilder(); return $qb @@ -286,24 +188,22 @@ class Notify extends Base { ->set('size', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)) ->where($qb->expr()->in('storage', $qb->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY, ':storage_ids'))) ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR))) - ->execute(); + ->executeStatement(); } - /** - * @return \OCP\IDBConnection - */ - private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output) { + private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output): IDBConnection { try { $connection->close(); } catch (\Exception $ex) { - $this->logger->logException($ex, ['app' => 'files_external', 'message' => 'Error while disconnecting from DB', 'level' => ILogger::WARN]); + $this->logger->warning('Error while disconnecting from DB', ['exception' => $ex]); $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); } - while (!$connection->isConnected()) { + $connected = false; + while (!$connected) { try { - $connection->connect(); + $connected = $connection->connect(); } catch (\Exception $ex) { - $this->logger->logException($ex, ['app' => 'files_external', 'message' => 'Error while re-connecting to database', 'level' => ILogger::WARN]); + $this->logger->warning('Error while re-connecting to database', ['exception' => $ex]); $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); sleep(60); } @@ -312,9 +212,12 @@ class Notify extends Base { } - private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, $verbose, OutputInterface $output) { + private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, OutputInterface $output): void { usleep(100 * 1000); //give time for the notify to start - $storage->file_put_contents('/.nc_test_file.txt', 'test content'); + if (!$storage->file_put_contents('/.nc_test_file.txt', 'test content')) { + $output->writeln('Failed to create test file for self-test'); + return; + } $storage->mkdir('/.nc_test_folder'); $storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content'); @@ -339,11 +242,11 @@ class Notify extends Base { } } - if ($foundRootChange && $foundSubfolderChange && $verbose) { - $output->writeln('<info>Self-test successful</info>'); - } elseif ($foundRootChange && !$foundSubfolderChange) { + if ($foundRootChange && $foundSubfolderChange) { + $output->writeln('<info>Self-test successful</info>', OutputInterface::VERBOSITY_VERBOSE); + } elseif ($foundRootChange) { $output->writeln('<error>Error while running self-test, change is subfolder not detected</error>'); - } elseif (!$foundRootChange) { + } else { $output->writeln('<error>Error while running self-test, no changes detected</error>'); } } diff --git a/apps/files_external/lib/Command/Option.php b/apps/files_external/lib/Command/Option.php index bc4600b2b6e..3fda3fcb3cf 100644 --- a/apps/files_external/lib/Command/Option.php +++ b/apps/files_external/lib/Command/Option.php @@ -1,26 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OCA\Files_External\Lib\StorageConfig; @@ -28,7 +12,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; class Option extends Config { - protected function configure() { + protected function configure(): void { $this ->setName('files_external:option') ->setDescription('Manage mount options for a mount') @@ -48,25 +32,21 @@ class Option extends Config { } /** - * @param StorageConfig $mount * @param string $key - * @param OutputInterface $output */ - protected function getOption(StorageConfig $mount, $key, OutputInterface $output) { + protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void { $value = $mount->getMountOption($key); if (!is_string($value)) { // show bools and objects correctly $value = json_encode($value); } - $output->writeln($value); + $output->writeln((string)$value); } /** - * @param StorageConfig $mount * @param string $key * @param string $value - * @param OutputInterface $output */ - protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output) { + protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void { $decoded = json_decode($value, true); if (!is_null($decoded)) { $value = $decoded; diff --git a/apps/files_external/lib/Command/Scan.php b/apps/files_external/lib/Command/Scan.php new file mode 100644 index 00000000000..75d98878baa --- /dev/null +++ b/apps/files_external/lib/Command/Scan.php @@ -0,0 +1,166 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Command; + +use OC\Files\Cache\Scanner; +use OCA\Files_External\Service\GlobalStoragesService; +use OCP\IUserManager; +use OCP\Lock\LockedException; +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', + '' + )->addOption( + 'unscanned', + '', + InputOption::VALUE_NONE, + 'only scan files which are marked as not fully scanned' + ); + 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): void { + $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): void { + $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE); + ++$this->foldersCounter; + $this->abortIfInterrupted(); + }); + + try { + if ($input->getOption('unscanned')) { + if ($path !== '') { + $output->writeln('<error>--unscanned is mutually exclusive with --path</error>'); + return 1; + } + $scanner->backgroundScan(); + } else { + $scanner->scan($path); + } + } catch (LockedException $e) { + if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) { + if ($e->getReadablePath() === 'scanner::') { + $output->writeln('<error>Another process is already scanning this storage</error>'); + } else { + $output->writeln('<error>Another process is already scanning \'' . substr($e->getReadablePath(), strlen('scanner::')) . '\' in this storage</error>'); + } + } else { + throw $e; + } + } + + $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); + } +} diff --git a/apps/files_external/lib/Command/StorageAuthBase.php b/apps/files_external/lib/Command/StorageAuthBase.php new file mode 100644 index 00000000000..6f830a08a60 --- /dev/null +++ b/apps/files_external/lib/Command/StorageAuthBase.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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((int)$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]; + } + } +} diff --git a/apps/files_external/lib/Command/Verify.php b/apps/files_external/lib/Command/Verify.php index adedbbd9a9e..ecebbe0f7e6 100644 --- a/apps/files_external/lib/Command/Verify.php +++ b/apps/files_external/lib/Command/Verify.php @@ -1,37 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_External\Command; use OC\Core\Command\Base; -use OCA\Files_External\Lib\Auth\AuthMechanism; -use OCA\Files_External\Lib\Backend\Backend; use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException; use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\MountConfig; use OCA\Files_External\NotFoundException; use OCA\Files_External\Service\GlobalStoragesService; +use OCP\AppFramework\Http; use OCP\Files\StorageNotAvailableException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -39,17 +21,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Verify extends Base { - /** - * @var GlobalStoragesService - */ - protected $globalService; - - public function __construct(GlobalStoragesService $globalService) { + public function __construct( + protected GlobalStoragesService $globalService, + ) { parent::__construct(); - $this->globalService = $globalService; } - protected function configure() { + protected function configure(): void { $this ->setName('files_external:verify') ->setDescription('Verify mount configuration') @@ -74,7 +52,7 @@ class Verify extends Base { $mount = $this->globalService->getStorage($mountId); } catch (NotFoundException $e) { $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>'); - return 404; + return Http::STATUS_NOT_FOUND; } $this->updateStorageStatus($mount, $configInput, $output); @@ -84,19 +62,17 @@ class Verify extends Base { 'code' => $mount->getStatus(), 'message' => $mount->getStatusMessage() ]); - return 0; + return self::SUCCESS; } - private function manipulateStorageConfig(StorageConfig $storage) { - /** @var AuthMechanism */ + private function manipulateStorageConfig(StorageConfig $storage): void { $authMechanism = $storage->getAuthMechanism(); $authMechanism->manipulateStorageConfig($storage); - /** @var Backend */ $backend = $storage->getBackend(); $backend->manipulateStorageConfig($storage); } - private function updateStorageStatus(StorageConfig &$storage, $configInput, OutputInterface $output) { + private function updateStorageStatus(StorageConfig &$storage, $configInput, OutputInterface $output): void { try { try { $this->manipulateStorageConfig($storage); @@ -111,22 +87,20 @@ class Verify extends Base { $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>'); return; } - list($key, $value) = explode('=', $configOption, 2); + [$key, $value] = explode('=', $configOption, 2); $storage->setBackendOption($key, $value); } - /** @var Backend */ $backend = $storage->getBackend(); // update status (can be time-consuming) $storage->setStatus( - \OCA\Files_External\MountConfig::getBackendStatus( + MountConfig::getBackendStatus( $backend->getStorageClass(), $storage->getBackendOptions(), - false ) ); } catch (InsufficientDataForMeaningfulAnswerException $e) { - $status = $e->getCode() ? $e->getCode() : StorageNotAvailableException::STATUS_INDETERMINATE; + $status = $e->getCode() ?: StorageNotAvailableException::STATUS_INDETERMINATE; $storage->setStatus( $status, $e->getMessage() |