Skip to content

Commit e644e8e

Browse files
committed
Perf: Optimize pages loading
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 98bd695 commit e644e8e

5 files changed

Lines changed: 409 additions & 1 deletion

File tree

lib/Db/PageLinkMapper.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OCA\Collectives\Db;
1111

1212
use OCP\DB\Exception;
13+
use OCP\DB\QueryBuilder\IQueryBuilder;
1314
use OCP\IDBConnection;
1415

1516
class PageLinkMapper {
@@ -31,6 +32,31 @@ public function findByPageId(int $pageId): array {
3132
return $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
3233
}
3334

35+
/**
36+
* @param int[] $pageIds
37+
* @return array<int, int[]> Linked page ids indexed by page id
38+
* @throws Exception
39+
*/
40+
public function findByPageIds(array $pageIds): array {
41+
if (empty($pageIds)) {
42+
return [];
43+
}
44+
45+
$qb = $this->db->getQueryBuilder();
46+
$qb->select('page_id', 'linked_page_id')
47+
->from(self::TABLE_NAME)
48+
->where($qb->expr()->in('page_id', $qb->createNamedParameter($pageIds, IQueryBuilder::PARAM_INT_ARRAY)));
49+
50+
$linkedPageIds = [];
51+
$result = $qb->executeQuery();
52+
while ($row = $result->fetch()) {
53+
$linkedPageIds[(int)$row['page_id']][] = (int)$row['linked_page_id'];
54+
}
55+
$result->closeCursor();
56+
57+
return $linkedPageIds;
58+
}
59+
3460
/**
3561
* @throws Exception
3662
*/

lib/Model/FileInfo.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Collectives\Model;
11+
12+
/**
13+
* Lightweight representation of a file entry from the `filecache` table.
14+
*
15+
* `path` is relative to the collective root folder (e.g. `Readme.md` or
16+
* `subfolder/page.md`), so it matches the semantics of `File::getInternalPath()`
17+
* within the jailed collective storage.
18+
*/
19+
class FileInfo {
20+
public function __construct(
21+
public readonly int $fileId,
22+
public readonly int $storage,
23+
public readonly string $path,
24+
public readonly int $parent,
25+
public readonly string $name,
26+
public readonly int $mimetype,
27+
public readonly int $mimepart,
28+
public readonly int $size,
29+
public readonly int $mtime,
30+
public readonly int $storageMtime,
31+
public readonly int $encrypted,
32+
public readonly string $etag,
33+
public readonly int $permissions,
34+
) {
35+
}
36+
37+
public function isPage(): bool {
38+
return str_ends_with($this->name, PageInfo::SUFFIX);
39+
}
40+
41+
public function isIndexPage(): bool {
42+
return $this->name === PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX;
43+
}
44+
}

lib/Model/PageInfo.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,70 @@ public function jsonSerialize(): array {
229229
];
230230
}
231231

