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 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,
], $folders)));
}
}
68 changes: 68 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 Expand Up @@ -1288,6 +1328,10 @@ public function setTeamCircleId(int $folderId, string $circleId): void {
if ($existingFolderId !== null && $existingFolderId !== $folderId) {
throw new Exception('This team already has a team space');
}
$existingCircleId = $this->getTeamCircleId($folderId);
if ($existingCircleId !== null && $existingCircleId !== $circleId) {
throw new Exception('This folder already belongs to another team');
}

$query = $this->connection->getQueryBuilder();
$query->update('group_folders')
Expand All @@ -1296,6 +1340,30 @@ public function setTeamCircleId(int $folderId, string $circleId): void {
$query->executeStatement();
}

/**
* Whether the folder is assigned exclusively to the given circle. A folder
* must have precisely one applicable mapping, and that mapping must be the
* target team; group mappings and additional teams make it ineligible.
*
* @throws Exception
*/
public function isExclusivelyAssignedToCircle(int $folderId, string $circleId): bool {
if ($circleId === '') {
return false;
}

$query = $this->connection->getQueryBuilder();
$query->select('group_id', 'circle_id')
->from('group_folders_groups')
->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
/** @var list<array{group_id: ?string, circle_id: ?string}> $rows */
$rows = $query->executeQuery()->fetchAll();

return count($rows) === 1
&& ($rows[0]['group_id'] ?? '') === ''
&& ($rows[0]['circle_id'] ?? '') === $circleId;
}

/**
* Clear the team ownership of a team space by resetting the
* `team_circle_id` column to null.
Expand Down
22 changes: 22 additions & 0 deletions lib/TeamSpace/TeamSpaceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ public function createTeamFolder(Team $team, int $quota = 0): TeamFolder {
return $folder;
}

/**
* @return list<TeamFolder>
*/
public function getLinkableTeamFolders(string $teamId): array {
return $this->service->getLinkableGroupFoldersForCircle($teamId);
}

public function linkTeamFolder(string $teamId, int $folderId): TeamFolder {
$this->service->linkExistingTeamSpace($teamId, $folderId);

$folder = $this->getTeamFolder($teamId);
if ($folder === null) {
throw new \RuntimeException('Linked team space could not be found');
}

return $folder;
}

public function updateTeamFolderQuota(string $teamId, int $quota): TeamFolder {
return $this->service->updateTeamSpaceQuota($teamId, $quota);
}

#[\Override]
public function unlinkTeamFolder(string $teamId): ?TeamFolder {
$folder = $this->getTeamFolder($teamId);
Expand Down
68 changes: 66 additions & 2 deletions lib/TeamSpace/TeamSpaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,33 @@ public function getTeamSpaceForCircle(string $circleId): ?TeamFolder {
if ($folder === null) {
return null;
}
return new TeamFolder($folder->id, $folder->mountPoint, $folder->quota);
}

/**
* Update the storage quota of the team space belonging to the given team.
*
* @param string $circleId The circle single id.
* @param int $quota Quota in bytes; zero means unlimited.
* @return TeamFolder The updated folder.
* @throws \RuntimeException if no team space is linked to the team.
*/
public function updateTeamSpaceQuota(string $circleId, int $quota): TeamFolder {
$folderId = $this->folderManager->getFolderIdByTeamCircleId($circleId);
if ($folderId === null) {
throw new \RuntimeException('No team space linked to this team');
}

$this->folderManager->setFolderQuota($folderId, $quota);

$folder = $this->folderManager->getFolder($folderId);
if ($folder === null) {
throw new \RuntimeException('Team space could not be found after updating quota');
}

$this->createAppDirectory($folderId);

return new TeamFolder($folder->id, $folder->mountPoint);
return new TeamFolder($folder->id, $folder->mountPoint, $folder->quota);
}

/**
Expand All @@ -246,11 +269,52 @@ public function getTeamSpaceForCircle(string $circleId): ?TeamFolder {
*/
public function getGroupFoldersForCircle(string $circleId): array {
return array_map(
static fn (FolderDefinition $folder): TeamFolder => new TeamFolder($folder->id, $folder->mountPoint),
static fn (FolderDefinition $folder): TeamFolder => new TeamFolder($folder->id, $folder->mountPoint, $folder->quota),
$this->folderManager->getFoldersForCircle($circleId),
);
}

/**
* Return existing folders directly available to a team that have not been
* made an exclusive team folder for another team.
*
* @return list<TeamFolder>
*/
public function getLinkableGroupFoldersForCircle(string $circleId): array {
return array_values(array_map(
static fn (FolderDefinition $folder): TeamFolder => new TeamFolder($folder->id, $folder->mountPoint, $folder->quota),
array_filter(
$this->folderManager->getFoldersForCircle($circleId),
fn (FolderDefinition $folder): bool => !$folder->isTeamSpace()
&& $this->folderManager->isExclusivelyAssignedToCircle($folder->id, $circleId),
),
));
}

/**
* Mark an existing, directly available folder as the team's exclusive
* team folder. The ownership and eligibility checks are repeated at the
* mutation boundary to preserve the one-to-one relationship.
*
* @throws \InvalidArgumentException When the folder is not eligible.
*/
public function linkExistingTeamSpace(string $circleId, int $folderId): void {
if ($this->folderManager->getFolderIdByTeamCircleId($circleId) !== null) {
return;
}

foreach ($this->folderManager->getFoldersForCircle($circleId) as $folder) {
if ($folder->id === $folderId
&& !$folder->isTeamSpace()
&& $this->folderManager->isExclusivelyAssignedToCircle($folderId, $circleId)) {
$this->folderManager->setTeamCircleId($folderId, $circleId);
return;
}
}

throw new \InvalidArgumentException('The folder is not available for this team');
}

/**
* Whether the given circle (looked up by single id) owns a team space.
*/
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
Loading
Loading