diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index a2634b0d3..c67d02015 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -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, 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, + ], $folders))); + } } diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 46853da25..8c0924ee1 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -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 + * @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 $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 $paths * @return list diff --git a/openapi.json b/openapi.json index 2510c8db4..d0d2c580f 100644 --- a/openapi.json +++ b/openapi.json @@ -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": [] diff --git a/src/settings/Api.ts b/src/settings/Api.ts index 6c300edec..d0d2f90be 100644 --- a/src/settings/Api.ts +++ b/src/settings/Api.ts @@ -29,6 +29,23 @@ export class Api { return Object.values(response.data.ocs.data) } + async listFoldersForCircle(circleId: string): Promise> { + const response = await axios.get>>(this.getUrl(`circles/${circleId}/folders`)) + return response.data.ocs.data + } + // Returns all NC groups async listGroups(): Promise { const response = await axios.get>(this.getUrl('delegation/groups')) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 75cfe740d..aaf6a39dc 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -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; export type components = { @@ -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; + }; + }; + }; + }; + }; + }; } diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index fdb1a8941..d8f8415eb 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -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')