diff options
Diffstat (limited to 'apps/files/lib/Command/TransferOwnership.php')
-rw-r--r-- | apps/files/lib/Command/TransferOwnership.php | 327 |
1 files changed, 91 insertions, 236 deletions
diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php index d175f66d171..f7663e26f28 100644 --- a/apps/files/lib/Command/TransferOwnership.php +++ b/apps/files/lib/Command/TransferOwnership.php @@ -1,92 +1,44 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Carla Schroder <carla@owncloud.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sujith H <sharidasan@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @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\Command; -use OC\Files\Filesystem; -use OC\Files\View; -use OCP\Files\FileInfo; -use OCP\Files\IHomeStorage; +use OCA\Files\Exception\TransferOwnershipException; +use OCA\Files\Service\OwnershipTransferService; +use OCA\Files_External\Config\ConfigAdapter; use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; +use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; -use OCP\Share\IManager; -use OCP\Share\IShare; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; class TransferOwnership extends Command { - - /** @var IUserManager $userManager */ - private $userManager; - - /** @var IManager */ - private $shareManager; - - /** @var IMountManager */ - private $mountManager; - - /** @var FileInfo[] */ - private $allFiles = []; - - /** @var FileInfo[] */ - private $encryptedFiles = []; - - /** @var IShare[] */ - private $shares = []; - - /** @var string */ - private $sourceUser; - - /** @var string */ - private $destinationUser; - - /** @var string */ - private $sourcePath; - - /** @var string */ - private $finalTarget; - - public function __construct(IUserManager $userManager, IManager $shareManager, IMountManager $mountManager) { - $this->userManager = $userManager; - $this->shareManager = $shareManager; - $this->mountManager = $mountManager; + public function __construct( + private IUserManager $userManager, + private OwnershipTransferService $transferService, + private IConfig $config, + private IMountManager $mountManager, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('files:transfer-ownership') - ->setDescription('All files and folders are moved to another user - shares are moved as well.') + ->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.') ->addArgument( 'source-user', InputArgument::REQUIRED, @@ -103,191 +55,94 @@ class TransferOwnership extends Command { InputOption::VALUE_REQUIRED, 'selectively provide the path to transfer. For example --path="folder_name"', '' + )->addOption( + 'move', + null, + InputOption::VALUE_NONE, + 'move data from source user to root directory of destination user, which must be empty' + )->addOption( + 'transfer-incoming-shares', + null, + InputOption::VALUE_OPTIONAL, + 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore', + '2' + )->addOption( + 'include-external-storage', + null, + InputOption::VALUE_NONE, + 'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory', + )->addOption( + 'force-include-external-storage', + null, + InputOption::VALUE_NONE, + 'don\'t ask for confirmation for transferring external storages', ); } - protected function execute(InputInterface $input, OutputInterface $output) { - $sourceUserObject = $this->userManager->get($input->getArgument('source-user')); - $destinationUserObject = $this->userManager->get($input->getArgument('destination-user')); - - if (!$sourceUserObject instanceof IUser) { - $output->writeln("<error>Unknown source user $this->sourceUser</error>"); - return 1; - } - - if (!$destinationUserObject instanceof IUser) { - $output->writeln("<error>Unknown destination user $this->destinationUser</error>"); - return 1; - } + protected function execute(InputInterface $input, OutputInterface $output): int { - $this->sourceUser = $sourceUserObject->getUID(); - $this->destinationUser = $destinationUserObject->getUID(); - $sourcePathOption = ltrim($input->getOption('path'), '/'); - $this->sourcePath = rtrim($this->sourceUser . '/files/' . $sourcePathOption, '/'); + /** + * Check if source and destination users are same. If they are same then just ignore the transfer. + */ - // target user has to be ready - if (!\OC::$server->getEncryptionManager()->isReadyForUser($this->destinationUser)) { - $output->writeln("<error>The target user is not ready to accept files. The user has at least to be logged in once.</error>"); - return 2; + if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) { + $output->writeln("<error>Ownership can't be transferred when Source and Destination users are the same user. Please check your input.</error>"); + return self::FAILURE; } - $date = date('Y-m-d H-i-s'); - $this->finalTarget = "$this->destinationUser/files/transferred from $this->sourceUser on $date"; - - // setup filesystem - Filesystem::initMountPoints($this->sourceUser); - Filesystem::initMountPoints($this->destinationUser); + $sourceUserObject = $this->userManager->get($input->getArgument('source-user')); + $destinationUserObject = $this->userManager->get($input->getArgument('destination-user')); - $view = new View(); - if (!$view->is_dir($this->sourcePath)) { - $output->writeln("<error>Unknown path provided: $sourcePathOption</error>"); - return 1; + if (!$sourceUserObject instanceof IUser) { + $output->writeln('<error>Unknown source user ' . $input->getArgument('source-user') . '</error>'); + return self::FAILURE; } - // analyse source folder - $this->analyse($output); - - // collect all the shares - $this->collectUsersShares($output); - - // transfer the files - $this->transfer($output); - - // restore the shares - $this->restoreShares($output); - } - - private function walkFiles(View $view, $path, \Closure $callBack) { - foreach ($view->getDirectoryContent($path) as $fileInfo) { - if (!$callBack($fileInfo)) { - return; - } - if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { - $this->walkFiles($view, $fileInfo->getPath(), $callBack); - } + if (!$destinationUserObject instanceof IUser) { + $output->writeln('<error>Unknown destination user ' . $input->getArgument('destination-user') . '</error>'); + return self::FAILURE; } - } - - /** - * @param OutputInterface $output - * @throws \Exception - */ - protected function analyse(OutputInterface $output) { - $view = new View(); - $output->writeln("Analysing files of $this->sourceUser ..."); - $progress = new ProgressBar($output); - $progress->start(); - $self = $this; - $this->walkFiles($view, $this->sourcePath, - function (FileInfo $fileInfo) use ($progress, $self) { - if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { - // only analyze into folders from main storage, - if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) { - return false; - } - return true; - } - $progress->advance(); - $this->allFiles[] = $fileInfo; - if ($fileInfo->isEncrypted()) { - $this->encryptedFiles[] = $fileInfo; + $path = ltrim($input->getOption('path'), '/'); + $includeExternalStorage = $input->getOption('include-external-storage'); + if ($includeExternalStorage) { + $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/')); + /** @var IMountPoint[] $mounts */ + $mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class); + if (count($mounts) > 0) { + $output->writeln(count($mounts) . ' external storages will be transferred:'); + foreach ($mounts as $mount) { + $output->writeln(' - <info>' . $mount->getMountPoint() . '</info>'); + } + $output->writeln(''); + $output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>'); + $output->writeln(''); + if (!$input->getOption('force-include-external-storage')) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false); + if (!$helper->ask($input, $output, $question)) { + return self::FAILURE; } - return true; - }); - $progress->finish(); - $output->writeln(''); - - // no file is allowed to be encrypted - if (!empty($this->encryptedFiles)) { - $output->writeln("<error>Some files are encrypted - please decrypt them first</error>"); - foreach($this->encryptedFiles as $encryptedFile) { - /** @var FileInfo $encryptedFile */ - $output->writeln(" " . $encryptedFile->getPath()); - } - throw new \Exception('Execution terminated.'); - } - - } - - /** - * @param OutputInterface $output - */ - private function collectUsersShares(OutputInterface $output) { - $output->writeln("Collecting all share information for files and folder of $this->sourceUser ..."); - - $progress = new ProgressBar($output, count($this->shares)); - foreach([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE] as $shareType) { - $offset = 0; - while (true) { - $sharePage = $this->shareManager->getSharesBy($this->sourceUser, $shareType, null, true, 50, $offset); - $progress->advance(count($sharePage)); - if (empty($sharePage)) { - break; } - $this->shares = array_merge($this->shares, $sharePage); - $offset += 50; } } - $progress->finish(); - $output->writeln(''); - } - - /** - * @param OutputInterface $output - */ - protected function transfer(OutputInterface $output) { - $view = new View(); - $output->writeln("Transferring files to $this->finalTarget ..."); - - // This change will help user to transfer the folder specified using --path option. - // Else only the content inside folder is transferred which is not correct. - if($this->sourcePath !== "$this->sourceUser/files") { - $view->mkdir($this->finalTarget); - $this->finalTarget = $this->finalTarget . '/' . basename($this->sourcePath); - } - $view->rename($this->sourcePath, $this->finalTarget); - if (!is_dir("$this->sourceUser/files")) { - // because the files folder is moved away we need to recreate it - $view->mkdir("$this->sourceUser/files"); + try { + $this->transferService->transfer( + $sourceUserObject, + $destinationUserObject, + $path, + $output, + $input->getOption('move') === true, + false, + $includeExternalStorage, + ); + } catch (TransferOwnershipException $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE; } - } - - /** - * @param OutputInterface $output - */ - private function restoreShares(OutputInterface $output) { - $output->writeln("Restoring shares ..."); - $progress = new ProgressBar($output, count($this->shares)); - foreach($this->shares as $share) { - try { - if ($share->getSharedWith() === $this->destinationUser) { - // Unmount the shares before deleting, so we don't try to get the storage later on. - $shareMountPoint = $this->mountManager->find('/' . $this->destinationUser . '/files' . $share->getTarget()); - if ($shareMountPoint) { - $this->mountManager->removeMount($shareMountPoint->getMountPoint()); - } - $this->shareManager->deleteShare($share); - } else { - if ($share->getShareOwner() === $this->sourceUser) { - $share->setShareOwner($this->destinationUser); - } - if ($share->getSharedBy() === $this->sourceUser) { - $share->setSharedBy($this->destinationUser); - } - - $this->shareManager->updateShare($share); - } - } catch (\OCP\Files\NotFoundException $e) { - $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>'); - } catch (\Exception $e) { - $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>'); - } - $progress->advance(); - } - $progress->finish(); - $output->writeln(''); + return self::SUCCESS; } } |