Skip to content

Commit

Permalink
fix(sharing): add command to fix broken shares after ownership transf…
Browse files Browse the repository at this point in the history
…erring

Signed-off-by: Luka Trovic <[email protected]>
  • Loading branch information
luka-nextcloud committed Nov 8, 2024
1 parent 78cf312 commit cc6c3dc
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 0 deletions.
1 change: 1 addition & 0 deletions apps/files_sharing/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Turning the feature off removes shared files and folders on the server for all s
<command>OCA\Files_Sharing\Command\CleanupRemoteStorages</command>
<command>OCA\Files_Sharing\Command\ExiprationNotification</command>
<command>OCA\Files_Sharing\Command\DeleteOrphanShares</command>
<command>OCA\Files_Sharing\Command\FixBrokenShares</command>
</commands>

<settings>
Expand Down
1 change: 1 addition & 0 deletions apps/files_sharing/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => $baseDir . '/../lib/Command/CleanupRemoteStorages.php',
'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => $baseDir . '/../lib/Command/DeleteOrphanShares.php',
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Command\\FixBrokenShares' => $baseDir . '/../lib/Command/FixBrokenShares.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files_sharing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => __DIR__ . '/..' . '/../lib/Command/CleanupRemoteStorages.php',
'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanShares.php',
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Command\\FixBrokenShares' => __DIR__ . '/..' . '/../lib/Command/FixBrokenShares.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php',
Expand Down
65 changes: 65 additions & 0 deletions apps/files_sharing/lib/Command/FixBrokenShares.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_Sharing\Command;

use OC\Core\Command\Base;
use OCA\Files_Sharing\OrphanHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class FixBrokenShares extends Base {
public function __construct(
private readonly OrphanHelper $orphanHelper
) {
parent::__construct();
}

protected function configure(): void {
$this
->setName('sharing:fix-broken-shares')
->setDescription('Fix broken shares after transfer ownership on old versions')
->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'only show which shares would be updated'
);
}

public function execute(InputInterface $input, OutputInterface $output): int {
$shares = $this->orphanHelper->getAllShares();
$dryRun = $input->getOption('dry-run');
$count = 0;

foreach ($shares as $share) {
if ($this->orphanHelper->isShareValid($share['owner'], $share['fileid']) || !$this->orphanHelper->fileExists($share['fileid'])) {
continue;
}

$owner = $this->orphanHelper->findOwner($share['fileid']);

if ($owner !== null) {
if ($dryRun) {
$output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) can be updated to owner <info>$owner</info>");
} else {
$this->orphanHelper->updateShareOwner($share['id'], $owner);
$output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) updated to owner <info>$owner</info>");
}
$count++;
}
}

if ($count === 0) {
$output->writeln('No broken shares detected');
}

return static::SUCCESS;
}
}
24 changes: 24 additions & 0 deletions apps/files_sharing/lib/OrphanHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

use OC\User\NoUserException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\IRootFolder;
use OCP\IDBConnection;

class OrphanHelper {
public function __construct(
private IDBConnection $connection,
private IRootFolder $rootFolder,
private IUserMountCache $userMountCache,
) {
}

Expand Down Expand Up @@ -68,4 +70,26 @@ public function getAllShares() {
];
}
}

public function findOwner(int $fileId): ?string {
$mounts = $this->userMountCache->getMountsForFileId($fileId);
if (!$mounts) {
return null;
}
foreach ($mounts as $mount) {
// Only the mount of owner has the internal path value
if ($mount->getInternalPath()) {
return $mount->getUser()->getUID();
}
}
return null;
}

public function updateShareOwner(int $shareId, string $owner): void {
$query = $this->connection->getQueryBuilder();
$query->update('share')
->set('uid_owner', $query->createNamedParameter($owner))
->where($query->expr()->eq('id', $query->createNamedParameter($shareId, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
}
}
116 changes: 116 additions & 0 deletions apps/files_sharing/tests/Command/FixBrokenSharesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/**
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files_Sharing\Tests\Command;

use OCA\Files_Sharing\Command\FixBrokenShares;
use OCA\Files_Sharing\OrphanHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Test\TestCase;

/**
* Class FixBrokenSharesTest
*
* @package OCA\Files_Sharing\Tests\Command
*/
class FixBrokenSharesTest extends TestCase {
/**
* @var FixBrokenShares
*/
private $command;

/**
* @var OrphanHelper|\PHPUnit\Framework\MockObject\MockObject
*/
private $orphanHelper;

protected function setUp(): void {
parent::setUp();

$this->orphanHelper = $this->createMock(OrphanHelper::class);
$this->command = new FixBrokenShares($this->orphanHelper);
}

public function testExecuteNoSharesDetected() {
$this->orphanHelper->expects($this->once())
->method('getAllShares')
->willReturn([
['id' => 1, 'owner' => 'user1', 'fileid' => 1, 'target' => 'target1'],
['id' => 2, 'owner' => 'user2', 'fileid' => 2, 'target' => 'target2'],
]);
$this->orphanHelper->expects($this->exactly(2))
->method('isShareValid')
->willReturn(true);

$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);

$output->expects($this->once())
->method('writeln')
->with('No broken shares detected');
$this->command->execute($input, $output);
}

public function testExecuteSharesDetected() {
$this->orphanHelper->expects($this->once())
->method('getAllShares')
->willReturn([
['id' => 1, 'owner' => 'user1', 'fileid' => 1, 'target' => 'target1'],
['id' => 2, 'owner' => 'user2', 'fileid' => 2, 'target' => 'target2'],
]);
$this->orphanHelper->expects($this->exactly(2))
->method('isShareValid')
->willReturnOnConsecutiveCalls(true, false);
$this->orphanHelper->expects($this->once())
->method('fileExists')
->willReturn(true);
$this->orphanHelper->expects($this->once())
->method('findOwner')
->willReturn('newOwner');
$this->orphanHelper->expects($this->once())
->method('updateShareOwner');

$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);

$output->expects($this->once())
->method('writeln')
->with('Share with id <info>2</info> (target: <info>target2</info>) updated to owner <info>newOwner</info>');
$this->command->execute($input, $output);
}

public function testExecuteSharesDetectedDryRun() {
$this->orphanHelper->expects($this->once())
->method('getAllShares')
->willReturn([
['id' => 1, 'owner' => 'user1', 'fileid' => 1, 'target' => 'target1'],
['id' => 2, 'owner' => 'user2', 'fileid' => 2, 'target' => 'target2'],
]);
$this->orphanHelper->expects($this->exactly(2))
->method('isShareValid')
->willReturnOnConsecutiveCalls(true, false);
$this->orphanHelper->expects($this->once())
->method('fileExists')
->willReturn(true);
$this->orphanHelper->expects($this->once())
->method('findOwner')
->willReturn('newOwner');
$this->orphanHelper->expects($this->never())
->method('updateShareOwner');

$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);

$output->expects($this->once())
->method('writeln')
->with('Share with id <info>2</info> (target: <info>target2</info>) can be updated to owner <info>newOwner</info>');
$input->expects($this->once())
->method('getOption')
->with('dry-run')
->willReturn(true);
$this->command->execute($input, $output);
}
}

0 comments on commit cc6c3dc

Please sign in to comment.