Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/Db/PageLinkMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\Collectives\Db;

use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

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

/**
* @param int[] $pageIds
* @return array<int, int[]> Linked page ids indexed by page id
* @throws Exception
*/
public function findByPageIds(array $pageIds): array {
if (empty($pageIds)) {
return [];
}

$linkedPageIds = [];
foreach (array_chunk($pageIds, 1000) as $chunk) {
$qb = $this->db->getQueryBuilder();
$qb->select('page_id', 'linked_page_id')
->from(self::TABLE_NAME)
->where($qb->expr()->in('page_id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));

$result = $qb->executeQuery();
while ($row = $result->fetch()) {
$linkedPageIds[(int)$row['page_id']][] = (int)$row['linked_page_id'];
}
$result->closeCursor();
}

return $linkedPageIds;
}

/**
* @throws Exception
*/
Expand Down
32 changes: 17 additions & 15 deletions lib/Db/PageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,24 @@ public function findByFileIds(array $fileIds, bool $trashed = false): array {
return [];
}

$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->tableName)
->where($qb->expr()->in('file_id',
$qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));

if ($trashed) {
$qb->andWhere($qb->expr()->isNotNull('trash_timestamp'));
} else {
$qb->andWhere($qb->expr()->isNull('trash_timestamp'));
}

$pages = $this->findEntities($qb);
$pagesByFileId = [];
foreach ($pages as $page) {
$pagesByFileId[$page->getFileId()] = $page;
foreach (array_chunk($fileIds, 1000) as $chunk) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->tableName)
->where($qb->expr()->in('file_id',
$qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));

if ($trashed) {
$qb->andWhere($qb->expr()->isNotNull('trash_timestamp'));
} else {
$qb->andWhere($qb->expr()->isNull('trash_timestamp'));
}

$pages = $this->findEntities($qb);
foreach ($pages as $page) {
$pagesByFileId[$page->getFileId()] = $page;
}
}

return $pagesByFileId;
Expand Down
44 changes: 44 additions & 0 deletions lib/Model/CollectiveFileInfo.php
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 CollectiveFileInfo {
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;
}
}
97 changes: 87 additions & 10 deletions lib/Model/PageInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,44 @@ public function jsonSerialize(): array {
];
}

/**
* Build the page info from a lightweight filecache entry (see PageService::getPagesFromFolder).
*/
public function fromFileInfo(
Comment thread
max-nextcloud marked this conversation as resolved.
CollectiveFileInfo $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 {
$dirName = dirname($fileInfo->path);
$dirName = $dirName === '.' ? '' : $dirName;
$this->fromData(
$fileInfo->fileId,
$dirName,
$fileInfo->isIndexPage(),
$parentId,
$fileInfo->name,
$fileInfo->mtime,
$fileInfo->size,
$collectivePath,
$lastUserId,
$lastUserDisplayName,
$emoji,
$subpageOrder,
$fullWidth,
$slug,
$tags,
$linkedPageIds,
);
}

/**
* @throws InvalidPathException
* @throws NotFoundException
Expand All @@ -245,11 +283,51 @@ public function fromFile(
?string $tags = null,
?array $linkedPageIds = null,
): void {
$this->setId($file->getId());
// Set folder name as title for all index pages except the collective landing page
$dirName = dirname($file->getInternalPath());
$dirName = $dirName === '.' ? '' : $dirName;
if (strcmp($file->getName(), self::INDEX_PAGE_TITLE . self::SUFFIX) === 0) {
$isIndexPage = strcmp($file->getName(), self::INDEX_PAGE_TITLE . self::SUFFIX) === 0;
$mountPoint = explode('/', $file->getMountPoint()->getMountPoint(), 4);
$collectivePath = count($mountPoint) >= 4 ? rtrim($mountPoint[3], '/') : null;
$this->fromData(
$file->getId(),
$dirName,
$isIndexPage,
$parentId,
$file->getName(),
$file->getMTime(),
(int)$file->getSize(),
$collectivePath,
$lastUserId,
$lastUserDisplayName,
$emoji,
$subpageOrder,
$fullWidth,
$slug,
$tags,
$linkedPageIds,
);
}

private function fromData(
int $id,
string $dirName,
bool $isIndexPage,
int $parentId,
string $fileName,
int $timestamp,
int $size,
?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($id);
if ($isIndexPage) {
if ($parentId === 0) {
// Landing page
$this->setTitle(Server::get(IFactory::class)->get('collectives')->t('Landing page'));
Expand All @@ -258,15 +336,14 @@ public function fromFile(
$this->setTitle(basename($dirName));
}
} else {
$this->setTitle(basename($file->getName(), self::SUFFIX));
$this->setTitle(basename($fileName, self::SUFFIX));
}
$this->setFilePath($dirName);
$this->setTimestamp($file->getMTime());
$this->setSize((int)$file->getSize());
$this->setFileName($file->getName());
$mountPoint = explode('/', $file->getMountPoint()->getMountPoint(), 4);
if (count($mountPoint) >= 4) {
$this->setCollectivePath(rtrim($mountPoint[3], '/'));
$this->setTimestamp($timestamp);
$this->setSize($size);
$this->setFileName($fileName);
if ($collectivePath !== null) {
$this->setCollectivePath($collectivePath);
}
if ($lastUserId !== null) {
$this->setLastUserId($lastUserId);
Expand Down
52 changes: 52 additions & 0 deletions lib/Mount/CollectiveFolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\CollectiveFileInfo;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Folder;
Expand Down Expand Up @@ -250,6 +251,57 @@ public function getFolderFileCachePerCollectiveId(array $ids): array {
return $result;
}

/**
* Load all filecache entries (files and folders) of a collective folder in a single query.
*
* @return CollectiveFileInfo[] Indexed by file id, with paths relative to the collective root folder
*
* @throws NotFoundException
* @throws \OCP\DB\Exception
*/
public function getFileCacheForCollective(int $collectiveId, ?string $subdirectory = null): array {
$jailPath = $this->getJailPath($collectiveId);
$storageId = $this->getRootFolderStorageId();

$qb = $this->connection->getQueryBuilder();

// Trailing slash matters: it restricts to descendants and
// avoids matching siblings (e.g. `16` must not match `160`).
$likePath = $jailPath . '/' . ($subdirectory ? $subdirectory . '/' : '');
$likePath = $this->connection->escapeLikeParameter($likePath) . '%';

$qb->select('fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart',
Comment thread
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)))
Comment thread
max-nextcloud marked this conversation as resolved.
->andWhere($qb->expr()->like('path', $qb->createNamedParameter($likePath)));

$prefixLength = strlen($jailPath . '/');
$result = [];
$cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
$relativePath = substr($row['path'], $prefixLength);
$result[(int)$row['fileid']] = new CollectiveFileInfo(
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
Expand Down
Loading
Loading