- 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.
300 lines
9.0 KiB
Go
300 lines
9.0 KiB
Go
package drive
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/paginate"
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
"github.com/ultisuite/ulti-backend/internal/driveroot"
|
|
"github.com/ultisuite/ulti-backend/internal/drivestore"
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
)
|
|
|
|
type resolvedRoot struct {
|
|
ref driveroot.Ref
|
|
davPath string
|
|
logicalDir string
|
|
ncUserID string
|
|
}
|
|
|
|
func (s *Service) ensureStore() *drivestore.Store {
|
|
if s.store == nil && s.db != nil {
|
|
s.store = drivestore.NewStore(s.db)
|
|
}
|
|
return s.store
|
|
}
|
|
|
|
func (s *Service) resolveRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) (resolvedRoot, error) {
|
|
ref.Path = nextcloud.NormalizeClientPath(ref.Path)
|
|
switch ref.Kind {
|
|
case driveroot.KindPersonal, "":
|
|
return resolvedRoot{
|
|
ref: driveroot.Personal(ref.Path),
|
|
davPath: s.nc.WebDAVPath(ncUserID, ref.Path),
|
|
logicalDir: ref.Path,
|
|
ncUserID: ncUserID,
|
|
}, nil
|
|
case driveroot.KindOrg:
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return resolvedRoot{}, ErrInvalid
|
|
}
|
|
folder, err := store.GetOrgFolder(ctx, ref.RootID)
|
|
if err != nil {
|
|
if errors.Is(err, drivestore.ErrOrgFolderNotFound) {
|
|
return resolvedRoot{}, ErrNotFound
|
|
}
|
|
return resolvedRoot{}, err
|
|
}
|
|
return resolvedRoot{
|
|
ref: driveroot.Org(ref.RootID, ref.Path),
|
|
davPath: nextcloud.GroupFolderWebDAVPath(folder.NCFolderID, ref.Path),
|
|
logicalDir: ref.Path,
|
|
ncUserID: ncUserID,
|
|
}, nil
|
|
case driveroot.KindMount:
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return resolvedRoot{}, ErrInvalid
|
|
}
|
|
mount, err := store.GetMount(ctx, ref.RootID)
|
|
if err != nil {
|
|
if errors.Is(err, drivestore.ErrMountNotFound) {
|
|
return resolvedRoot{}, ErrNotFound
|
|
}
|
|
return resolvedRoot{}, err
|
|
}
|
|
fullPath := joinMountPath(mount.MountPoint, ref.Path)
|
|
return resolvedRoot{
|
|
ref: driveroot.Mount(ref.RootID, ref.Path),
|
|
davPath: s.nc.WebDAVPath(ncUserID, fullPath),
|
|
logicalDir: ref.Path,
|
|
ncUserID: ncUserID,
|
|
}, nil
|
|
default:
|
|
return resolvedRoot{}, ErrInvalid
|
|
}
|
|
}
|
|
|
|
func joinMountPath(mountPoint, logicalPath string) string {
|
|
mp := strings.Trim(mountPoint, "/")
|
|
lp := strings.Trim(logicalPath, "/")
|
|
if mp == "" {
|
|
if lp == "" {
|
|
return "/"
|
|
}
|
|
return "/" + lp
|
|
}
|
|
if lp == "" {
|
|
return "/" + mp
|
|
}
|
|
return "/" + mp + "/" + lp
|
|
}
|
|
|
|
func (s *Service) resolveFileRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) (resolvedRoot, error) {
|
|
resolved, err := s.resolveRoot(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return resolvedRoot{}, err
|
|
}
|
|
if ref.Path != "/" && !strings.HasSuffix(resolved.ref.Path, "/") {
|
|
return resolved, nil
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func (s *Service) resolveFileDAV(ctx context.Context, ncUserID string, ref driveroot.Ref) (resolvedRoot, string, error) {
|
|
base, err := s.resolveRoot(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return resolvedRoot{}, "", err
|
|
}
|
|
filePath := nextcloud.NormalizeClientPath(ref.Path)
|
|
if filePath == "/" {
|
|
return base, base.davPath, nil
|
|
}
|
|
davPath := base.davPath
|
|
if !strings.HasSuffix(davPath, "/") && filePath != "/" {
|
|
// base.davPath is directory; append file segment if needed
|
|
rel := strings.TrimPrefix(filePath, "/")
|
|
if base.ref.Path == "/" || strings.HasPrefix(filePath, base.ref.Path+"/") || base.ref.Path == filePath {
|
|
// already included in path from ref
|
|
}
|
|
_ = rel
|
|
}
|
|
// For file operations, ref.Path is full logical path within root
|
|
switch base.ref.Kind {
|
|
case driveroot.KindOrg:
|
|
folder, _ := s.ensureStore().GetOrgFolder(ctx, ref.RootID)
|
|
davPath = nextcloud.GroupFolderWebDAVPath(folder.NCFolderID, filePath)
|
|
case driveroot.KindMount:
|
|
mount, _ := s.ensureStore().GetMount(ctx, ref.RootID)
|
|
davPath = s.nc.WebDAVPath(ncUserID, joinMountPath(mount.MountPoint, filePath))
|
|
default:
|
|
davPath = s.nc.WebDAVPath(ncUserID, filePath)
|
|
}
|
|
return base, davPath, nil
|
|
}
|
|
|
|
func (s *Service) ListFilesAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref, params query.ListParams) (FilesList, error) {
|
|
resolved, err := s.resolveRoot(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return FilesList{}, err
|
|
}
|
|
var files []nextcloud.FileInfo
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
files, err = s.nc.ListFiles(ctx, ncUserID, ref.Path)
|
|
} else {
|
|
files, err = s.nc.ListFilesAtDAV(ctx, ncUserID, resolved.davPath, ref.Path)
|
|
}
|
|
if err != nil {
|
|
return FilesList{}, mapDriveError(err)
|
|
}
|
|
files = driveroot.EnrichFiles(files, resolved.ref)
|
|
filtered := visibleDriveFiles(files, params.Q)
|
|
page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
|
|
return FilesList{
|
|
Files: page,
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) StatFileAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) (nextcloud.FileInfo, error) {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return nextcloud.FileInfo{}, err
|
|
}
|
|
var file nextcloud.FileInfo
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
file, err = s.nc.StatFile(ctx, ncUserID, ref.Path)
|
|
} else {
|
|
file, err = s.nc.StatFileAtDAV(ctx, ncUserID, davPath, ref.Path)
|
|
}
|
|
if err != nil {
|
|
return nextcloud.FileInfo{}, mapDriveError(err)
|
|
}
|
|
return driveroot.EnrichFile(file, ref), nil
|
|
}
|
|
|
|
func (s *Service) UploadAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref, content io.Reader, contentType string, contentLength int64) error {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
return s.Upload(ctx, ncUserID, ref.Path, content, contentType, contentLength)
|
|
}
|
|
return mapDriveError(s.nc.UploadAtDAV(ctx, ncUserID, davPath, contentType, content))
|
|
}
|
|
|
|
func (s *Service) DeleteAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) error {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
return s.Delete(ctx, ncUserID, ref.Path)
|
|
}
|
|
return mapDriveError(s.nc.DeleteAtDAV(ctx, ncUserID, davPath))
|
|
}
|
|
|
|
func (s *Service) CreateFolderAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) error {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
return s.CreateFolder(ctx, ncUserID, ref.Path)
|
|
}
|
|
return mapDriveError(s.nc.CreateFolderAtDAV(ctx, ncUserID, davPath))
|
|
}
|
|
|
|
func (s *Service) DownloadAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref) (io.ReadCloser, string, error) {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if ref.Kind == driveroot.KindPersonal || ref.Kind == "" {
|
|
return s.Download(ctx, ncUserID, ref.Path)
|
|
}
|
|
body, contentType, err := s.nc.DownloadAtDAV(ctx, ncUserID, davPath)
|
|
if err != nil {
|
|
return nil, "", mapDriveError(err)
|
|
}
|
|
return body, contentType, nil
|
|
}
|
|
|
|
func (s *Service) MoveAtRoot(ctx context.Context, ncUserID string, source, destination driveroot.Ref) error {
|
|
srcDAV, err := s.davPathForRef(ctx, ncUserID, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destDAV, err := s.davPathForRef(ctx, ncUserID, destination)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if source.Kind == driveroot.KindPersonal && destination.Kind == driveroot.KindPersonal {
|
|
return s.Move(ctx, ncUserID, source.Path, destination.Path)
|
|
}
|
|
return mapDriveError(s.nc.MoveAtDAV(ctx, ncUserID, srcDAV, destDAV))
|
|
}
|
|
|
|
func (s *Service) CopyAtRoot(ctx context.Context, ncUserID string, source, destination driveroot.Ref) error {
|
|
srcDAV, err := s.davPathForRef(ctx, ncUserID, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destDAV, err := s.davPathForRef(ctx, ncUserID, destination)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if source.Kind == driveroot.KindPersonal && destination.Kind == driveroot.KindPersonal {
|
|
return s.Copy(ctx, ncUserID, source.Path, destination.Path)
|
|
}
|
|
return mapDriveError(s.nc.CopyAtDAV(ctx, ncUserID, srcDAV, destDAV))
|
|
}
|
|
|
|
func (s *Service) RenameAtRoot(ctx context.Context, ncUserID string, ref driveroot.Ref, newName string) error {
|
|
if strings.Contains(newName, "/") {
|
|
return ErrInvalid
|
|
}
|
|
dir := path.Dir(strings.TrimPrefix(ref.Path, "/"))
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
destPath := "/" + strings.Trim(newName, "/")
|
|
if dir != "" {
|
|
destPath = "/" + dir + destPath
|
|
}
|
|
dest := driveroot.Ref{Kind: ref.Kind, RootID: ref.RootID, Path: destPath}
|
|
return s.MoveAtRoot(ctx, ncUserID, ref, dest)
|
|
}
|
|
|
|
func (s *Service) davPathForRef(ctx context.Context, ncUserID string, ref driveroot.Ref) (string, error) {
|
|
_, davPath, err := s.resolveFileDAV(ctx, ncUserID, ref)
|
|
return davPath, err
|
|
}
|
|
|
|
func (s *Service) platformUserID(ctx context.Context, externalID string) (string, error) {
|
|
if s.db == nil {
|
|
return "", fmt.Errorf("database not configured")
|
|
}
|
|
var id string
|
|
err := s.db.QueryRow(ctx, `SELECT id::text FROM users WHERE external_id = $1`, externalID).Scan(&id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func extendServiceStore(s *Service, db *pgxpool.Pool) {
|
|
if s != nil && s.store == nil && db != nil {
|
|
s.store = drivestore.NewStore(db)
|
|
}
|
|
}
|