Skip to content
Closed
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
38 changes: 38 additions & 0 deletions lib/Controller/FolderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -642,4 +642,42 @@
public function getFoldersCount(): DataResponse {
return new DataResponse(['count' => $this->manager->countAllFolders()]);
}

/**
* Gets all Groupfolders assigned to a circle with quota and size information
*
* @param string $circleId The circle single id to look up folders for.
* @return DataResponse<Http::STATUS_OK, list<array{id: int, mount_point: string, quota: int, size: int, is_team_folder: bool}>, array{}>
*
* 200: Groupfolders for circle returned
*/
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/circles/{circleId}/folders', requirements: ['circleId' => '.+'])]
public function getFoldersForCircle(string $circleId): DataResponse {
$storageId = $this->getRootFolderStorageId();
if ($storageId === null) {
throw new OCSNotFoundException();
}

$folders = [];
foreach ($this->manager->getFoldersWithSizeForCircle($circleId) as $folder) {
$folders[$folder->id] = $this->formatFolder($folder);
}

if ($this->delegationService->hasOnlyApiAccess()) {
$folders = $this->foldersFilter->getForApiUser($folders);

Check failure on line 668 in lib/Controller/FolderController.php

View workflow job for this annotation

GitHub Actions / static-phpstan-analysis

Parameter #1 $folders of method OCA\GroupFolders\Service\FoldersFilter::getForApiUser() expects array<string, array{id: int, mount_point: string, group_details: array<string, array{displayName: string, permissions: int, type: 'circle'|'group'}>, groups: array<string, int>, quota: int, size: int, acl: bool, acl_default_no_permission: bool, ...}>, array<int, array{id: int, mount_point: string, group_details: array<string, array{displayName: string, permissions: int, type: 'circle'|'group'}>, groups: array<string, int>, quota: int, size: int, acl: bool, acl_default_no_permission: bool, ...}> given.
}

if (!$this->delegationService->hasApiAccess()) {
$folders = array_values(array_filter(array_map($this->filterNonAdminFolder(...), $folders)));
}

return new DataResponse(array_values(array_map(static fn (array $folder): array => [
'id' => $folder['id'],
'mount_point' => $folder['mount_point'],
'quota' => $folder['quota'],
'size' => (int)$folder['size'],
'is_team_folder' => ($folder['team_circle_id'] ?? null) !== null,
], $folders)));
}
}
40 changes: 40 additions & 0 deletions lib/Folder/FolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,46 @@ public function getFoldersForCircle(string $circleId): array {
return array_map($this->rowToFolder(...), $rows);
}

/**
* Return all group folders directly assigned to or owned by a circle,
* including their current size from the file cache.
*
* @return list<FolderWithMappingsAndCache>
* @throws Exception
*/
public function getFoldersWithSizeForCircle(string $circleId): array {
if ($circleId === '') {
throw new \InvalidArgumentException('circleId cannot be empty');
}

$query = $this->selectWithFileCache();
$query->leftJoin('f', 'group_folders_groups', 'g', $query->expr()->eq('f.folder_id', 'g.folder_id'))
->where($query->expr()->orX(

@cristianscheid cristianscheid Sep 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this allow returning duplicate folder rows? For example, if a folder has more than one row in group_folders_groups (e.g. shared with multiple groups).

Maybe we could do something similar to what's done on getAllFoldersWithSize() and add a groupBy('f.folder_id')?

$query->expr()->eq('g.circle_id', $query->createNamedParameter($circleId)),
$query->expr()->eq('f.team_circle_id', $query->createNamedParameter($circleId)),
));

/** @var list<array{folder_id: int|string, mount_point: string, quota: int|string, acl: bool, acl_default_no_permission: bool, storage_id: int|string, root_id: int|string, options: string, team_circle_id: ?string}> $rows */
$rows = $query->executeQuery()->fetchAll();

$folderIds = array_map(static fn (array $row): int => (int)$row['folder_id'], $rows);
$applicableMap = $this->getAllApplicable($folderIds);
$folderMappings = $this->getAllFolderMappings($folderIds);

return array_map(function (array $row) use ($applicableMap, $folderMappings): FolderWithMappingsAndCache {
$folder = $this->rowToFolder($row);
$id = $folder->id;
return FolderWithMappingsAndCache::fromFolderWithMapping(
FolderDefinitionWithMappings::fromFolder(
$folder,
$applicableMap[$id] ?? [],
$this->getManageAcl($folderMappings[$id] ?? []),
),
Cache::cacheEntryFromData($row, $this->mimeTypeLoader),
);
}, $rows);
}

