Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.4 KiB
Go
103 lines
3.4 KiB
Go
package files
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
// Auflösung von Pfaden zu Ordnern – gebraucht von WebDAV, das über Pfade
|
||
// adressiert statt über IDs.
|
||
|
||
// ── WebDAV helpers ────────────────────────────────────────────────────────────
|
||
|
||
// FolderByPath resolves a path like ["photos", "2024"] to a Folder.
|
||
// Returns a zero-ID folder for an empty parts slice (root).
|
||
func (s *Store) FolderByPath(ctx context.Context, principalID uuid.UUID, parts []string) (*Folder, error) {
|
||
return s.FolderByPathFrom(ctx, principalID, nil, parts)
|
||
}
|
||
|
||
// FolderByPathFrom löst Pfadteile ab einem bestimmten Ordner auf. base == nil
|
||
// heißt: ab der Wurzel des Prinzipals.
|
||
//
|
||
// Der Einstiegspunkt ist nötig, weil WebDAV geteilte Ordner unter einem
|
||
// virtuellen Pfad einhängt: Dort beginnt der Weg nicht bei der Wurzel des
|
||
// Eigentümers, sondern genau bei dem Ordner, der freigegeben wurde – alles
|
||
// darüber geht den Empfänger nichts an.
|
||
func (s *Store) FolderByPathFrom(ctx context.Context, principalID uuid.UUID, base *uuid.UUID, parts []string) (*Folder, error) {
|
||
if len(parts) == 0 {
|
||
if base == nil {
|
||
return &Folder{OwnerID: &principalID, Name: "/"}, nil
|
||
}
|
||
return s.folderByID(ctx, *base)
|
||
}
|
||
parentID := base
|
||
var current *Folder
|
||
for _, part := range parts {
|
||
var f Folder
|
||
var err error
|
||
if parentID == nil {
|
||
err = s.db.QueryRow(ctx,
|
||
`SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders
|
||
WHERE (owner_id = $1 OR group_id = $1) AND parent_id IS NULL AND name=$2`,
|
||
principalID, part,
|
||
).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt)
|
||
} else {
|
||
err = s.db.QueryRow(ctx,
|
||
`SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders
|
||
WHERE (owner_id = $1 OR group_id = $1) AND parent_id=$2 AND name=$3`,
|
||
principalID, parentID, part,
|
||
).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt)
|
||
}
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("folder by path: %w", err)
|
||
}
|
||
parentID = &f.ID
|
||
current = &f
|
||
}
|
||
return current, nil
|
||
}
|
||
|
||
func (s *Store) folderByID(ctx context.Context, id uuid.UUID) (*Folder, error) {
|
||
var f Folder
|
||
err := s.db.QueryRow(ctx,
|
||
`SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders WHERE id = $1`, id,
|
||
).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("folder by id: %w", err)
|
||
}
|
||
return &f, nil
|
||
}
|
||
|
||
// FileInFolder finds a non-deleted file by name within a folder (nil = root).
|
||
func (s *Store) FileInFolder(ctx context.Context, ownerID uuid.UUID, folderID *uuid.UUID, name string) (*File, error) {
|
||
var f File
|
||
var err error
|
||
if folderID == nil {
|
||
err = scanFile(s.db.QueryRow(ctx,
|
||
`SELECT `+fileCols+` FROM files
|
||
WHERE (owner_id = $1 OR group_id = $1) AND folder_id IS NULL AND name=$2 AND is_deleted=false`,
|
||
ownerID, name,
|
||
), &f)
|
||
} else {
|
||
err = scanFile(s.db.QueryRow(ctx,
|
||
`SELECT `+fileCols+` FROM files
|
||
WHERE (owner_id = $1 OR group_id = $1) AND folder_id=$2 AND name=$3 AND is_deleted=false`,
|
||
ownerID, folderID, name,
|
||
), &f)
|
||
}
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, ErrNotFound
|
||
}
|
||
return &f, err
|
||
}
|