Skip to content
Open
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 aclMappingSearch(int $id, string $search = ''): DataResponse {
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_space: 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_' . $folder->id] = $this->formatFolder($folder);
}

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

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_space' => ($folder['team_circle_id'] ?? null) !== null,

@cristianscheid cristianscheid Sep 4, 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.

regarding 'is_team_space', are we not moving away from the "team space" term? as per this comment by Jos

], $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(
$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_space"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"mount_point": {
"type": "string"
},
"quota": {
"type": "integer",
"format": "int64"
},
"size": {
"type": "integer",
"format": "int64"
},
"is_team_space": {
"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_space: boolean
}>> {
const response = await axios.get<OCSResponse<Array<{
id: number
mount_point: string
quota: number
size: number
is_team_space: 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_space: boolean;
}[];
};
};
};
};
/** @description Current user is not logged in */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: unknown;
};
};
};
};
};
};
}
35 changes: 35 additions & 0 deletions tests/Folder/FolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,41 @@ public function testIsExclusivelyAssignedToCircleRejectsAdditionalMappings(): vo
$this->assertFalse($this->manager->isExclusivelyAssignedToCircle($folderId, 'circle-owner'));
}

public function testGetFoldersWithSizeForCircleIncludesAssignedAndOwnedFolders(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
->with('groupfolders.quota.default', FileInfo::SPACE_UNLIMITED)
->willReturn(FileInfo::SPACE_UNLIMITED);

$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