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>
104 lines
4.9 KiB
PL/PgSQL
104 lines
4.9 KiB
PL/PgSQL
-- Aether – Initial schema
|
||
-- Phase 0: users, sessions, folders, files (no email)
|
||
|
||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||
|
||
-- ── Users ─────────────────────────────────────────────────────────────────────
|
||
CREATE TABLE users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
username TEXT NOT NULL UNIQUE,
|
||
password TEXT NOT NULL, -- Argon2id hash
|
||
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||
quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10 GB default
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- ── Sessions ──────────────────────────────────────────────────────────────────
|
||
CREATE TABLE sessions (
|
||
id TEXT PRIMARY KEY,
|
||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
data BYTEA NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
expires_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
|
||
|
||
-- ── Folders ───────────────────────────────────────────────────────────────────
|
||
CREATE TABLE folders (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
UNIQUE (owner_id, parent_id, name)
|
||
);
|
||
|
||
CREATE INDEX idx_folders_owner ON folders(owner_id);
|
||
CREATE INDEX idx_folders_parent ON folders(parent_id);
|
||
|
||
-- ── Files ─────────────────────────────────────────────────────────────────────
|
||
CREATE TABLE files (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
folder_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
||
size BIGINT NOT NULL DEFAULT 0,
|
||
checksum TEXT NOT NULL, -- SHA-256 hex
|
||
storage_key TEXT NOT NULL, -- path within blob storage
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||
deleted_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_files_owner ON files(owner_id);
|
||
CREATE INDEX idx_files_folder ON files(folder_id);
|
||
CREATE INDEX idx_files_deleted ON files(is_deleted);
|
||
CREATE INDEX idx_files_storage ON files(storage_key);
|
||
|
||
-- Partial unique index: no two non-deleted files can share name+folder for the same owner.
|
||
CREATE UNIQUE INDEX idx_files_unique_name
|
||
ON files(owner_id, folder_id, name)
|
||
WHERE NOT is_deleted;
|
||
|
||
-- ── File versions ─────────────────────────────────────────────────────────────
|
||
-- Previous versions of files (kept for Soft-Delete + versioning).
|
||
CREATE TABLE file_versions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||
version INTEGER NOT NULL,
|
||
size BIGINT NOT NULL,
|
||
checksum TEXT NOT NULL,
|
||
storage_key TEXT NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_file_versions_file ON file_versions(file_id);
|
||
|
||
-- ── Updated_at trigger ────────────────────────────────────────────────────────
|
||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
NEW.updated_at = NOW();
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER trg_users_updated_at
|
||
BEFORE UPDATE ON users
|
||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||
|
||
CREATE TRIGGER trg_folders_updated_at
|
||
BEFORE UPDATE ON folders
|
||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||
|
||
CREATE TRIGGER trg_files_updated_at
|
||
BEFORE UPDATE ON files
|
||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|