perf(sharing): Avoid loading all shares from all users when unsharing

First check which users have a shares and for which providers and then
only load these shares.

Avoid doing at most 5 SQL queries for each users a share was shared with if
there are no shares.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
This commit is contained in:
Carl Schwan 2026-02-04 12:14:51 +01:00
parent 346c4bd69a
commit 0676fba514
No known key found for this signature in database
GPG key ID: 02325448204E452A
6 changed files with 143 additions and 15 deletions

View file

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

View file

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

View file

@ -1079,11 +1079,10 @@ class DefaultShareProvider implements
/**
* Create a share object from a database row
*
* @param mixed[] $data
* @return 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'])

View file

@ -19,6 +19,7 @@ use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\SharedStorage;
use OCA\ShareByMail\ShareByMailProvider;
use OCP\Constants;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
@ -32,6 +33,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;
@ -90,6 +92,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
@ -1039,14 +1042,50 @@ class Manager implements IManager {
IShare::TYPE_EMAIL,
];
foreach ($userIds as $userId) {
// Figure out which users has some shares with which providers
$qb = $this->connection->getQueryBuilder();
$qb->select('uid_initiator', 'share_type')
->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();
$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)) {
$rawShares[$data['uid_initiator']][] = $data['share_type'];
}
}
$cursor->closeCursor();
foreach ($rawShares as $userId => $shareTypes) {
foreach ($shareTypes as $shareType) {
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);

View file

@ -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();

View file

@ -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;
@ -33,6 +36,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;
@ -59,6 +63,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;
@ -75,7 +80,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;
@ -97,6 +102,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);
@ -110,6 +116,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);
@ -157,6 +164,7 @@ class ManagerTest extends \Test\TestCase {
$this->shareDisabledChecker,
$this->dateTimeZone,
$this->appConfig,
$this->connection,
);
}
@ -182,6 +190,7 @@ class ManagerTest extends \Test\TestCase {
$this->shareDisabledChecker,
$this->dateTimeZone,
$this->appConfig,
$this->connection,
]);
}
@ -486,6 +495,26 @@ class ManagerTest extends \Test\TestCase {
$manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare);
$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],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@ -546,6 +575,26 @@ 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],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@ -574,6 +623,26 @@ class ManagerTest extends \Test\TestCase {
/* No share is promoted because generalCreateChecks does not throw */
$manager->expects($this->never())->method('updateShare');
$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],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@ -641,6 +710,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_initiator' => 'userC', 'share_type' => IShare::TYPE_USER],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}