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>
130 lines
4.3 KiB
Go
130 lines
4.3 KiB
Go
package files
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
// Favoriten der Seitenleiste. Reihenfolge und Auf-/Zugeklappt-Zustand
|
||
// liegen serverseitig, damit sie auf jedem Gerät gleich sind.
|
||
|
||
// ── Favorites ─────────────────────────────────────────────────────────────────
|
||
|
||
func (s *Store) ListFavorites(ctx context.Context, userID uuid.UUID) ([]*Favorite, error) {
|
||
// Nur Favoriten anzeigen, die noch erreichbar sind. Ein Lesezeichen auf
|
||
// einen Ordner, den man nicht mehr öffnen darf – weil man die Gruppe
|
||
// verlassen hat oder die Freigabe zurückgezogen wurde – bliebe sonst als
|
||
// toter Eintrag in der Seitenleiste stehen und liefe beim Klick ins Leere.
|
||
rows, err := s.db.Query(ctx,
|
||
`WITH RECURSIVE shared_roots AS (
|
||
SELECT e.resource_folder_id AS id
|
||
FROM acl_entries e
|
||
WHERE e.resource_folder_id IS NOT NULL
|
||
AND (e.subject_user_id = $1
|
||
OR e.subject_group_id IN (SELECT group_id FROM group_members WHERE user_id = $1))
|
||
UNION
|
||
-- Nach unten aufspannen: Wer einen Ordner freigegeben bekommt, darf
|
||
-- auch alles darunter, und darf sich das ebenso merken.
|
||
SELECT fo.id FROM folders fo JOIN shared_roots sr ON fo.parent_id = sr.id
|
||
)
|
||
SELECT f.id, f.user_id, f.folder_id, f.position, f.is_expanded, f.created_at,
|
||
fo.name, fo.parent_id
|
||
FROM favorites f
|
||
JOIN folders fo ON fo.id = f.folder_id
|
||
WHERE f.user_id = $1
|
||
AND ( fo.owner_id = $1
|
||
OR fo.group_id IN (SELECT group_id FROM group_members WHERE user_id = $1)
|
||
OR fo.id IN (SELECT id FROM shared_roots) )
|
||
ORDER BY f.position`,
|
||
userID,
|
||
)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list favorites: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
favs := make([]*Favorite, 0)
|
||
for rows.Next() {
|
||
var fav Favorite
|
||
if err := rows.Scan(
|
||
&fav.ID, &fav.UserID, &fav.FolderID, &fav.Position, &fav.IsExpanded, &fav.CreatedAt,
|
||
&fav.FolderName, &fav.FolderParentID,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
favs = append(favs, &fav)
|
||
}
|
||
return favs, rows.Err()
|
||
}
|
||
|
||
func (s *Store) AddFavorite(ctx context.Context, userID, folderID uuid.UUID) (*Favorite, error) {
|
||
var fav Favorite
|
||
err := s.db.QueryRow(ctx,
|
||
`INSERT INTO favorites (user_id, folder_id, position)
|
||
VALUES ($1, $2, (SELECT COALESCE(MAX(position)+1, 0) FROM favorites WHERE user_id=$1))
|
||
ON CONFLICT (user_id, folder_id) DO NOTHING
|
||
RETURNING id, user_id, folder_id, position, is_expanded, created_at`,
|
||
userID, folderID,
|
||
).Scan(&fav.ID, &fav.UserID, &fav.FolderID, &fav.Position, &fav.IsExpanded, &fav.CreatedAt)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, ErrNameTaken // already a favorite
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("add favorite: %w", err)
|
||
}
|
||
// Fetch folder name for response.
|
||
var f Folder
|
||
_ = s.db.QueryRow(ctx,
|
||
`SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders WHERE id=$1`, folderID,
|
||
).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt)
|
||
fav.FolderName = f.Name
|
||
fav.FolderParentID = f.ParentID
|
||
return &fav, nil
|
||
}
|
||
|
||
func (s *Store) RemoveFavorite(ctx context.Context, userID, folderID uuid.UUID) error {
|
||
tag, err := s.db.Exec(ctx,
|
||
`DELETE FROM favorites WHERE user_id=$1 AND folder_id=$2`, userID, folderID)
|
||
if err != nil {
|
||
return fmt.Errorf("remove favorite: %w", err)
|
||
}
|
||
if tag.RowsAffected() == 0 {
|
||
return ErrNotFound
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Store) SetFavoriteExpanded(ctx context.Context, userID, folderID uuid.UUID, expanded bool) error {
|
||
tag, err := s.db.Exec(ctx,
|
||
`UPDATE favorites SET is_expanded=$3 WHERE user_id=$1 AND folder_id=$2`,
|
||
userID, folderID, expanded,
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("set favorite expanded: %w", err)
|
||
}
|
||
if tag.RowsAffected() == 0 {
|
||
return ErrNotFound
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Store) ReorderFavorites(ctx context.Context, userID uuid.UUID, folderIDs []uuid.UUID) error {
|
||
tx, err := s.db.Begin(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback(ctx)
|
||
for i, fid := range folderIDs {
|
||
if _, err := tx.Exec(ctx,
|
||
`UPDATE favorites SET position=$3 WHERE user_id=$1 AND folder_id=$2`,
|
||
userID, fid, i,
|
||
); err != nil {
|
||
return fmt.Errorf("reorder favorites: %w", err)
|
||
}
|
||
}
|
||
return tx.Commit(ctx)
|
||
}
|