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:
HoneyandClaude Opus 5 committed 2026-08-24 09:25:36 +02:00
commit 31a027c93a
225 files changed
+38042

No files matched your search

+129
View File
@@ -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)
}