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,63 @@
|
||||
-- Volltext über den *Inhalt* – die zweite Hälfte der Suche.
|
||||
--
|
||||
-- Migration 000013 machte Datei*namen* durchsuchbar. Das reicht, solange
|
||||
-- Dateien sprechende Namen haben. Ein Scanner nennt seine Ablage aber
|
||||
-- "scan_0042.pdf", und genau die Belege sollen auffindbar werden.
|
||||
|
||||
-- ── Der erkannte Text ─────────────────────────────────────────────────────────
|
||||
-- Eigene Tabelle statt einer Spalte an files: Der Text ist um Größenordnungen
|
||||
-- länger als der Rest der Zeile, und files wird bei jedem Auflisten gelesen.
|
||||
-- Postgres lagert lange Werte zwar aus (TOAST), aber die tsvector-Spalte und
|
||||
-- ihr GIN-Index gehörten dann trotzdem an eine Tabelle, die ständig in
|
||||
-- Bewegung ist.
|
||||
CREATE TABLE file_text (
|
||||
file_id UUID PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
-- Woher der Text stammt. 'layer' = das PDF hatte bereits eine Textebene
|
||||
-- (pdftotext, verlustfrei); 'ocr' = aus dem Bild erkannt, kann Fehler
|
||||
-- enthalten. Der Unterschied ist für die Fehlersuche wichtig: Findet die
|
||||
-- Suche ein Wort nicht, sagt die Herkunft, ob OCR es verlesen hat.
|
||||
source TEXT NOT NULL CHECK (source IN ('layer', 'ocr')),
|
||||
-- Die Sprachen, mit denen erkannt wurde ("deu+eng"). Bei 'layer' leer.
|
||||
lang TEXT NOT NULL DEFAULT '',
|
||||
-- Zeichenzahl des Originals vor dem Kürzen – siehe maxTextBytes im Worker.
|
||||
chars INTEGER NOT NULL DEFAULT 0,
|
||||
pages INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Berechnete Spalte statt eines Triggers: Sie kann nie mit dem Inhalt
|
||||
-- auseinanderlaufen, weil Postgres sie bei jedem Schreiben selbst füllt.
|
||||
--
|
||||
-- 'german' fest verdrahtet, nicht 'simple': Ohne Stammformen fände
|
||||
-- "Rechnung" die "Rechnungen" nicht. Deutsch ist die Sprache der Belege,
|
||||
-- um die es hier geht; englische Wörter darin verlieren nur ihre
|
||||
-- Stammformbildung, sie bleiben wörtlich auffindbar. Eine Änderung wäre
|
||||
-- eine Migration – der Preis dafür, dass der Ausdruck IMMUTABLE sein muss.
|
||||
tsv tsvector GENERATED ALWAYS AS (to_tsvector('german', content)) STORED
|
||||
);
|
||||
|
||||
CREATE INDEX idx_file_text_tsv ON file_text USING gin (tsv);
|
||||
|
||||
-- ── Die Warteschlange ─────────────────────────────────────────────────────────
|
||||
-- Warum eine Tabelle und nicht einfach ein Goroutine-Aufruf wie bei den
|
||||
-- Vorschaubildern: OCR dauert Sekunden bis Minuten. Startet der Server in
|
||||
-- dieser Zeit neu – Update, Absturz, Neustart des Wirts –, ist ein Auftrag im
|
||||
-- Speicher verloren, und die Datei bleibt für immer unauffindbar, ohne dass es
|
||||
-- jemand merkt. Bei einem Vorschaubild fällt das auf; bei fehlendem Text nicht.
|
||||
CREATE TABLE ocr_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- Ein Auftrag je Datei. Wird dieselbe Datei erneut hochgeladen, ersetzt der
|
||||
-- neue Auftrag den alten (ON CONFLICT), statt sich daneben zu stapeln.
|
||||
file_id UUID NOT NULL UNIQUE REFERENCES files(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Der Arbeiter fragt nur nach offenen Aufträgen. Ein Teilindex bleibt klein,
|
||||
-- auch wenn sich zehntausend erledigte Zeilen ansammeln.
|
||||
CREATE INDEX idx_ocr_jobs_pending ON ocr_jobs(created_at)
|
||||
WHERE status IN ('pending', 'running');
|
||||
Reference in new issue
Block a user