server/apps/files/lib/Service/OwnershipTransferService.php
Roeland Jago Douma 38735c2bd0 Allow specifying this is the first login
On firstlogin we allow non empty target folders. So that for guest
transfers the user sees the same UI.

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-02-03 15:00:03 +00:00

305 lines
9 KiB
PHP

<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Sascha Wiswedel <sascha.wiswedel@nextcloud.com>
*
* @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/>.
*
*/
namespace OCA\Files\Service;
use Closure;
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Files\Exception\TransferOwnershipException;
use OCP\Encryption\IManager as IEncryptionManager;
use OCP\Files\FileInfo;
use OCP\Files\IHomeStorage;
use OCP\Files\Mount\IMountManager;
use OCP\IUser;
use OCP\Share\IManager as IShareManager;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use function array_merge;
use function basename;
use function count;
use function date;
use function is_dir;
use function rtrim;
class OwnershipTransferService {
/** @var IEncryptionManager */
private $encryptionManager;
/** @var IShareManager */
private $shareManager;
/** @var IMountManager */
private $mountManager;
public function __construct(IEncryptionManager $manager,
IShareManager $shareManager,
IMountManager $mountManager) {
$this->encryptionManager = $manager;
$this->shareManager = $shareManager;
$this->mountManager = $mountManager;
}
/**
* @param IUser $sourceUser
* @param IUser $destinationUser
* @param string $path
*
* @param OutputInterface|null $output
* @param bool $move
* @throws TransferOwnershipException
* @throws \OC\User\NoUserException
*/
public function transfer(IUser $sourceUser,
IUser $destinationUser,
string $path,
?OutputInterface $output = null,
bool $move = false,
bool $firstLogin = false): void {
$output = $output ?? new NullOutput();
$sourceUid = $sourceUser->getUID();
$destinationUid = $destinationUser->getUID();
$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
// target user has to be ready
if ($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);
}
if ($move) {
$finalTarget = "$destinationUid/files/";
} else {
$date = date('Y-m-d H-i-s');
$finalTarget = "$destinationUid/files/transferred from $sourceUid on $date";
}
// setup filesystem
Filesystem::initMountPoints($sourceUid);
Filesystem::initMountPoints($destinationUid);
$view = new View();
if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
throw new TransferOwnershipException("Unknown path provided: $path", 1);
}
if ($move && (
!$view->is_dir($finalTarget) || (
!$firstLogin &&
count($view->getDirectoryContent($finalTarget)) > 0
)
)
) {
throw new TransferOwnershipException("Destination path does not exists or is not empty", 1);
}
// analyse source folder
$this->analyse(
$sourceUid,
$destinationUid,
$sourcePath,
$view,
$output
);
// collect all the shares
$shares = $this->collectUsersShares(
$sourceUid,
$output
);
// transfer the files
$this->transferFiles(
$sourceUid,
$sourcePath,
$finalTarget,
$view,
$output
);
// restore the shares
$this->restoreShares(
$sourceUid,
$destinationUid,
$shares,
$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);
}
}
}
/**
* @param OutputInterface $output
*
* @throws \Exception
*/
protected function analyse(string $sourceUid,
string $destinationUid,
string $sourcePath,
View $view,
OutputInterface $output): void {
$output->writeln('Validating quota');
$size = $view->getFileInfo($sourcePath, false)->getSize(false);
$freeSpace = $view->free_space($destinationUid . '/files/');
if ($size > $freeSpace) {
$output->writeln('<error>Target user does not have enough free space available.</error>');
throw new \Exception('Execution terminated.');
}
$output->writeln("Analysing files of $sourceUid ...");
$progress = new ProgressBar($output);
$progress->start();
$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;
});
$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>");
foreach ($encryptedFiles as $encryptedFile) {
/** @var FileInfo $encryptedFile */
$output->writeln(" " . $encryptedFile->getPath());
}
throw new \Exception('Execution terminated.');
}
}
private function collectUsersShares(string $sourceUid,
OutputInterface $output): array {
$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
$shares = [];
$progress = new ProgressBar($output);
foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {
$offset = 0;
while (true) {
$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset);
$progress->advance(count($sharePage));
if (empty($sharePage)) {
break;
}
$shares = array_merge($shares, $sharePage);
$offset += 50;
}
}
$progress->finish();
$output->writeln('');
return $shares;
}
/**
* @throws TransferOwnershipException
*/
protected function transferFiles(string $sourceUid,
string $sourcePath,
string $finalTarget,
View $view,
OutputInterface $output): void {
$output->writeln("Transferring files to $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 ($sourcePath !== "$sourceUid/files") {
$view->mkdir($finalTarget);
$finalTarget = $finalTarget . '/' . basename($sourcePath);
}
if ($view->rename($sourcePath, $finalTarget) === false) {
throw new TransferOwnershipException("Could not transfer files.", 1);
}
if (!is_dir("$sourceUid/files")) {
// because the files folder is moved away we need to recreate it
$view->mkdir("$sourceUid/files");
}
}
private function restoreShares(string $sourceUid,
string $destinationUid,
array $shares,
OutputInterface $output) {
$output->writeln("Restoring shares ...");
$progress = new ProgressBar($output, count($shares));
foreach ($shares as $share) {
try {
if ($share->getShareType() === \OCP\Share::SHARE_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) {
$this->mountManager->removeMount($shareMountPoint->getMountPoint());
}
$this->shareManager->deleteShare($share);
} else {
if ($share->getShareOwner() === $sourceUid) {
$share->setShareOwner($destinationUid);
}
if ($share->getSharedBy() === $sourceUid) {
$share->setSharedBy($destinationUid);
}
$this->shareManager->updateShare($share);
}
} catch (\OCP\Files\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>');
}
$progress->advance();
}
$progress->finish();
$output->writeln('');
}
}