Skip to content

Commit

Permalink
Merge pull request #28118 from nextcloud/transfer-incoming-shares
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv authored Sep 15, 2021
2 parents 208a7b6 + 5b664e0 commit 995aa65
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 6 deletions.
43 changes: 40 additions & 3 deletions apps/files/lib/Command/TransferOwnership.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OCA\Files\Command;

use OCA\Files\Exception\TransferOwnershipException;
use OCA\Files\Service\OwnershipTransferService;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -51,17 +53,22 @@ class TransferOwnership extends Command {
/** @var OwnershipTransferService */
private $transferService;

/** @var IConfig */
private $config;

public function __construct(IUserManager $userManager,
OwnershipTransferService $transferService) {
OwnershipTransferService $transferService,
IConfig $config) {
parent::__construct();
$this->userManager = $userManager;
$this->transferService = $transferService;
$this->config = $config;
}

protected function configure() {
$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,
Expand All @@ -83,6 +90,12 @@ protected function configure() {
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,
'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)',
'2'
);
}

Expand Down Expand Up @@ -111,12 +124,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

try {
$includeIncomingArgument = $input->getOption('transfer-incoming-shares');

switch ($includeIncomingArgument) {
case '0':
$includeIncoming = false;
break;
case '1':
$includeIncoming = true;
break;
case '2':
$includeIncoming = $this->config->getSystemValue('transferIncomingShares', false);
if (gettype($includeIncoming) !== 'boolean') {
$output->writeln("<error> config.php: 'transfer-incoming-shares': wrong usage. Transfer aborted.</error>");
return 1;
}
break;
default:
$output->writeln("<error>Option --transfer-incoming-shares: wrong usage. Transfer aborted.</error>");
return 1;
break;
}

$this->transferService->transfer(
$sourceUserObject,
$destinationUserObject,
ltrim($input->getOption('path'), '/'),
$output,
$input->getOption('move') === true
$input->getOption('move') === true,
false,
$includeIncoming
);
} catch (TransferOwnershipException $e) {
$output->writeln("<error>" . $e->getMessage() . "</error>");
Expand Down
141 changes: 140 additions & 1 deletion apps/files/lib/Service/OwnershipTransferService.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files\Service;

use Closure;
Expand Down Expand Up @@ -93,7 +94,8 @@ public function transfer(IUser $sourceUser,
string $path,
?OutputInterface $output = null,
bool $move = false,
bool $firstLogin = false): void {
bool $firstLogin = false,
bool $transferIncomingShares = false): void {
$output = $output ?? new NullOutput();
$sourceUid = $sourceUser->getUID();
$destinationUid = $destinationUser->getUID();
Expand Down Expand Up @@ -180,6 +182,31 @@ public function transfer(IUser $sourceUser,
$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
);
}
}

private function walkFiles(View $view, $path, Closure $callBack) {
Expand Down Expand Up @@ -253,6 +280,7 @@ private function collectUsersShares(string $sourceUid,

$shares = [];
$progress = new ProgressBar($output);

foreach ([IShare::TYPE_GROUP, IShare::TYPE_USER, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_ROOM, IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_DECK] as $shareType) {
$offset = 0;
while (true) {
Expand Down Expand Up @@ -288,6 +316,41 @@ private function collectUsersShares(string $sourceUid,
return $shares;
}

private function collectIncomingShares(string $sourceUid,
OutputInterface $output,
View $view,
bool $addKeys = false): array {
$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");

$shares = [];
$progress = new ProgressBar($output);

$offset = 0;
while (true) {
$sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
$progress->advance(count($sharePage));
if (empty($sharePage)) {
break;
}
if ($addKeys) {
foreach ($sharePage as $singleShare) {
$shares[$singleShare->getNodeId()] = $singleShare;
}
} else {
foreach ($sharePage as $singleShare) {
$shares[] = $singleShare;
}
}

$offset += 50;
}


$progress->finish();
$output->writeln('');
return $shares;
}

/**
* @throws TransferOwnershipException
*/
Expand Down Expand Up @@ -356,4 +419,80 @@ private function restoreShares(string $sourceUid,
$progress->finish();
$output->writeln('');
}

private function transferIncomingShares(string $sourceUid,
string $destinationUid,
array $sourceShares,
array $destinationShares,
OutputInterface $output,
string $path,
string $finalTarget,
bool $move): void {
$output->writeln("Restoring incoming shares ...");
$progress = new ProgressBar($output, count($sourceShares));
$prefix = "$destinationUid/files";
if (substr($finalTarget, 0, strlen($prefix)) === $prefix) {
$finalShareTarget = substr($finalTarget, strlen($prefix));
}
foreach ($sourceShares as $share) {
try {
// Only restore if share is in given path.
$pathToCheck = '/' . trim($path) . '/';
if (substr($share->getTarget(), 0, strlen($pathToCheck)) !== $pathToCheck) {
continue;
}
$shareTarget = $share->getTarget();
$shareTarget = $finalShareTarget . $shareTarget;
if ($share->getShareType() === IShare::TYPE_USER &&
$share->getSharedBy() === $destinationUid) {
$this->shareManager->deleteShare($share);
} elseif (isset($destinationShares[$share->getNodeId()])) {
$destinationShare = $destinationShares[$share->getNodeId()];
// Keep the share which has the most permissions and discard the other one.
if ($destinationShare->getPermissions() < $share->getPermissions()) {
$this->shareManager->deleteShare($destinationShare);
$share->setSharedWith($destinationUid);
// trigger refetching of the node so that the new owner and mountpoint are taken into account
// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
$this->userMountCache->clear();
$share->setNodeId($share->getNode()->getId());
$this->shareManager->updateShare($share);
// The share is already transferred.
$progress->advance();
if ($move) {
continue;
}
$share->setTarget($shareTarget);
$this->shareManager->moveShare($share, $destinationUid);
continue;
}
$this->shareManager->deleteShare($share);
} elseif ($share->getShareOwner() === $destinationUid) {
$this->shareManager->deleteShare($share);
} else {
$share->setSharedWith($destinationUid);
$share->setNodeId($share->getNode()->getId());
$this->shareManager->updateShare($share);
// trigger refetching of the node so that the new owner and mountpoint are taken into account
// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
$this->userMountCache->clear();
// The share is already transferred.
$progress->advance();
if ($move) {
continue;
}
$share->setTarget($shareTarget);
$this->shareManager->moveShare($share, $destinationUid);
continue;
}
} 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('');
}
}
16 changes: 14 additions & 2 deletions build/integration/features/bootstrap/CommandLineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private function findLastTransferFolderForUser($sourceUser, $targetUser) {
}

/**
* @When /^transferring ownership from "([^"]+)" to "([^"]+)"/
* @When /^transferring ownership from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnership($user1, $user2) {
if ($this->runOcc(['files:transfer-ownership', $user1, $user2]) === 0) {
Expand All @@ -109,7 +109,7 @@ public function transferringOwnership($user1, $user2) {
}

/**
* @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"/
* @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnershipPath($path, $user1, $user2) {
$path = '--path=' . $path;
Expand All @@ -121,6 +121,18 @@ public function transferringOwnershipPath($path, $user1, $user2) {
}
}

/**
* @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)" with received shares$/
*/
public function transferringOwnershipPathWithIncomingShares($path, $user1, $user2) {
$path = '--path=' . $path;
if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2, '--transfer-incoming-shares=1']) === 0) {
$this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2);
} else {
// failure
$this->lastTransferPath = null;
}
}

/**
* @When /^using received transfer folder of "([^"]+)" as dav path$/
Expand Down
29 changes: 29 additions & 0 deletions build/integration/features/transfer-ownership.feature
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,35 @@ Feature: transfer-ownership
And Getting info of last share
And the OCS status code should be "404"

Scenario: transferring ownership transfers received shares into subdir when requested
Given user "user0" exists
And user "user1" exists
And user "user2" exists
And User "user2" created a folder "/transfer-share"
And User "user2" created a folder "/do-not-transfer"
And User "user0" created a folder "/sub"
And folder "/transfer-share" of user "user2" is shared with user "user0" with permissions 31
And user "user0" accepts last share
And User "user0" moved folder "/transfer-share" to "/sub/transfer-share"
And folder "/do-not-transfer" of user "user2" is shared with user "user0" with permissions 31
And user "user0" accepts last share
When transferring ownership of path "sub" from "user0" to "user1" with received shares
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then as "user1" the folder "/sub" exists
And as "user1" the folder "/do-not-transfer" does not exist
And as "user1" the folder "/sub/do-not-transfer" does not exist
And as "user1" the folder "/sub/transfer-share" exists
And using old dav path
And as "user1" the folder "/transfer-share" does not exist
And as "user1" the folder "/do-not-transfer" does not exist
And using old dav path
And as "user0" the folder "/sub" does not exist
And as "user0" the folder "/do-not-transfer" exists
And Getting info of last share
And the OCS status code should be "404"

Scenario: transferring ownership does not transfer external storage
Given user "user0" exists
And user "user1" exists
Expand Down

0 comments on commit 995aa65

Please sign in to comment.