-
Notifications
You must be signed in to change notification settings - Fork 43
Perf: Optimize pages loading (Filecache path like approach) #2549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Collectives\Model; | ||
|
|
||
| /** | ||
| * Lightweight representation of a file entry from the `filecache` table. | ||
| * | ||
| * `path` is relative to the collective root folder (e.g. `Readme.md` or | ||
| * `subfolder/page.md`), so it matches the semantics of `File::getInternalPath()` | ||
| * within the jailed collective storage. | ||
| */ | ||
| class FileInfo { | ||
| public function __construct( | ||
| public readonly int $fileId, | ||
| public readonly int $storage, | ||
| public readonly string $path, | ||
| public readonly int $parent, | ||
| public readonly string $name, | ||
| public readonly int $mimetype, | ||
| public readonly int $mimepart, | ||
| public readonly int $size, | ||
| public readonly int $mtime, | ||
| public readonly int $storageMtime, | ||
| public readonly int $encrypted, | ||
| public readonly string $etag, | ||
| public readonly int $permissions, | ||
| ) { | ||
| } | ||
|
|
||
| public function isPage(): bool { | ||
| return str_ends_with($this->name, PageInfo::SUFFIX); | ||
| } | ||
|
|
||
| public function isIndexPage(): bool { | ||
| return $this->name === PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,6 +229,70 @@ public function jsonSerialize(): array { | |
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Build the page info from a lightweight filecache entry (see PageService::getPagesFromFolderV2). | ||
| */ | ||
| public function fromFileInfo( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This copies much of the logic of |
||
| FileInfo $fileInfo, | ||
| int $parentId, | ||
| ?string $collectivePath = null, | ||
| ?string $lastUserId = null, | ||
| ?string $lastUserDisplayName = null, | ||
| ?string $emoji = null, | ||
| ?string $subpageOrder = null, | ||
| ?bool $fullWidth = false, | ||
| ?string $slug = null, | ||
| ?string $tags = null, | ||
| ?array $linkedPageIds = null, | ||
| ): void { | ||
| $this->setId($fileInfo->fileId); | ||
| $dirName = dirname($fileInfo->path); | ||
| $dirName = $dirName === '.' ? '' : $dirName; | ||
| if ($fileInfo->isIndexPage()) { | ||
| if ($parentId === 0) { | ||
| // Landing page | ||
| $this->setTitle(Server::get(IFactory::class)->get('collectives')->t('Landing page')); | ||
| } else { | ||
| // Index page | ||
| $this->setTitle(basename($dirName)); | ||
| } | ||
| } else { | ||
| $this->setTitle(basename($fileInfo->name, self::SUFFIX)); | ||
| } | ||
| $this->setFilePath($dirName); | ||
| $this->setTimestamp($fileInfo->mtime); | ||
| $this->setSize($fileInfo->size); | ||
| $this->setFileName($fileInfo->name); | ||
| if ($collectivePath !== null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess these checks can be omitted as the default is |
||
| $this->setCollectivePath($collectivePath); | ||
| } | ||
| if ($lastUserId !== null) { | ||
| $this->setLastUserId($lastUserId); | ||
| } | ||
| if ($lastUserDisplayName !== null) { | ||
| $this->setLastUserDisplayName($lastUserDisplayName); | ||
| } | ||
| if ($emoji !== null) { | ||
| $this->setEmoji($emoji); | ||
| } | ||
| if ($fullWidth !== null) { | ||
| $this->setFullWidth($fullWidth); | ||
| } | ||
| if ($subpageOrder !== null) { | ||
| $this->setSubpageOrder($subpageOrder); | ||
| } | ||
| if ($slug !== null) { | ||
| $this->setSlug($slug); | ||
| } | ||
| if ($tags !== null) { | ||
| $this->setTags($tags); | ||
| } | ||
| if ($linkedPageIds !== null) { | ||
| $this->setLinkedPageIds($linkedPageIds); | ||
| } | ||
| $this->setParentId($parentId); | ||
| } | ||
|
|
||
| /** | ||
| * @throws InvalidPathException | ||
| * @throws NotFoundException | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| use OC\Files\Storage\Wrapper\Jail; | ||
| use OC\Files\Storage\Wrapper\PermissionsMask; | ||
| use OCA\Collectives\ACL\ACLStorageWrapper; | ||
| use OCA\Collectives\Model\FileInfo; | ||
| use OCP\DB\QueryBuilder\IQueryBuilder; | ||
| use OCP\Files\Cache\ICacheEntry; | ||
| use OCP\Files\Folder; | ||
|
|
@@ -250,6 +251,53 @@ public function getFolderFileCachePerCollectiveId(array $ids): array { | |
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * Load all filecache entries (files and folders) of a collective folder in a single query. | ||
| * | ||
| * @return FileInfo[] Indexed by file id, with paths relative to the collective root folder | ||
| * | ||
| * @throws NotFoundException | ||
| * @throws \OCP\DB\Exception | ||
| */ | ||
| public function getFileCacheForCollective(int $collectiveId): array { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be an option to pass the folder name here as well and further narrow down the query to only contain files in this subfolder? This would make the query less heavy when
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be particularly helpful for building the templateFolder page tree, as that one doesn't need to process all the files from the collective rood folder. |
||
| $jailPath = $this->getJailPath($collectiveId); | ||
| $storageId = $this->getRootFolderStorageId(); | ||
|
|
||
| $qb = $this->connection->getQueryBuilder(); | ||
| $qb->select('fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart', | ||
|
mejo- marked this conversation as resolved.
|
||
| 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions') | ||
| ->from('filecache') | ||
| ->where($qb->expr()->eq('storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to not further filter on
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My goal was to introduce a new index that will cover columns used in filter. So, in this case we should extend it with mime type then |
||
| // Trailing slash matters: it restricts to descendants of this collective and | ||
| // avoids matching sibling collectives (e.g. `16` must not match `160`). | ||
| ->andWhere($qb->expr()->like('path', $qb->createNamedParameter($this->connection->escapeLikeParameter($jailPath . '/') . '%'))); | ||
|
|
||
| $prefixLength = strlen($jailPath . '/'); | ||
| $result = []; | ||
| $cursor = $qb->executeQuery(); | ||
| while ($row = $cursor->fetch()) { | ||
| $relativePath = substr($row['path'], $prefixLength); | ||
| $result[(int)$row['fileid']] = new FileInfo( | ||
| fileId: (int)$row['fileid'], | ||
| storage: (int)$row['storage'], | ||
| path: $relativePath, | ||
| parent: (int)$row['parent'], | ||
| name: (string)$row['name'], | ||
| mimetype: (int)$row['mimetype'], | ||
| mimepart: (int)$row['mimepart'], | ||
| size: (int)$row['size'], | ||
| mtime: (int)$row['mtime'], | ||
| storageMtime: (int)$row['storage_mtime'], | ||
| encrypted: (int)$row['encrypted'], | ||
| etag: (string)$row['etag'], | ||
| permissions: (int)$row['permissions'], | ||
| ); | ||
| } | ||
| $cursor->closeCursor(); | ||
|
|
||
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * @throws InvalidPathException | ||
| * @throws NotFoundException | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe better to call this
CollectiveFileInfoto not confuse it with the publicFileInfoAPI from server.