/**
* @param list<string> $paths
* @return list<FolderDefinitionWithPermissions>
Expand Down
129 changes: 129 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,135 @@
}
}
}
},
"/index.php/apps/groupfolders/circles/{circleId}/folders": {
"get": {
"operationId": "folder-get-folders-for-circle",
"summary": "Gets all Groupfolders assigned to a circle with quota and size information",
"tags": [
"folder"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "circleId",
"in": "path",
"description": "The circle single id to look up folders for.",
"required": true,
"schema": {
"type": "string",
"pattern": "^.+$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Groupfolders for circle returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"mount_point",
"quota",
"size",
"is_team_folder"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"mount_point": {
"type": "string"
},
"quota": {
"type": "integer",
"format": "int64"
},
"size": {
"type": "integer",
"format": "int64"
},
"is_team_folder": {
"type": "boolean"
}
}
}
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
}
},
"tags": []
Expand Down
17 changes: 17 additions & 0 deletions src/settings/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ export class Api {
return Object.values(response.data.ocs.data)
}

async listFoldersForCircle(circleId: string): Promise<Array<{
id: number
mount_point: string
quota: number
size: number
is_team_folder: boolean
}>> {
const response = await axios.get<OCSResponse<Array<{
id: number
mount_point: string
quota: number
size: number
is_team_folder: boolean
}>>>(this.getUrl(`circles/${circleId}/folders`))
return response.data.ocs.data
}

// Returns all NC groups
async listGroups(): Promise<DelegationGroup[]> {
const response = await axios.get<OCSResponse<DelegationGroup[]>>(this.getUrl('delegation/groups'))
Expand Down
71 changes: 71 additions & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,23 @@ export type paths = {
patch?: never;
trace?: never;
};
"/index.php/apps/groupfolders/circles/{circleId}/folders": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Gets all Groupfolders assigned to a circle with quota and size information */
get: operations["folder-get-folders-for-circle"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
};
export type webhooks = Record<string, never>;
export type components = {
Expand Down Expand Up @@ -1479,4 +1496,58 @@ export interface operations {
};
};
};
"folder-get-folders-for-circle": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
/** @description The circle single id to look up folders for. */
circleId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Groupfolders for circle returned */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** Format: int64 */
id: number;
mount_point: string;
/** Format: int64 */
quota: number;
/** Format: int64 */
size: number;
is_team_folder: boolean;
}[];
};
};
};
};
/** @description Current user is not logged in */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: unknown;
};
};
};
};
};
};
}
31 changes: 31 additions & 0 deletions tests/Folder/FolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,37 @@ public function testIsExclusivelyAssignedToCircleRejectsAdditionalMappings(): vo
$this->assertFalse($this->manager->isExclusivelyAssignedToCircle($folderId, 'circle-owner'));
}

public function testGetFoldersWithSizeForCircleIncludesAssignedAndOwnedFolders(): void {
$assignedFolderId = $this->manager->createFolder('circle-assigned-folder');
$ownedFolderId = $this->manager->createFolder('circle-owned-folder');
$otherFolderId = $this->manager->createFolder('other-circle-folder');
$this->manager->setTeamCircleId($ownedFolderId, 'circle-owner');

foreach ([
[$assignedFolderId, 'circle-owner'],
[$otherFolderId, 'other-circle'],
] as [$folderId, $circleId]) {
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->insert('group_folders_groups')
->values([
'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT),
'group_id' => $query->createNamedParameter(''),
'circle_id' => $query->createNamedParameter($circleId),
'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL),
]);
$query->executeStatement();
}

$folders = $this->manager->getFoldersWithSizeForCircle('circle-owner');
$folderIds = array_map(static fn (FolderDefinition $folder): int => $folder->id, $folders);
sort($folderIds);
$expectedFolderIds = [$assignedFolderId, $ownedFolderId];
sort($expectedFolderIds);

$this->assertSame($expectedFolderIds, $folderIds);
$this->assertNotContains($otherFolderId, $folderIds);
}

public function testTeamCircleIdIsHydratedAsNullableString(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
Expand Down
Loading