Merge pull request #28118 from nextcloud/transfer-incoming-shares

This commit is contained in:
John Molakvoæ 2021-09-15 10:26:56 +02:00 committed by GitHub
commit 995aa65183
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 223 additions and 6 deletions

View file

@ -31,12 +31,14 @@ declare(strict_types=1);
* 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;
@ -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,
@ -83,6 +90,12 @@ class TransferOwnership extends Command {
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'
);
}
@ -111,12 +124,36 @@ class TransferOwnership extends Command {
}
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>");

View file

@ -29,6 +29,7 @@ declare(strict_types=1);
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files\Service;
use Closure;
@ -93,7 +94,8 @@ class OwnershipTransferService {
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();
@ -180,6 +182,31 @@ 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
);
}
}
private function walkFiles(View $view, $path, Closure $callBack) {
@ -253,6 +280,7 @@ class OwnershipTransferService {
$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) {
@ -288,6 +316,41 @@ class OwnershipTransferService {
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
*/
@ -356,4 +419,80 @@ class OwnershipTransferService {
$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('');
}
}

View file

@ -97,7 +97,7 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
/**
* @When /^transferring ownership from "([^"]+)" to "([^"]+)"/
* @When /^transferring ownership from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnership($user1, $user2) {
if ($this->runOcc(['files:transfer-ownership', $user1, $user2]) === 0) {
@ -109,7 +109,7 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
/**
* @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"/
* @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnershipPath($path, $user1, $user2) {
$path = '--path=' . $path;
@ -121,6 +121,18 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
}
/**
* @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$/

View file

@ -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