232+
/**
233+
* Build the page info from a lightweight filecache entry (see PageService::getPagesFromFolderV2).
234+
*/
235+
public function fromFileInfo(
236+
FileInfo $fileInfo,
237+
int $parentId,
238+
?string $collectivePath = null,
239+
?string $lastUserId = null,
240+
?string $lastUserDisplayName = null,
241+
?string $emoji = null,
242+
?string $subpageOrder = null,
243+
?bool $fullWidth = false,
244+
?string $slug = null,
245+
?string $tags = null,
246+
?array $linkedPageIds = null,
247+
): void {
248+
$this->setId($fileInfo->fileId);
249+
$dirName = dirname($fileInfo->path);
250+
$dirName = $dirName === '.' ? '' : $dirName;
251+
if ($fileInfo->isIndexPage()) {
252+
if ($parentId === 0) {
253+
// Landing page
254+
$this->setTitle(Server::get(IFactory::class)->get('collectives')->t('Landing page'));
255+
} else {
256+
// Index page
257+
$this->setTitle(basename($dirName));
258+
}
259+
} else {
260+
$this->setTitle(basename($fileInfo->name, self::SUFFIX));
261+
}
262+
$this->setFilePath($dirName);
263+
$this->setTimestamp($fileInfo->mtime);
264+
$this->setSize($fileInfo->size);
265+
$this->setFileName($fileInfo->name);
266+
if ($collectivePath !== null) {
267+
$this->setCollectivePath($collectivePath);
268+
}
269+
if ($lastUserId !== null) {
270+
$this->setLastUserId($lastUserId);
271+
}
272+
if ($lastUserDisplayName !== null) {
273+
$this->setLastUserDisplayName($lastUserDisplayName);
274+
}
275+
if ($emoji !== null) {
276+
$this->setEmoji($emoji);
277+
}
278+
if ($fullWidth !== null) {
279+
$this->setFullWidth($fullWidth);
280+
}
281+
if ($subpageOrder !== null) {
282+
$this->setSubpageOrder($subpageOrder);
283+
}
284+
if ($slug !== null) {
285+
$this->setSlug($slug);
286+
}
287+
if ($tags !== null) {
288+
$this->setTags($tags);
289+
}
290+
if ($linkedPageIds !== null) {
291+
$this->setLinkedPageIds($linkedPageIds);
292+
}
293+
$this->setParentId($parentId);
294+
}
295+
232296
/**
233297
* @throws InvalidPathException
234298
* @throws NotFoundException

lib/Mount/CollectiveFolderManager.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OC\Files\Storage\Wrapper\Jail;
1616
use OC\Files\Storage\Wrapper\PermissionsMask;
1717
use OCA\Collectives\ACL\ACLStorageWrapper;
18+
use OCA\Collectives\Model\FileInfo;
1819
use OCP\DB\QueryBuilder\IQueryBuilder;
1920
use OCP\Files\Cache\ICacheEntry;
2021
use OCP\Files\Folder;
@@ -250,6 +251,53 @@ public function getFolderFileCachePerCollectiveId(array $ids): array {
250251
return $result;
251252
}
252253

254+
/**
255+
* Load all filecache entries (files and folders) of a collective folder in a single query.
256+
*
257+
* @return FileInfo[] Indexed by file id, with paths relative to the collective root folder
258+
*
259+
* @throws NotFoundException
260+
* @throws \OCP\DB\Exception
261+
*/
262+
public function getFileCacheForCollective(int $collectiveId): array {
263+
$jailPath = $this->getJailPath($collectiveId);
264+
$storageId = $this->getRootFolderStorageId();
265+
266+
$qb = $this->connection->getQueryBuilder();
267+
$qb->select('fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart',
268+
'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions')
269+
->from('filecache')
270+
->where($qb->expr()->eq('storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
271+
// Trailing slash matters: it restricts to descendants of this collective and
272+
// avoids matching sibling collectives (e.g. `16` must not match `160`).
273+
->andWhere($qb->expr()->like('path', $qb->createNamedParameter($this->connection->escapeLikeParameter($jailPath . '/') . '%')));
274+
275+
$prefixLength = strlen($jailPath . '/');
276+
$result = [];
277+
$cursor = $qb->executeQuery();
278+
while ($row = $cursor->fetch()) {
279+
$relativePath = substr($row['path'], $prefixLength);
280+
$result[(int)$row['fileid']] = new FileInfo(
281+
fileId: (int)$row['fileid'],
282+
storage: (int)$row['storage'],
283+
path: $relativePath,
284+
parent: (int)$row['parent'],
285+
name: (string)$row['name'],
286+
mimetype: (int)$row['mimetype'],
287+
mimepart: (int)$row['mimepart'],
288+
size: (int)$row['size'],
289+
mtime: (int)$row['mtime'],
290+
storageMtime: (int)$row['storage_mtime'],
291+
encrypted: (int)$row['encrypted'],
292+
etag: (string)$row['etag'],
293+
permissions: (int)$row['permissions'],
294+
);
295+
}
296+
$cursor->closeCursor();
297+
298+
return $result;
299+
}
300+
253301
/**
254302
* @throws InvalidPathException
255303
* @throws NotFoundException

0 commit comments

Comments
 (0)