feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
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>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user