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) } }