ultisuite-backend/internal/api/drive/mounts_service.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

299 lines
8.2 KiB
Go

package drive
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/ultisuite/ulti-backend/internal/drivestore"
"github.com/ultisuite/ulti-backend/internal/nextcloud"
"github.com/ultisuite/ulti-backend/internal/orgpolicy"
)
var ErrOAuthNotConfigured = errors.New("oauth provider not configured")
type MountView struct {
ID string `json:"id"`
Scope string `json:"scope"`
OrgSlug *string `json:"org_slug,omitempty"`
DisplayName string `json:"display_name"`
BackendType string `json:"backend_type"`
MountPoint string `json:"mount_point"`
Status string `json:"status"`
LastError string `json:"last_error,omitempty"`
NCMountID *int `json:"nc_mount_id,omitempty"`
NeedsOAuth bool `json:"needs_oauth,omitempty"`
}
type CreateMountParams struct {
Scope string
OrgSlug string
DisplayName string
BackendType string
WebDAV *nextcloud.WebDAVMountConfig
OAuthBackend string
OAuthAuth string
}
func oauthProviderForBackend(backendType string) (providerKey, ncBackend, authBackend string, isOAuth bool) {
switch strings.TrimSpace(strings.ToLower(backendType)) {
case "googledrive", "google":
return orgpolicy.MountOAuthProviderGoogle, "googledrive", "oauth2::google", true
case "dropbox":
return orgpolicy.MountOAuthProviderDropbox, "dropbox", "oauth2::dropbox", true
case "onedrive", "microsoft":
return orgpolicy.MountOAuthProviderMicrosoft, "onedrive", "oauth2::microsoft", true
default:
return "", "", "", false
}
}
func (s *Service) orgPolicyLoader() *orgpolicy.Loader {
return orgpolicy.NewLoader(s.db, nil)
}
func (s *Service) ListMountsForUser(ctx context.Context, platformUserID, ncUserID string, orgSlugs []string) ([]MountView, error) {
store := s.ensureStore()
if store == nil {
return nil, fmt.Errorf("store not configured")
}
mounts, err := store.ListMountsForUser(ctx, platformUserID, orgSlugs)
if err != nil {
return nil, err
}
out := make([]MountView, 0, len(mounts))
for _, m := range mounts {
out = append(out, mapMountView(m))
}
return out, nil
}
func mapMountView(m drivestore.Mount) MountView {
view := MountView{
ID: m.ID,
Scope: m.Scope,
OrgSlug: m.OrgSlug,
DisplayName: m.DisplayName,
BackendType: m.BackendType,
MountPoint: m.MountPoint,
Status: m.Status,
LastError: m.LastError,
NCMountID: m.NCMountID,
}
if m.Status == "pending_oauth" {
view.NeedsOAuth = true
}
return view
}
func (s *Service) CreateMount(ctx context.Context, platformUserID, ncUserID string, p CreateMountParams) (MountView, error) {
store := s.ensureStore()
if store == nil {
return MountView{}, fmt.Errorf("store not configured")
}
displayName := strings.TrimSpace(p.DisplayName)
backendType := strings.TrimSpace(strings.ToLower(p.BackendType))
if displayName == "" || backendType == "" {
return MountView{}, ErrInvalid
}
mountPoint := "/" + strings.Trim(displayName, "/")
scope := strings.TrimSpace(strings.ToLower(p.Scope))
if scope != "user" && scope != "org" {
return MountView{}, ErrInvalid
}
var ncMountID int
var err error
var configEnc []byte
status := "active"
providerKey, ncBackend, authBackend, isOAuth := oauthProviderForBackend(backendType)
if isOAuth {
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
if err != nil {
return MountView{}, err
}
if !creds.Enabled {
return MountView{}, ErrOAuthNotConfigured
}
oauthConfig := map[string]string{
"client_id": creds.ClientID,
"client_secret": creds.ClientSecret,
"configured": "false",
"token": "",
}
configEnc, _ = json.Marshal(oauthConfig)
if scope == "org" {
return MountView{}, ErrInvalid
}
ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, ncBackend, authBackend, oauthConfig)
status = "pending_oauth"
} else {
switch backendType {
case "webdav", "dav":
if p.WebDAV == nil {
return MountView{}, ErrInvalid
}
configEnc, _ = json.Marshal(p.WebDAV)
if scope == "org" {
ncMountID, err = s.nc.CreateGlobalWebDAVMount(ctx, mountPoint, *p.WebDAV)
} else {
ncMountID, err = s.nc.CreateUserWebDAVMount(ctx, ncUserID, mountPoint, *p.WebDAV)
}
case "googledrive", "google", "dropbox", "onedrive", "microsoft":
return MountView{}, ErrOAuthNotConfigured
default:
if p.OAuthBackend != "" {
auth := p.OAuthAuth
if auth == "" {
auth = "oauth2::" + backendType
}
ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, p.OAuthBackend, auth, nil)
status = "pending_oauth"
} else {
return MountView{}, ErrInvalid
}
}
}
lastError := ""
if err != nil {
status = "error"
lastError = err.Error()
}
var ownerID *string
var orgSlug *string
if scope == "user" {
ownerID = &platformUserID
} else {
slug := strings.TrimSpace(strings.ToLower(p.OrgSlug))
if slug == "" {
return MountView{}, ErrInvalid
}
orgSlug = &slug
}
var ncIDPtr *int
if ncMountID > 0 {
ncIDPtr = &ncMountID
}
row, err := store.CreateMount(ctx, drivestore.CreateMountParams{
Scope: scope,
OwnerUserID: ownerID,
OrgSlug: orgSlug,
NCMountID: ncIDPtr,
DisplayName: displayName,
BackendType: backendType,
MountPoint: mountPoint,
Status: status,
ConfigEnc: configEnc,
})
if err != nil {
if ncMountID > 0 {
_ = s.nc.DeleteExternalMount(ctx, ncMountID)
}
return MountView{}, err
}
if status == "error" {
_ = store.UpdateMountStatus(ctx, row.ID, status, lastError, ncIDPtr)
}
return mapMountView(row), nil
}
func (s *Service) DeleteMount(ctx context.Context, mountID string) error {
store := s.ensureStore()
if store == nil {
return fmt.Errorf("store not configured")
}
mount, err := store.GetMount(ctx, mountID)
if err != nil {
return err
}
if mount.NCMountID != nil && *mount.NCMountID > 0 {
if err := s.nc.DeleteExternalMount(ctx, *mount.NCMountID); err != nil {
return mapDriveError(err)
}
}
return store.DeleteMount(ctx, mountID)
}
func (s *Service) GetMountOAuthURL(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI string) (string, error) {
if err := validateMountOAuthRedirectURI(redirectURI); err != nil {
return "", err
}
store := s.ensureStore()
if store == nil {
return "", fmt.Errorf("store not configured")
}
mount, err := store.GetMount(ctx, mountID)
if err != nil {
return "", err
}
if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID {
return "", ErrForbidden
}
if mount.NCMountID == nil {
return "", ErrInvalid
}
providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType)
if !isOAuth {
return "", ErrInvalid
}
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
if err != nil {
return "", err
}
if !creds.Enabled {
return "", ErrOAuthNotConfigured
}
return s.nc.StartExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI)
}
func (s *Service) CompleteMountOAuth(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI, code string) error {
if err := validateMountOAuthRedirectURI(redirectURI); err != nil {
return err
}
store := s.ensureStore()
if store == nil {
return fmt.Errorf("store not configured")
}
code = strings.TrimSpace(code)
if code == "" {
return ErrInvalid
}
mount, err := store.GetMount(ctx, mountID)
if err != nil {
return err
}
if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID {
return ErrForbidden
}
if mount.NCMountID == nil {
return ErrInvalid
}
providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType)
if !isOAuth {
return ErrInvalid
}
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
if err != nil {
return err
}
if !creds.Enabled {
return ErrOAuthNotConfigured
}
token, err := s.nc.CompleteExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI, code)
if err != nil {
_ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID)
return err
}
if err := s.nc.UpdateUserExternalMountOAuth(ctx, ncUserID, *mount.NCMountID, creds.ClientID, creds.ClientSecret, token); err != nil {
_ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID)
return err
}
return store.UpdateMountStatus(ctx, mount.ID, "active", "", mount.NCMountID)
}