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

+50
View File
@@ -0,0 +1,50 @@
package files
import (
"net/http"
"aether/pkg/response"
)
// TextPages beantwortet GET /api/v1/files/{id}/text.
//
// **Wofür das da ist:** Der PDF-Betrachter im Browser sucht im Dokument – aber
// bei einem *Scan* steht im PDF kein einziges Wort, nur ein Bild. Der erkannte
// Text liegt daneben in der Datenbank. Ohne diesen Endpunkt fände die Suche im
// Dokument ausgerechnet bei dem Dateityp nichts, für den die Texterkennung
// überhaupt gebaut wurde.
//
// Geliefert wird der Text **in Seiten zerlegt**. Mehr geht nicht: Wo genau auf
// dem Bild ein Wort steht, weiß nur die Erkennung selbst, und wir bewahren
// ihre Koordinaten nicht auf. Die Seite ist aber der Sprung, auf den es
// ankommt – bei einem zwölfseitigen Beleg ist das der Unterschied zwischen
// „gefunden" und „such weiter".
//
// Die Prüfung ist dieselbe wie beim Herunterladen: Wer die Datei nicht lesen
// darf, bekommt hier auch ihren Text nicht. Das ist keine Kleinigkeit – der
// Text *ist* der Inhalt der Rechnung.
func (h *Handler) TextPages(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r, "id")
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid file id")
return
}
if _, ok := h.resolveFile(w, r, id, false); !ok {
return
}
// Ohne Texterkennung gibt es keinen Text – das ist eine Auskunft, kein
// Fehler. Der Betrachter zeigt dann schlicht keine Fundstellen an.
if h.docs == nil {
response.JSON(w, http.StatusOK, map[string]any{"pages": []string{}, "source": ""})
return
}
pages, source, err := h.docs.TextPages(r.Context(), id)
if err != nil {
response.Error(w, http.StatusInternalServerError, "database error")
return
}
if pages == nil {
pages = []string{}
}
response.JSON(w, http.StatusOK, map[string]any{"pages": pages, "source": source})
}