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

+64
View File
@@ -0,0 +1,64 @@
package webdav
import "testing"
// dedupe vergibt Namen für gleichnamige Freigaben. Entscheidend ist, dass die
// Vergabe bei gleichem Bestand immer gleich ausfällt: Ein Sync-Client, dem sich
// Pfade zwischen zwei Läufen verschieben, lädt alles neu hoch oder löscht es.
func TestDedupeIsStable(t *testing.T) {
in := []sharedEntry{
{owner: "anna", name: "Projekt"},
{owner: "ben", name: "Projekt"},
{owner: "anna", name: "Projekt"},
{owner: "anna", name: "Notizen"},
}
first := namesOf(dedupe(append([]sharedEntry(nil), in...)))
// Andere Eingabereihenfolge, gleicher Bestand → gleiches Ergebnis.
shuffled := []sharedEntry{in[3], in[2], in[1], in[0]}
second := namesOf(dedupe(shuffled))
if len(first) != len(second) {
t.Fatalf("unterschiedlich viele Einträge: %v vs %v", first, second)
}
for i := range first {
if first[i] != second[i] {
t.Fatalf("Reihenfolge nicht stabil:\n %v\n %v", first, second)
}
}
}
func TestDedupeMakesNamesUniquePerOwner(t *testing.T) {
got := namesOf(dedupe([]sharedEntry{
{owner: "anna", name: "Projekt"},
{owner: "anna", name: "Projekt"},
{owner: "anna", name: "Projekt"},
// Gleicher Name bei einem anderen Eigentümer ist kein Konflikt – die
// beiden liegen in verschiedenen Verzeichnissen.
{owner: "ben", name: "Projekt"},
}))
want := []string{"anna/Projekt", "anna/Projekt (2)", "anna/Projekt (3)", "ben/Projekt"}
for i := range want {
if got[i] != want[i] {
t.Errorf("Eintrag %d = %q, erwartet %q", i, got[i], want[i])
}
}
}
// owners liefert die zweite Ebene unter /Shared – jede Person genau einmal.
func TestOwnersAreUnique(t *testing.T) {
got := owners([]sharedEntry{
{owner: "anna"}, {owner: "anna"}, {owner: "ben"}, {owner: "anna"},
})
if len(got) != 2 || got[0] != "anna" || got[1] != "ben" {
t.Fatalf("owners = %v, erwartet [anna ben]", got)
}
}
func namesOf(entries []sharedEntry) []string {
out := make([]string, len(entries))
for i, e := range entries {
out[i] = e.owner + "/" + e.name
}
return out
}