diff --git a/lib/ACL/RuleManager.php b/lib/ACL/RuleManager.php index 0f4272168..3334b882e 100644 --- a/lib/ACL/RuleManager.php +++ b/lib/ACL/RuleManager.php @@ -52,15 +52,24 @@ private function createRule(array $data): ?Rule { public function getRulesForFilesById(IUser $user, array $fileIds): array { $userMappings = $this->userMappingManager->getMappingsForUser($user); + if ($fileIds === []) { + return []; + } + $query = $this->connection->getQueryBuilder(); $query->select(['fileid', 'mapping_type', 'mapping_id', 'mask', 'permissions']) ->from('group_folders_acl') - ->where($query->expr()->in('fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($query->expr()->orX(...array_map(fn (IUserMapping $userMapping): ICompositeExpression => $query->expr()->andX( $query->expr()->eq('mapping_type', $query->createNamedParameter($userMapping->getType())), $query->expr()->eq('mapping_id', $query->createNamedParameter($userMapping->getId())) ), $userMappings))); + $fileIdConditions = $query->expr()->orX(); + foreach (array_chunk($fileIds, 1000) as $chunk) { + $fileIdConditions->add($query->expr()->in('fileid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + } + $query->andWhere($fileIdConditions); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); @@ -192,16 +201,24 @@ public function getRulesForFilesByParent(IUser $user, int $storageId, int $paren public function getAllRulesForPaths(int $storageId, array $filePaths): array { $hashes = array_map(fn (string $path): string => md5(trim($path, '/')), $filePaths); + if ($hashes === []) { + return []; + } + $query = $this->connection->getQueryBuilder(); $query->select(['f.fileid', 'mapping_type', 'mapping_id', 'mask', 'a.permissions', 'f.path']) ->from('group_folders_acl', 'a') ->innerJoin('a', 'filecache', 'f', $query->expr()->eq('f.fileid', 'a.fileid')) - ->where($query->expr()->in('f.path_hash', $query->createNamedParameter($hashes, IQueryBuilder::PARAM_STR_ARRAY))) ->andWhere($query->expr()->eq('f.storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); - $rows = $query->executeQuery()->fetchAll(); + $hashConditions = $query->expr()->orX(); + foreach (array_chunk($hashes, 1000) as $chunk) { + $hashConditions->add($query->expr()->in('f.path_hash', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))); + } + $query->andWhere($hashConditions); /** @var list $rows */ + $rows = $query->executeQuery()->fetchAll(); return $this->rulesByPath($rows); } diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 9bc3868c6..6ff4939a8 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -187,6 +187,10 @@ public function getAllFoldersWithSize(int $offset = 0, ?int $limit = null, strin public function getAllFoldersForUserWithSize(IUser $user): array { $groups = $this->groupManager->getUserGroupIds($user); + if ($groups === []) { + return []; + } + $query = $this->selectWithFileCache(); $query->innerJoin( 'f', @@ -194,8 +198,13 @@ public function getAllFoldersForUserWithSize(IUser $user): array { 'a', $query->expr()->eq('f.folder_id', 'a.folder_id'), ) - ->selectAlias('a.permissions', 'group_permissions') - ->where($query->expr()->in('a.group_id', $query->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))); + ->selectAlias('a.permissions', 'group_permissions'); + + $groupConditions = $query->expr()->orX(); + foreach (array_chunk($groups, 1000) as $chunk) { + $groupConditions->add($query->expr()->in('a.group_id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))); + } + $query->where($groupConditions); /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); @@ -232,11 +241,14 @@ private function getAllFolderMappings(?array $folderIds = null): array { } $query = $this->connection->getQueryBuilder(); - $query->select('*') ->from('group_folders_manage', 'g'); if ($folderIds !== null) { - $query->where($query->expr()->in('folder_id', $query->createNamedParameter($folderIds, IQueryBuilder::PARAM_INT_ARRAY))); + $folderConditions = $query->expr()->orX(); + foreach (array_chunk($folderIds, 1000) as $chunk) { + $folderConditions->add($query->expr()->in('folder_id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + } + $query->where($folderConditions); } /** @var list $rows */ @@ -245,7 +257,6 @@ private function getAllFolderMappings(?array $folderIds = null): array { $folderMap = []; foreach ($rows as $row) { $id = $row['folder_id']; - $folderMap[$id] ??= []; $folderMap[$id][] = $row; } @@ -377,20 +388,21 @@ private function getAllApplicable(?array $folderIds = null): array { } $queryHelper = $this->getCirclesManager()?->getQueryHelper(); - $query = $queryHelper?->getQueryBuilder() ?? $this->connection->getQueryBuilder(); $query->select('g.folder_id', 'g.group_id', 'g.circle_id', 'g.permissions') ->from('group_folders_groups', 'g'); if ($folderIds !== null) { - $query->where($query->expr()->in('g.folder_id', $query->createNamedParameter($folderIds, IQueryBuilder::PARAM_INT_ARRAY))); + $folderConditions = $query->expr()->orX(); + foreach (array_chunk($folderIds, 1000) as $chunk) { + $folderConditions->add($query->expr()->in('g.folder_id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + } + $query->where($folderConditions); } - $queryHelper?->addCircleDetails('g', 'circle_id'); /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $applicableMap = []; - $groupDisplayNameCache = []; foreach ($rows as $row) { diff --git a/lib/Trash/TrashManager.php b/lib/Trash/TrashManager.php index faf507081..61c8b66ab 100644 --- a/lib/Trash/TrashManager.php +++ b/lib/Trash/TrashManager.php @@ -22,12 +22,20 @@ public function __construct( * @return list */ public function listTrashForFolders(array $folderIds): array { - $query = $this->connection->getQueryBuilder(); + if ($folderIds === []) { + return []; + } + $query = $this->connection->getQueryBuilder(); $query->select(['trash_id', 'name', 'deleted_time', 'original_location', 'folder_id', 'file_id', 'deleted_by']) ->from('group_folders_trash') - ->orderBy('deleted_time') - ->where($query->expr()->in('folder_id', $query->createNamedParameter($folderIds, IQueryBuilder::PARAM_INT_ARRAY))); + ->orderBy('deleted_time'); + + $folderConditions = $query->expr()->orX(); + foreach (array_chunk($folderIds, 1000) as $chunk) { + $folderConditions->add($query->expr()->in('folder_id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + } + $query->where($folderConditions); /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); diff --git a/tests/ACL/RuleManagerTest.php b/tests/ACL/RuleManagerTest.php index 4018c1688..9afe7e3b9 100644 --- a/tests/ACL/RuleManagerTest.php +++ b/tests/ACL/RuleManagerTest.php @@ -230,4 +230,92 @@ public function testGetByParent(): void { $this->ruleManager->deleteRule($rule1); $this->ruleManager->deleteRule($rule2); } + + public function testGetRulesForFilesByIdChunked(): void { + $storage = new Temporary([]); + $storage->mkdir('foo'); + for ($i = 0; $i < 1001; $i++) { + $storage->touch('foo/file_' . $i); + } + $storage->getScanner()->scan(''); + $cache = $storage->getCache(); + + $mapping = new UserMapping('user', '1'); + $this->userMappingManager->expects($this->any()) + ->method('getMappingsForUser') + ->with($this->user) + ->willReturn([$mapping]); + + $this->eventDispatcher->expects($this->any()) + ->method('dispatchTyped'); + + $targetId = $cache->getId('foo/file_0'); + $rule = new Rule($mapping, $targetId, 0b00001111, 0b00001001); + $this->ruleManager->saveRule($rule); + + $fileIds = []; + for ($i = 0; $i < 1001; $i++) { + $fileIds[] = $cache->getId('foo/file_' . $i); + } + + $result = $this->ruleManager->getRulesForFilesById($this->user, $fileIds); + $this->assertEquals([$targetId => [$rule]], $result); + + // cleanup + $this->ruleManager->deleteRule($rule); + } + + public function testGetAllRulesForPaths(): void { + $storage = new Temporary([]); + $storage->mkdir('foo'); + $storage->touch('foo/bar'); + $storage->getScanner()->scan(''); + $cache = $storage->getCache(); + $fooId = $cache->getId('foo'); + $storageId = $cache->getNumericStorageId(); + + $mapping = new UserMapping('user', '1'); + + $this->eventDispatcher->expects($this->any()) + ->method('dispatchTyped'); + + $rule = new Rule($mapping, $fooId, 0b00001111, 0b00001001); + $this->ruleManager->saveRule($rule); + + $result = $this->ruleManager->getAllRulesForPaths($storageId, ['foo', 'foo/bar', 'nonexistent']); + $this->assertEquals(['foo' => [$rule]], $result); + + // cleanup + $this->ruleManager->deleteRule($rule); + } + + public function testGetAllRulesForPathsChunked(): void { + $storage = new Temporary([]); + $storage->mkdir('foo'); + $paths = []; + for ($i = 0; $i < 1100; $i++) { + $path = 'foo/' . $i; + $paths[] = $path; + $storage->touch($path); + } + $storage->getScanner()->scan(''); + $cache = $storage->getCache(); + $fooId = $cache->getId('foo'); + $storageId = $cache->getNumericStorageId(); + + $mapping = new UserMapping('user', '1'); + + $this->eventDispatcher->expects($this->any()) + ->method('dispatchTyped'); + + $rule = new Rule($mapping, $fooId, 0b00001111, 0b00001001); + $this->ruleManager->saveRule($rule); + + $result = $this->ruleManager->getAllRulesForPaths($storageId, array_merge(['foo'], $paths)); + $this->assertArrayHasKey('foo', $result); + $this->assertEquals([$rule], $result['foo']); + + // cleanup + $this->ruleManager->deleteRule($rule); + } } diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index eda4f24ee..217753e21 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -517,4 +517,48 @@ public function testQuotaDefaultValue(): void { } $this->assertEquals(1024 ** 4, $folder->quota); } + + public function testGetAllFoldersForUserWithSize(): void { + $this->config->expects($this->any()) + ->method('getSystemValueInt') + ->with('groupfolders.quota.default', FileInfo::SPACE_UNLIMITED) + ->willReturn(FileInfo::SPACE_UNLIMITED); + + $this->mimeLoader->expects($this->any()) + ->method('getMimetypeById') + ->willReturn('application/octet-stream'); + + $folderId = $this->manager->createFolder('test'); + $this->manager->addApplicableGroup($folderId, 'g1'); + + $user = $this->getUser(['g1', 'g2']); + $result = $this->manager->getAllFoldersForUserWithSize($user); + + $this->assertCount(1, $result); + $this->assertArrayHasKey($folderId, $result); + } + + public function testGetAllFoldersForUserWithSizeChunked(): void { + $this->config->expects($this->any()) + ->method('getSystemValueInt') + ->with('groupfolders.quota.default', FileInfo::SPACE_UNLIMITED) + ->willReturn(FileInfo::SPACE_UNLIMITED); + + $this->mimeLoader->expects($this->any()) + ->method('getMimetypeById') + ->willReturn('application/octet-stream'); + + $folderId = $this->manager->createFolder('chunked'); + $this->manager->addApplicableGroup($folderId, 'target_group'); + + // Place 'target_group' in the second chunk to exercise the chunking code path. + $groups = array_map(fn (int $i): string => 'group_' . $i, range(1, 1000)); + $groups[] = 'target_group'; + + $user = $this->getUser($groups); + $result = $this->manager->getAllFoldersForUserWithSize($user); + + $this->assertCount(1, $result); + $this->assertArrayHasKey($folderId, $result); + } } diff --git a/tests/Trash/TrashManagerTest.php b/tests/Trash/TrashManagerTest.php new file mode 100644 index 000000000..f73c7c72c --- /dev/null +++ b/tests/Trash/TrashManagerTest.php @@ -0,0 +1,71 @@ +connection = Server::get(IDBConnection::class); + $this->trashManager = new TrashManager($this->connection); + $this->cleanTrash(); + } + + #[\Override] + protected function tearDown(): void { + $this->cleanTrash(); + parent::tearDown(); + } + + private function cleanTrash(): void { + $query = $this->connection->getQueryBuilder(); + $query->delete('group_folders_trash')->executeStatement(); + } + + public function testListTrashForFolders(): void { + $this->trashManager->addTrashItem(1, 'file1.txt', 1000, 'path/to/file1.txt', 101, 'user1'); + $this->trashManager->addTrashItem(2, 'file2.txt', 2000, 'path/to/file2.txt', 102, 'user2'); + $this->trashManager->addTrashItem(3, 'file3.txt', 3000, 'path/to/file3.txt', 103, 'user3'); + + $result = $this->trashManager->listTrashForFolders([1, 2]); + + $this->assertCount(2, $result); + $folderIds = array_column($result, 'folder_id'); + $this->assertContains(1, $folderIds); + $this->assertContains(2, $folderIds); + $this->assertNotContains(3, $folderIds); + } + + public function testListTrashForFoldersChunked(): void { + $this->trashManager->addTrashItem(1, 'file.txt', 1000, 'path/to/file.txt', 101, 'user1'); + + // Build a list of 1001 folder IDs; the folder with the trash item is placed + // in the second chunk to exercise the chunking code path. + $folderIds = range(2, 1001); + $folderIds[] = 1; + + $result = $this->trashManager->listTrashForFolders($folderIds); + + $this->assertCount(1, $result); + $this->assertEquals(1, $result[0]['folder_id']); + $this->assertEquals('file.txt', $result[0]['name']); + } +}