package nextcloud import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) const groupFolderAllPermissions = 31 type GroupFolder struct { ID int `json:"id"` MountPoint string `json:"mount_point"` Groups map[string]int `json:"groups"` Quota int64 `json:"quota"` Size int64 `json:"size"` ACL bool `json:"acl"` Manage []string `json:"manage"` } // GroupFolderWebDAVPath builds the WebDAV URL for a group folder root or subpath. func GroupFolderWebDAVPath(folderID int, logicalPath string) string { base := fmt.Sprintf("/remote.php/dav/groupfolders/%d", folderID) logical := strings.Trim(logicalPath, "/") if logical == "" { return base } parts := strings.Split(logical, "/") for i, p := range parts { parts[i] = url.PathEscape(p) } return base + "/" + strings.Join(parts, "/") } func (c *Client) ListGroupFolders(ctx context.Context, userID string) ([]GroupFolder, error) { resp, err := c.DoAsUser(ctx, "GET", "/index.php/apps/groupfolders/folders?format=json", nil, userID, ocsJSONHeaders()) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, &HTTPStatusError{Operation: "list groupfolders", StatusCode: resp.StatusCode} } return decodeGroupFoldersResponse(resp.Body) } func (c *Client) ListGroupFoldersAdmin(ctx context.Context) ([]GroupFolder, error) { resp, err := c.doRequest(ctx, "GET", "/index.php/apps/groupfolders/folders?format=json", nil, ocsJSONHeaders()) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, &HTTPStatusError{Operation: "list groupfolders admin", StatusCode: resp.StatusCode} } return decodeGroupFoldersResponse(resp.Body) } func decodeGroupFoldersResponse(body io.Reader) ([]GroupFolder, error) { var payload struct { OCS struct { Data json.RawMessage `json:"data"` } `json:"ocs"` } if err := json.NewDecoder(body).Decode(&payload); err != nil { return nil, err } raw := payload.OCS.Data if len(raw) == 0 || string(raw) == "[]" || string(raw) == "null" { return nil, nil } var asMap map[string]GroupFolder if err := json.Unmarshal(raw, &asMap); err == nil && len(asMap) > 0 { out := make([]GroupFolder, 0, len(asMap)) for _, item := range asMap { out = append(out, item) } return out, nil } var asList []GroupFolder if err := json.Unmarshal(raw, &asList); err != nil { return nil, err } return asList, nil } func (c *Client) CreateGroupFolder(ctx context.Context, mountPoint string) (int, error) { form := url.Values{} form.Set("mountpoint", mountPoint) resp, err := c.doRequest(ctx, "POST", "/index.php/apps/groupfolders/folders?format=json", strings.NewReader(form.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return 0, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return 0, &HTTPStatusError{Operation: "create groupfolder", StatusCode: resp.StatusCode} } var payload struct { OCS struct { Data struct { ID int `json:"id"` } `json:"data"` } `json:"ocs"` } if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return 0, err } if payload.OCS.Data.ID <= 0 { return 0, fmt.Errorf("groupfolder create returned empty id") } return payload.OCS.Data.ID, nil } func (c *Client) DeleteGroupFolder(ctx context.Context, folderID int) error { path := fmt.Sprintf("/index.php/apps/groupfolders/folders/%d?format=json", folderID) resp, err := c.doRequest(ctx, "DELETE", path, nil, ocsJSONHeaders()) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "delete groupfolder", StatusCode: resp.StatusCode} } return nil } func (c *Client) AssignGroupToFolder(ctx context.Context, folderID int, groupID string, permissions int) error { if permissions <= 0 { permissions = groupFolderAllPermissions } form := url.Values{} form.Set("group", groupID) path := fmt.Sprintf("/index.php/apps/groupfolders/folders/%d/groups?format=json", folderID) resp, err := c.doRequest(ctx, "POST", path, strings.NewReader(form.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "assign group to folder", StatusCode: resp.StatusCode} } formPerm := url.Values{} formPerm.Set("permissions", strconv.Itoa(permissions)) pathPerm := fmt.Sprintf("/index.php/apps/groupfolders/folders/%d/groups/%s?format=json", folderID, url.PathEscape(groupID)) resp2, err := c.doRequest(ctx, "POST", pathPerm, strings.NewReader(formPerm.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return err } defer resp2.Body.Close() if resp2.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "set groupfolder permissions", StatusCode: resp2.StatusCode} } return nil } func (c *Client) SetGroupFolderQuota(ctx context.Context, folderID int, quotaBytes int64) error { form := url.Values{} form.Set("quota", strconv.FormatInt(quotaBytes, 10)) path := fmt.Sprintf("/index.php/apps/groupfolders/folders/%d/quota?format=json", folderID) resp, err := c.doRequest(ctx, "POST", path, strings.NewReader(form.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "set groupfolder quota", StatusCode: resp.StatusCode} } return nil } func (c *Client) RenameGroupFolder(ctx context.Context, folderID int, mountPoint string) error { form := url.Values{} form.Set("mountpoint", mountPoint) path := fmt.Sprintf("/index.php/apps/groupfolders/folders/%d/mountpoint?format=json", folderID) resp, err := c.doRequest(ctx, "POST", path, strings.NewReader(form.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "rename groupfolder", StatusCode: resp.StatusCode} } return nil } func (c *Client) EnsureGroup(ctx context.Context, groupID string) error { groupID = strings.TrimSpace(groupID) if groupID == "" { return fmt.Errorf("group id is empty") } exists, err := c.groupExists(ctx, groupID) if err != nil { return err } if exists { return nil } form := url.Values{} form.Set("groupid", groupID) resp, err := c.doRequest(ctx, "POST", "/ocs/v1.php/cloud/groups", strings.NewReader(form.Encode()), map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return &HTTPStatusError{Operation: "create group", StatusCode: resp.StatusCode} } return nil } func (c *Client) groupExists(ctx context.Context, groupID string) (bool, error) { path := "/ocs/v1.php/cloud/groups/" + url.PathEscape(groupID) resp, err := c.doRequest(ctx, "GET", path, nil, ocsJSONHeaders()) if err != nil { return false, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: return true, nil case http.StatusNotFound: return false, nil default: return false, &HTTPStatusError{Operation: "group exists", StatusCode: resp.StatusCode} } } func OrgGroupID(orgSlug string) string { slug := strings.TrimSpace(strings.ToLower(orgSlug)) slug = strings.ReplaceAll(slug, " ", "-") return "org-" + slug }