ultisuite-backend/internal/nextcloud/external_storage.go
R3D347HR4Y 1d063237b9
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(transcription): integrate Faster Whisper for Jitsi transcriptions
- Added support for Faster Whisper transcription via Jigasi and Skynet.
- Updated .env.example to include new environment variables for transcription settings.
- Enhanced Jitsi Docker Compose configuration to include Skynet and Jigasi services.
- Introduced new API endpoints for managing organizational folders in the drive service.
- Updated Nextcloud initialization script to enable external file mounting.
- Improved error handling and response structures in the drive API.
- Added new properties for organization settings related to transcription and agenda management.
2026-06-12 19:10:18 +02:00

279 lines
8.4 KiB
Go

package nextcloud
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
type ExternalMount struct {
ID int `json:"id"`
MountPoint string `json:"mount_point"`
Backend string `json:"backend"`
Status int `json:"status"`
}
type WebDAVMountConfig struct {
Host string `json:"host"`
Root string `json:"root"`
User string `json:"user"`
Password string `json:"password"`
Secure bool `json:"secure"`
}
// CreateUserWebDAVMount registers a WebDAV external storage mount for a user.
func (c *Client) CreateUserWebDAVMount(ctx context.Context, userID, mountPoint string, cfg WebDAVMountConfig) (int, error) {
return c.createExternalMount(ctx, mountPoint, "dav", "password::password", userID, map[string]string{
"host": cfg.Host,
"root": cfg.Root,
"user": cfg.User,
"password": cfg.Password,
"secure": boolString(cfg.Secure),
})
}
// CreateGlobalWebDAVMount registers an org-wide WebDAV mount (all users).
func (c *Client) CreateGlobalWebDAVMount(ctx context.Context, mountPoint string, cfg WebDAVMountConfig) (int, error) {
return c.createExternalMount(ctx, mountPoint, "dav", "password::password", "", map[string]string{
"host": cfg.Host,
"root": cfg.Root,
"user": cfg.User,
"password": cfg.Password,
"secure": boolString(cfg.Secure),
})
}
func boolString(v bool) string {
if v {
return "true"
}
return "false"
}
func (c *Client) createExternalMount(ctx context.Context, mountPoint, backend, authBackend, userID string, config map[string]string) (int, error) {
form := url.Values{}
form.Set("mountPoint", mountPoint)
form.Set("backend", backend)
form.Set("authBackend", authBackend)
if userID != "" {
form.Set("user", userID)
}
for k, v := range config {
form.Set("config["+k+"]", v)
}
apiPath := "/index.php/apps/files_external/api/v1/mounts?format=json"
resp, err := c.doRequest(ctx, "POST", apiPath, 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 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return 0, fmt.Errorf("create external mount: %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
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("external mount create returned empty id")
}
return payload.OCS.Data.ID, nil
}
func (c *Client) DeleteExternalMount(ctx context.Context, mountID int) error {
apiPath := fmt.Sprintf("/index.php/apps/files_external/api/v1/mounts/%d?format=json", mountID)
resp, err := c.doRequest(ctx, "DELETE", apiPath, nil, ocsJSONHeaders())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &HTTPStatusError{Operation: "delete external mount", StatusCode: resp.StatusCode}
}
return nil
}
func (c *Client) ListUserExternalMounts(ctx context.Context, userID string) ([]ExternalMount, error) {
apiPath := "/index.php/apps/files_external/api/v1/mounts?format=json"
resp, err := c.DoAsUser(ctx, "GET", apiPath, nil, userID, ocsJSONHeaders())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &HTTPStatusError{Operation: "list external mounts", StatusCode: resp.StatusCode}
}
return decodeExternalMounts(resp.Body)
}
func (c *Client) ListGlobalExternalMounts(ctx context.Context) ([]ExternalMount, error) {
apiPath := "/index.php/apps/files_external/globalstorages?format=json"
resp, err := c.doRequest(ctx, "GET", apiPath, nil, ocsJSONHeaders())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &HTTPStatusError{Operation: "list global external mounts", StatusCode: resp.StatusCode}
}
return decodeExternalMounts(resp.Body)
}
func decodeExternalMounts(body io.Reader) ([]ExternalMount, 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) == "null" {
return nil, nil
}
var mounts []ExternalMount
if err := json.Unmarshal(raw, &mounts); err == nil {
return mounts, nil
}
var asMap map[string]ExternalMount
if err := json.Unmarshal(raw, &asMap); err != nil {
return nil, err
}
out := make([]ExternalMount, 0, len(asMap))
for _, m := range asMap {
out = append(out, m)
}
return out, nil
}
// CreateOAuthExternalMount creates a mount using OAuth2 backend (Google, Dropbox, etc.).
func (c *Client) CreateOAuthExternalMount(ctx context.Context, userID, mountPoint, backend, authBackend string, oauthConfig map[string]string) (int, error) {
config := map[string]string{
"configured": "false",
"token": "",
}
for k, v := range oauthConfig {
config[k] = v
}
return c.createExternalMount(ctx, mountPoint, backend, authBackend, userID, config)
}
type OAuth2StepResult struct {
URL string
Token string
}
func (c *Client) StartExternalStorageOAuth2(ctx context.Context, userID, clientID, clientSecret, redirectURI string) (string, error) {
result, err := c.postExternalStorageOAuth2(ctx, userID, map[string]string{
"step": "1",
"client_id": clientID,
"client_secret": clientSecret,
"redirect": redirectURI,
})
if err != nil {
return "", err
}
if result.URL == "" {
return "", fmt.Errorf("oauth2 step 1: empty authorization url")
}
return result.URL, nil
}
func (c *Client) CompleteExternalStorageOAuth2(ctx context.Context, userID, clientID, clientSecret, redirectURI, code string) (string, error) {
result, err := c.postExternalStorageOAuth2(ctx, userID, map[string]string{
"step": "2",
"client_id": clientID,
"client_secret": clientSecret,
"redirect": redirectURI,
"code": code,
})
if err != nil {
return "", err
}
if result.Token == "" {
return "", fmt.Errorf("oauth2 step 2: empty token")
}
return result.Token, nil
}
func (c *Client) UpdateUserExternalMountOAuth(ctx context.Context, userID string, mountID int, clientID, clientSecret, token string) error {
form := url.Values{}
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("token", token)
form.Set("configured", "true")
apiPath := fmt.Sprintf("/index.php/apps/files_external/userstorages/%d?format=json", mountID)
resp, err := c.DoAsUser(ctx, "PUT", apiPath, strings.NewReader(form.Encode()), userID, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("update external mount oauth: %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
func (c *Client) postExternalStorageOAuth2(ctx context.Context, userID string, fields map[string]string) (OAuth2StepResult, error) {
form := url.Values{}
for k, v := range fields {
form.Set(k, v)
}
apiPath := "/index.php/apps/files_external/ajax/oauth2.php"
resp, err := c.DoAsUser(ctx, "POST", apiPath, strings.NewReader(form.Encode()), userID, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
})
if err != nil {
return OAuth2StepResult{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 8192))
if err != nil {
return OAuth2StepResult{}, err
}
if resp.StatusCode != http.StatusOK {
return OAuth2StepResult{}, fmt.Errorf("oauth2 request: %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var payload struct {
Status string `json:"status"`
Data struct {
URL string `json:"url"`
Token string `json:"token"`
Message string `json:"message"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return OAuth2StepResult{}, fmt.Errorf("oauth2 response decode: %w", err)
}
if payload.Status != "success" {
msg := strings.TrimSpace(payload.Data.Message)
if msg == "" {
msg = strings.TrimSpace(string(body))
}
return OAuth2StepResult{}, fmt.Errorf("oauth2 failed: %s", msg)
}
return OAuth2StepResult{URL: payload.Data.URL, Token: payload.Data.Token}, nil
}
func ParseMountID(raw string) (int, error) {
return strconv.Atoi(strings.TrimSpace(raw))
}