aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/lib/Service/OwnershipTransferService.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/lib/Service/OwnershipTransferService.php')
-rw-r--r--apps/files/lib/Service/OwnershipTransferService.php346
1 files changed, 209 insertions, 137 deletions
diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php
index 1a28785fa1f..afef5d2093d 100644
--- a/apps/files/lib/Service/OwnershipTransferService.php
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -3,50 +3,33 @@
declare(strict_types=1);
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sascha Wiswedel <sascha.wiswedel@nextcloud.com>
- * @author Tobia De Koninck <LEDfan@users.noreply.github.com>
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @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: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Service;
use Closure;
-use OC\Encryption\Manager as EncryptionManager;
+use Exception;
use OC\Files\Filesystem;
use OC\Files\View;
+use OC\User\NoUserException;
+use OCA\Encryption\Util;
use OCA\Files\Exception\TransferOwnershipException;
+use OCA\Files_External\Config\ConfigAdapter;
use OCP\Encryption\IManager as IEncryptionManager;
+use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IUserMountCache;
+use OCP\Files\File;
use OCP\Files\FileInfo;
-use OCP\Files\IHomeStorage;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
+use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use OCP\Server;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use Symfony\Component\Console\Helper\ProgressBar;
@@ -61,16 +44,15 @@ use function rtrim;
class OwnershipTransferService {
- private IEncryptionManager|EncryptionManager $encryptionManager;
-
public function __construct(
- IEncryptionManager $encryptionManager,
+ private IEncryptionManager $encryptionManager,
private IShareManager $shareManager,
private IMountManager $mountManager,
private IUserMountCache $userMountCache,
private IUserManager $userManager,
+ private IFactory $l10nFactory,
+ private IRootFolder $rootFolder,
) {
- $this->encryptionManager = $encryptionManager;
}
/**
@@ -81,7 +63,7 @@ class OwnershipTransferService {
* @param OutputInterface|null $output
* @param bool $move
* @throws TransferOwnershipException
- * @throws \OC\User\NoUserException
+ * @throws NoUserException
*/
public function transfer(
IUser $sourceUser,
@@ -90,7 +72,7 @@ class OwnershipTransferService {
?OutputInterface $output = null,
bool $move = false,
bool $firstLogin = false,
- bool $transferIncomingShares = false,
+ bool $includeExternalStorage = false,
): void {
$output = $output ?? new NullOutput();
$sourceUid = $sourceUser->getUID();
@@ -100,15 +82,17 @@ class OwnershipTransferService {
// If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
|| !$this->encryptionManager->isReadyForUser($destinationUid)) {
- throw new TransferOwnershipException("The target user is not ready to accept files. The user has at least to have logged in once.", 2);
+ throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
}
// setup filesystem
// Requesting the user folder will set it up if the user hasn't logged in before
// We need a setupFS for the full filesystem setup before as otherwise we will just return
// a lazy root folder which does not create the destination users folder
+ \OC_Util::setupFS($sourceUser->getUID());
\OC_Util::setupFS($destinationUser->getUID());
- \OC::$server->getUserFolder($destinationUser->getUID());
+ $this->rootFolder->getUserFolder($sourceUser->getUID());
+ $this->rootFolder->getUserFolder($destinationUser->getUID());
Filesystem::initMountPoints($sourceUid);
Filesystem::initMountPoints($destinationUid);
@@ -117,19 +101,15 @@ class OwnershipTransferService {
if ($move) {
$finalTarget = "$destinationUid/files/";
} else {
+ $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
$date = date('Y-m-d H-i-s');
- // Remove some characters which are prone to cause errors
- $cleanUserName = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $sourceUser->getDisplayName());
- // Replace multiple dashes with one dash
- $cleanUserName = preg_replace('/-{2,}/s', '-', $cleanUserName);
- $cleanUserName = $cleanUserName ?: $sourceUid;
-
- $finalTarget = "$destinationUid/files/transferred from $cleanUserName on $date";
+ $cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName()) ?: $sourceUid;
+ $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
try {
$view->verifyPath(dirname($finalTarget), basename($finalTarget));
} catch (InvalidPathException $e) {
- $finalTarget = "$destinationUid/files/transferred from $sourceUid on $date";
+ $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
}
}
@@ -143,7 +123,7 @@ class OwnershipTransferService {
}
if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
- throw new TransferOwnershipException("Destination path does not exists or is not empty", 1);
+ throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
}
@@ -164,13 +144,39 @@ class OwnershipTransferService {
$sourcePath
);
+ $sourceSize = $view->getFileInfo($sourcePath)->getSize();
+
// transfer the files
$this->transferFiles(
$sourceUid,
$sourcePath,
$finalTarget,
$view,
- $output
+ $output,
+ $includeExternalStorage,
+ );
+ $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
+
+ // transfer the incoming shares
+ $sourceShares = $this->collectIncomingShares(
+ $sourceUid,
+ $output,
+ $sourcePath,
+ );
+ $destinationShares = $this->collectIncomingShares(
+ $destinationUid,
+ $output,
+ null,
+ );
+ $this->transferIncomingShares(
+ $sourceUid,
+ $destinationUid,
+ $sourceShares,
+ $destinationShares,
+ $output,
+ $path,
+ $finalTarget,
+ $move
);
$destinationPath = $finalTarget . '/' . $path;
@@ -182,33 +188,18 @@ class OwnershipTransferService {
$shares,
$output
);
-
- // transfer the incoming shares
- if ($transferIncomingShares === true) {
- $sourceShares = $this->collectIncomingShares(
- $sourceUid,
- $output,
- $view
- );
- $destinationShares = $this->collectIncomingShares(
- $destinationUid,
- $output,
- $view,
- true
- );
- $this->transferIncomingShares(
- $sourceUid,
- $destinationUid,
- $sourceShares,
- $destinationShares,
- $output,
- $path,
- $finalTarget,
- $move
- );
+ if ($sizeDifference !== 0) {
+ $output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
}
}
+ private function sanitizeFolderName(string $name): string {
+ // Remove some characters which are prone to cause errors
+ $name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
+ // Replace multiple dashes with one dash
+ return preg_replace('/-{2,}/s', '-', $name);
+ }
+
private function walkFiles(View $view, $path, Closure $callBack) {
foreach ($view->getDirectoryContent($path) as $fileInfo) {
if (!$callBack($fileInfo)) {
@@ -223,52 +214,83 @@ class OwnershipTransferService {
/**
* @param OutputInterface $output
*
- * @throws \Exception
+ * @throws TransferOwnershipException
*/
- protected function analyse(string $sourceUid,
+ protected function analyse(
+ string $sourceUid,
string $destinationUid,
string $sourcePath,
View $view,
- OutputInterface $output): void {
+ OutputInterface $output,
+ bool $includeExternalStorage = false,
+ ): void {
$output->writeln('Validating quota');
- $size = $view->getFileInfo($sourcePath, false)->getSize(false);
+ $sourceFileInfo = $view->getFileInfo($sourcePath, false);
+ if ($sourceFileInfo === false) {
+ throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
+ }
+ $size = $sourceFileInfo->getSize(false);
$freeSpace = $view->free_space($destinationUid . '/files/');
if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
- $output->writeln('<error>Target user does not have enough free space available.</error>');
- throw new \Exception('Execution terminated.');
+ throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
}
$output->writeln("Analysing files of $sourceUid ...");
$progress = new ProgressBar($output);
$progress->start();
+ if ($this->encryptionManager->isEnabled()) {
+ $masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
+ } else {
+ $masterKeyEnabled = false;
+ }
$encryptedFiles = [];
- $this->walkFiles($view, $sourcePath,
- function (FileInfo $fileInfo) use ($progress) {
- 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();
- if ($fileInfo->isEncrypted()) {
- $encryptedFiles[] = $fileInfo;
- }
- return true;
- });
+ if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ if ($sourceFileInfo->isEncrypted()) {
+ /* Encrypted folder means e2ee encrypted */
+ $encryptedFiles[] = $sourceFileInfo;
+ } else {
+ $this->walkFiles($view, $sourcePath,
+ function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
+ if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ $mount = $fileInfo->getMountPoint();
+ // only analyze into folders from main storage,
+ if (
+ $mount->getMountProvider() instanceof IHomeMountProvider
+ || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
+ ) {
+ if ($fileInfo->isEncrypted()) {
+ /* Encrypted folder means e2ee encrypted, we cannot transfer it */
+ $encryptedFiles[] = $fileInfo;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ $progress->advance();
+ if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
+ /* Encrypted file means SSE, we can only transfer it if master key is enabled */
+ $encryptedFiles[] = $fileInfo;
+ }
+ return true;
+ });
+ }
+ } elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
+ /* Encrypted file means SSE, we can only transfer it if master key is enabled */
+ $encryptedFiles[] = $sourceFileInfo;
+ }
$progress->finish();
$output->writeln('');
// no file is allowed to be encrypted
if (!empty($encryptedFiles)) {
- $output->writeln("<error>Some files are encrypted - please decrypt them first.</error>");
+ $output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
foreach ($encryptedFiles as $encryptedFile) {
/** @var FileInfo $encryptedFile */
- $output->writeln(" " . $encryptedFile->getPath());
+ $output->writeln(' ' . $encryptedFile->getPath());
}
- throw new \Exception('Execution terminated.');
+ throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
}
}
@@ -303,7 +325,7 @@ class OwnershipTransferService {
foreach ($supportedShareTypes as $shareType) {
$offset = 0;
while (true) {
- $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset);
+ $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
$progress->advance(count($sharePage));
if (empty($sharePage)) {
break;
@@ -311,16 +333,11 @@ class OwnershipTransferService {
if ($path !== "$sourceUid/files") {
$sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
try {
- $relativePath = $view->getPath($share->getNodeId());
- $singleFileTranfer = $view->is_file($normalizedPath);
- if ($singleFileTranfer) {
- return Filesystem::normalizePath($relativePath) === $normalizedPath;
- }
+ $sourceNode = $share->getNode();
+ $relativePath = $view->getRelativePath($sourceNode->getPath());
- return mb_strpos(
- Filesystem::normalizePath($relativePath . '/', false),
- $normalizedPath . '/') === 0;
- } catch (\Exception $e) {
+ return str_starts_with($relativePath . '/', $normalizedPath . '/');
+ } catch (Exception $e) {
return false;
}
});
@@ -333,20 +350,31 @@ class OwnershipTransferService {
$progress->finish();
$output->writeln('');
- return array_map(fn (IShare $share) => [
- 'share' => $share,
- 'suffix' => substr(Filesystem::normalizePath($view->getPath($share->getNodeId())), strlen($normalizedPath)),
- ], $shares);
+ return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
+ try {
+ $nodePath = $view->getRelativePath($share->getNode()->getPath());
+ } catch (NotFoundException $e) {
+ $output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
+ return null;
+ }
+
+ return [
+ 'share' => $share,
+ 'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
+ ];
+ }, $shares)));
}
- private function collectIncomingShares(string $sourceUid,
+ private function collectIncomingShares(
+ string $sourceUid,
OutputInterface $output,
- View $view,
- bool $addKeys = false): array {
+ ?string $path,
+ ): array {
$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
$shares = [];
$progress = new ProgressBar($output);
+ $normalizedPath = Filesystem::normalizePath($path);
$offset = 0;
while (true) {
@@ -355,14 +383,19 @@ class OwnershipTransferService {
if (empty($sharePage)) {
break;
}
- if ($addKeys) {
- foreach ($sharePage as $singleShare) {
- $shares[$singleShare->getNodeId()] = $singleShare;
- }
- } else {
- foreach ($sharePage as $singleShare) {
- $shares[] = $singleShare;
- }
+
+ if ($path !== null && $path !== "$sourceUid/files") {
+ $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
+ try {
+ return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
+ } catch (Exception) {
+ return false;
+ }
+ });
+ }
+
+ foreach ($sharePage as $share) {
+ $shares[$share->getNodeId()] = $share;
}
$offset += 50;
@@ -377,11 +410,14 @@ class OwnershipTransferService {
/**
* @throws TransferOwnershipException
*/
- protected function transferFiles(string $sourceUid,
+ protected function transferFiles(
+ string $sourceUid,
string $sourcePath,
string $finalTarget,
View $view,
- OutputInterface $output): void {
+ OutputInterface $output,
+ bool $includeExternalStorage,
+ ): void {
$output->writeln("Transferring files to $finalTarget ...");
// This change will help user to transfer the folder specified using --path option.
@@ -390,15 +426,50 @@ class OwnershipTransferService {
$view->mkdir($finalTarget);
$finalTarget = $finalTarget . '/' . basename($sourcePath);
}
- if ($view->rename($sourcePath, $finalTarget) === false) {
- throw new TransferOwnershipException("Could not transfer files.", 1);
+ $sourceInfo = $view->getFileInfo($sourcePath);
+
+ /// handle the external storages mounted at the root, or the admin specifying an external storage with --path
+ if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
+ $this->moveMountContents($view, $sourcePath, $finalTarget);
+ } else {
+ if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
+ throw new TransferOwnershipException('Could not transfer files.', 1);
+ }
+ }
+
+ if ($includeExternalStorage) {
+ $nestedMounts = $this->mountManager->findIn($sourcePath);
+ foreach ($nestedMounts as $mount) {
+ if ($mount->getMountProvider() === ConfigAdapter::class) {
+ $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
+ $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
+ }
+ }
}
+
if (!is_dir("$sourceUid/files")) {
// because the files folder is moved away we need to recreate it
$view->mkdir("$sourceUid/files");
}
}
+ private function moveMountContents(View $rootView, string $source, string $target) {
+ if ($rootView->copy($source, $target)) {
+ // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
+ // we need to empty the contents instead
+ $content = $rootView->getDirectoryContent($source);
+ foreach ($content as $item) {
+ if ($item->getType() === FileInfo::TYPE_FOLDER) {
+ $rootView->rmdir($item->getPath());
+ } else {
+ $rootView->unlink($item->getPath());
+ }
+ }
+ } else {
+ throw new TransferOwnershipException("Could not transfer $source to $target");
+ }
+ }
+
/**
* @param string $targetLocation New location of the transfered node
* @param array<array{share: IShare, suffix: string}> $shares previously collected share information
@@ -410,14 +481,14 @@ class OwnershipTransferService {
array $shares,
OutputInterface $output,
):void {
- $output->writeln("Restoring shares ...");
+ $output->writeln('Restoring shares ...');
$progress = new ProgressBar($output, count($shares));
- $rootFolder = \OCP\Server::get(IRootFolder::class);
foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
try {
- if ($share->getShareType() === IShare::TYPE_USER &&
- $share->getSharedWith() === $destinationUid) {
+ $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedWith() === $destinationUid) {
// Unmount the shares before deleting, so we don't try to get the storage later on.
$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
if ($shareMountPoint) {
@@ -432,8 +503,8 @@ class OwnershipTransferService {
$share->setSharedBy($destinationUid);
}
- if ($share->getShareType() === IShare::TYPE_USER &&
- !$this->userManager->userExists($share->getSharedWith())) {
+ if ($share->getShareType() === IShare::TYPE_USER
+ && !$this->userManager->userExists($share->getSharedWith())) {
// stray share with deleted user
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
$this->shareManager->deleteShare($share);
@@ -448,18 +519,19 @@ class OwnershipTransferService {
// Normally the ID is preserved,
// but for transferes between different storages the ID might change
$newNodeId = $share->getNode()->getId();
- } catch (\OCP\Files\NotFoundException) {
+ } catch (NotFoundException) {
// ID has changed due to transfer between different storages
// Try to get the new ID from the target path and suffix of the share
- $node = $rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
+ $node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
$newNodeId = $node->getId();
+ $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
}
$share->setNodeId($newNodeId);
- $this->shareManager->updateShare($share);
+ $this->shareManager->updateShare($share, onlyValid: false);
}
}
- } catch (\OCP\Files\NotFoundException $e) {
+ } catch (NotFoundException $e) {
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
} catch (\Throwable $e) {
$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
@@ -478,7 +550,7 @@ class OwnershipTransferService {
string $path,
string $finalTarget,
bool $move): void {
- $output->writeln("Restoring incoming shares ...");
+ $output->writeln('Restoring incoming shares ...');
$progress = new ProgressBar($output, count($sourceShares));
$prefix = "$destinationUid/files";
$finalShareTarget = '';
@@ -497,8 +569,8 @@ class OwnershipTransferService {
}
$shareTarget = $share->getTarget();
$shareTarget = $finalShareTarget . $shareTarget;
- if ($share->getShareType() === IShare::TYPE_USER &&
- $share->getSharedBy() === $destinationUid) {
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedBy() === $destinationUid) {
$this->shareManager->deleteShare($share);
} elseif (isset($destinationShares[$share->getNodeId()])) {
$destinationShare = $destinationShares[$share->getNodeId()];
@@ -539,7 +611,7 @@ class OwnershipTransferService {
$this->shareManager->moveShare($share, $destinationUid);
continue;
}
- } catch (\OCP\Files\NotFoundException $e) {
+ } catch (NotFoundException $e) {
$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
} catch (\Throwable $e) {
$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');