mirror of
https://github.com/nextcloud/server.git
synced 2026-03-07 07:50:57 -05:00
Merge pull request #58658 from nextcloud/backport/58057/stable33
[stable33] perf(sharing): Avoid loading all shares from all users when unsharing
This commit is contained in:
commit
e3d0ead392
6 changed files with 216 additions and 43 deletions
|
|
@ -18,6 +18,7 @@ use OCP\Files\Mount\IMountManager;
|
|||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDateTimeZone;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
|
|
@ -92,6 +93,7 @@ class CapabilitiesTest extends \Test\TestCase {
|
|||
$this->createMock(ShareDisableChecker::class),
|
||||
$this->createMock(IDateTimeZone::class),
|
||||
$appConfig,
|
||||
$this->createMock(IDBConnection::class),
|
||||
);
|
||||
|
||||
$cap = new Capabilities($config, $appConfig, $shareManager, $appManager);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ use OCP\IConfig;
|
|||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Share\IManager;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
#[Group(name: 'DB')]
|
||||
class SharingTest extends TestCase {
|
||||
private Sharing $admin;
|
||||
|
||||
|
|
|
|||
|
|
@ -1072,11 +1072,10 @@ class DefaultShareProvider implements
|
|||
/**
|
||||
* Create a share object from a database row
|
||||
*
|
||||
* @param mixed[] $data
|
||||
* @return \OCP\Share\IShare
|
||||
* @param array<string, mixed> $data
|
||||
* @throws InvalidShare
|
||||
*/
|
||||
private function createShare($data) {
|
||||
private function createShare($data): IShare {
|
||||
$share = new Share($this->rootFolder, $this->userManager);
|
||||
$share->setId($data['id'])
|
||||
->setShareType((int)$data['share_type'])
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use OC\Share20\Exception\ProviderException;
|
|||
use OCA\Files_Sharing\AppInfo\Application;
|
||||
use OCA\Files_Sharing\SharedStorage;
|
||||
use OCA\ShareByMail\ShareByMailProvider;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\File;
|
||||
|
|
@ -29,6 +30,7 @@ use OCP\HintException;
|
|||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDateTimeZone;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
|
|
@ -85,6 +87,7 @@ class Manager implements IManager {
|
|||
private ShareDisableChecker $shareDisableChecker,
|
||||
private IDateTimeZone $dateTimeZone,
|
||||
private IAppConfig $appConfig,
|
||||
private IDBConnection $connection,
|
||||
) {
|
||||
$this->l = $this->l10nFactory->get('lib');
|
||||
// The constructor of LegacyHooks registers the listeners of share events
|
||||
|
|
@ -1031,35 +1034,76 @@ class Manager implements IManager {
|
|||
IShare::TYPE_EMAIL,
|
||||
];
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
foreach ($shareTypes as $shareType) {
|
||||
// Figure out which users has some shares with which providers
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('uid_initiator', 'share_type', 'uid_owner', 'file_source')
|
||||
->from('share')
|
||||
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
|
||||
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->in('uid_initiator', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)),
|
||||
// Special case for old shares created via the web UI
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->in('uid_owner', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)),
|
||||
$qb->expr()->isNull('uid_initiator')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!$node instanceof Folder) {
|
||||
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
|
||||
$qb->orderBy('id');
|
||||
|
||||
$cursor = $qb->executeQuery();
|
||||
/** @var array<string, list<array{IShare::TYPE_*, Node}>> $rawShare */
|
||||
$rawShares = [];
|
||||
while ($data = $cursor->fetch()) {
|
||||
if (!isset($rawShares[$data['uid_initiator']])) {
|
||||
$rawShares[$data['uid_initiator']] = [];
|
||||
}
|
||||
if (!in_array($data['share_type'], $rawShares[$data['uid_initiator']], true)) {
|
||||
if ($node instanceof Folder) {
|
||||
if ($data['file_source'] === null || $data['uid_owner'] === null) {
|
||||
/* Ignore share of non-existing node */
|
||||
continue;
|
||||
}
|
||||
|
||||
// for federated shares the owner can be a remote user, in this
|
||||
// case we use the initiator
|
||||
if ($this->userManager->userExists($data['uid_owner'])) {
|
||||
$userFolder = $this->rootFolder->getUserFolder($data['uid_owner']);
|
||||
} else {
|
||||
$userFolder = $this->rootFolder->getUserFolder($data['uid_initiator']);
|
||||
}
|
||||
$sharedNode = $userFolder->getFirstNodeById((int)$data['file_source']);
|
||||
if (!$sharedNode) {
|
||||
continue;
|
||||
}
|
||||
if ($node->getRelativePath($sharedNode->getPath()) !== null) {
|
||||
$rawShares[$data['uid_initiator']][] = [(int)$data['share_type'], $sharedNode];
|
||||
}
|
||||
} elseif ($node instanceof File) {
|
||||
$rawShares[$data['uid_initiator']][] = [(int)$data['share_type'], $node];
|
||||
}
|
||||
}
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
|
||||
foreach ($rawShares as $userId => $shareInfos) {
|
||||
foreach ($shareInfos as $shareInfo) {
|
||||
[$shareType, $sharedNode] = $shareInfo;
|
||||
try {
|
||||
$provider = $this->factory->getProviderForType($shareType);
|
||||
} catch (ProviderException $e) {
|
||||
} catch (ProviderException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof Folder) {
|
||||
/* We need to get all shares by this user to get subshares */
|
||||
$shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0);
|
||||
|
||||
foreach ($shares as $share) {
|
||||
try {
|
||||
$path = $share->getNode()->getPath();
|
||||
} catch (NotFoundException) {
|
||||
/* Ignore share of non-existing node */
|
||||
continue;
|
||||
}
|
||||
if ($node->getRelativePath($path) !== null) {
|
||||
/* If relative path is not null it means the shared node is the same or in a subfolder */
|
||||
$reshareRecords[] = $share;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$shares = $provider->getSharesBy($userId, $shareType, $node, false, -1, 0);
|
||||
foreach ($shares as $child) {
|
||||
$reshareRecords[] = $child;
|
||||
}
|
||||
$shares = $provider->getSharesBy($userId, $shareType, $sharedNode, false, -1, 0);
|
||||
foreach ($shares as $child) {
|
||||
$reshareRecords[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ namespace Test\Share20;
|
|||
|
||||
use OC\EventDispatcher\EventDispatcher;
|
||||
use OC\Share20\LegacyHooks;
|
||||
use OC\Share20\Manager;
|
||||
use OCP\Constants;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
|
|
@ -24,6 +23,7 @@ use OCP\Share\Events\ShareDeletedFromSelfEvent;
|
|||
use OCP\Share\IManager as IShareManager;
|
||||
use OCP\Share\IShare;
|
||||
use OCP\Util;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
|
|
@ -40,15 +40,11 @@ class Dummy {
|
|||
}
|
||||
}
|
||||
|
||||
#[Group(name: 'DB')]
|
||||
class LegacyHooksTest extends TestCase {
|
||||
/** @var LegacyHooks */
|
||||
private $hooks;
|
||||
|
||||
/** @var IEventDispatcher */
|
||||
private $eventDispatcher;
|
||||
|
||||
/** @var Manager */
|
||||
private $manager;
|
||||
private LegacyHooks $hooks;
|
||||
private IEventDispatcher $eventDispatcher;
|
||||
private IShareManager $manager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ use OC\Share20\Manager;
|
|||
use OC\Share20\Share;
|
||||
use OC\Share20\ShareDisableChecker;
|
||||
use OCP\Constants;
|
||||
use OCP\DB\IResult;
|
||||
use OCP\DB\QueryBuilder\IExpressionBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\File;
|
||||
|
|
@ -32,6 +35,7 @@ use OCP\HintException;
|
|||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDateTimeZone;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IL10N;
|
||||
|
|
@ -58,6 +62,7 @@ use OCP\Share\IShareProvider;
|
|||
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
|
||||
use OCP\Util;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -74,7 +79,7 @@ class DummyShareManagerListener {
|
|||
*
|
||||
* @package Test\Share20
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\Group('DB')]
|
||||
#[Group(name: 'DB')]
|
||||
class ManagerTest extends \Test\TestCase {
|
||||
protected Manager $manager;
|
||||
protected LoggerInterface&MockObject $logger;
|
||||
|
|
@ -96,6 +101,7 @@ class ManagerTest extends \Test\TestCase {
|
|||
private DateTimeZone $timezone;
|
||||
protected IDateTimeZone&MockObject $dateTimeZone;
|
||||
protected IAppConfig&MockObject $appConfig;
|
||||
protected IDBConnection&MockObject $connection;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
|
@ -109,6 +115,7 @@ class ManagerTest extends \Test\TestCase {
|
|||
$this->dispatcher = $this->createMock(IEventDispatcher::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->knownUserService = $this->createMock(KnownUserService::class);
|
||||
$this->connection = $this->createMock(IDBConnection::class);
|
||||
|
||||
$this->shareDisabledChecker = new ShareDisableChecker($this->config, $this->userManager, $this->groupManager);
|
||||
$this->dateTimeZone = $this->createMock(IDateTimeZone::class);
|
||||
|
|
@ -156,6 +163,7 @@ class ManagerTest extends \Test\TestCase {
|
|||
$this->shareDisabledChecker,
|
||||
$this->dateTimeZone,
|
||||
$this->appConfig,
|
||||
$this->connection,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +189,7 @@ class ManagerTest extends \Test\TestCase {
|
|||
$this->shareDisabledChecker,
|
||||
$this->dateTimeZone,
|
||||
$this->appConfig,
|
||||
$this->connection,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -457,6 +466,7 @@ class ManagerTest extends \Test\TestCase {
|
|||
->getMock();
|
||||
|
||||
$file = $this->createMock(File::class);
|
||||
$file->method('getId')->willReturn(42);
|
||||
|
||||
$share = $this->createMock(IShare::class);
|
||||
$share->method('getShareType')->willReturn(IShare::TYPE_USER);
|
||||
|
|
@ -485,6 +495,33 @@ class ManagerTest extends \Test\TestCase {
|
|||
|
||||
$manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare);
|
||||
|
||||
$this->userManager->method('userExists')->willReturn(true);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder);
|
||||
$userFolder->method('getFirstNodeById')
|
||||
->with(42)
|
||||
->willReturn($file);
|
||||
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
$result = $this->createMock(IResult::class);
|
||||
$qb->method('select')
|
||||
->willReturn($qb);
|
||||
$qb->method('from')
|
||||
->willReturn($qb);
|
||||
$qb->method('andWhere')
|
||||
->willReturn($qb);
|
||||
$qb->method('expr')
|
||||
->willReturn($this->createMock(IExpressionBuilder::class));
|
||||
$qb->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$this->connection->method('getQueryBuilder')
|
||||
->willReturn($qb);
|
||||
$result->method('fetch')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42],
|
||||
false,
|
||||
);
|
||||
|
||||
self::invokePrivate($manager, 'promoteReshares', [$share]);
|
||||
}
|
||||
|
||||
|
|
@ -522,14 +559,18 @@ class ManagerTest extends \Test\TestCase {
|
|||
$reShareInOtherFolder->method('getNode')->willReturn($otherFolder);
|
||||
|
||||
$this->defaultProvider->method('getSharesBy')
|
||||
->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare, $reShareInSubFolder, $reShareInOtherFolder) {
|
||||
->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($folder, $subFolder, $reShare, $reShareInSubFolder, $reShareInOtherFolder) {
|
||||
if ($shareType === IShare::TYPE_USER) {
|
||||
return match($userId) {
|
||||
'userB' => [$reShare,$reShareInSubFolder,$reShareInOtherFolder],
|
||||
};
|
||||
} else {
|
||||
return [];
|
||||
if ($userId === 'userB') {
|
||||
if ($node === $folder) {
|
||||
return [$reShare];
|
||||
}
|
||||
if ($node === $subFolder) {
|
||||
return [$reShareInSubFolder];
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->fail();
|
||||
});
|
||||
$manager->method('generalCreateChecks')->willThrowException(new GenericShareException());
|
||||
|
||||
|
|
@ -545,6 +586,40 @@ class ManagerTest extends \Test\TestCase {
|
|||
return $expected;
|
||||
});
|
||||
|
||||
$this->userManager->method('userExists')->willReturn(true);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder);
|
||||
$userFolder->method('getFirstNodeById')
|
||||
->willReturnCallback(function ($id) use ($subFolder, $otherFolder, $folder) {
|
||||
return match ($id) {
|
||||
41 => $subFolder,
|
||||
42 => $otherFolder,
|
||||
43 => $folder,
|
||||
};
|
||||
});
|
||||
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
$result = $this->createMock(IResult::class);
|
||||
$qb->method('select')
|
||||
->willReturn($qb);
|
||||
$qb->method('from')
|
||||
->willReturn($qb);
|
||||
$qb->method('andWhere')
|
||||
->willReturn($qb);
|
||||
$qb->method('expr')
|
||||
->willReturn($this->createMock(IExpressionBuilder::class));
|
||||
$qb->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$this->connection->method('getQueryBuilder')
|
||||
->willReturn($qb);
|
||||
$result->method('fetch')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 41],
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42],
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 43],
|
||||
false,
|
||||
);
|
||||
|
||||
self::invokePrivate($manager, 'promoteReshares', [$share]);
|
||||
}
|
||||
|
||||
|
|
@ -573,6 +648,33 @@ class ManagerTest extends \Test\TestCase {
|
|||
/* No share is promoted because generalCreateChecks does not throw */
|
||||
$manager->expects($this->never())->method('updateShare');
|
||||
|
||||
$this->userManager->method('userExists')->willReturn(true);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder);
|
||||
$userFolder->method('getFirstNodeById')
|
||||
->with(42)
|
||||
->willReturn($folder);
|
||||
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
$result = $this->createMock(IResult::class);
|
||||
$qb->method('select')
|
||||
->willReturn($qb);
|
||||
$qb->method('from')
|
||||
->willReturn($qb);
|
||||
$qb->method('andWhere')
|
||||
->willReturn($qb);
|
||||
$qb->method('expr')
|
||||
->willReturn($this->createMock(IExpressionBuilder::class));
|
||||
$qb->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$this->connection->method('getQueryBuilder')
|
||||
->willReturn($qb);
|
||||
$result->method('fetch')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42],
|
||||
false,
|
||||
);
|
||||
|
||||
self::invokePrivate($manager, 'promoteReshares', [$share]);
|
||||
}
|
||||
|
||||
|
|
@ -628,6 +730,13 @@ class ManagerTest extends \Test\TestCase {
|
|||
|
||||
$manager->method('getSharedWith')->willReturn([]);
|
||||
|
||||
$this->userManager->method('userExists')->willReturn(true);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder);
|
||||
$userFolder->method('getFirstNodeById')
|
||||
->with(42)
|
||||
->willReturn($folder);
|
||||
|
||||
$calls = [
|
||||
$reShare1,
|
||||
$reShare2,
|
||||
|
|
@ -640,6 +749,27 @@ class ManagerTest extends \Test\TestCase {
|
|||
return $expected;
|
||||
});
|
||||
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
$result = $this->createMock(IResult::class);
|
||||
$qb->method('select')
|
||||
->willReturn($qb);
|
||||
$qb->method('from')
|
||||
->willReturn($qb);
|
||||
$qb->method('andWhere')
|
||||
->willReturn($qb);
|
||||
$qb->method('expr')
|
||||
->willReturn($this->createMock(IExpressionBuilder::class));
|
||||
$qb->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$this->connection->method('getQueryBuilder')
|
||||
->willReturn($qb);
|
||||
$result->method('fetch')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42],
|
||||
['uid_initiator' => 'userC', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42],
|
||||
false,
|
||||
);
|
||||
|
||||
self::invokePrivate($manager, 'promoteReshares', [$share]);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue