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,107 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pool: %w", err)
|
||||
}
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func Migrate(dsn string) error {
|
||||
src, err := iofs.New(migrationsFS, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("load migrations: %w", err)
|
||||
}
|
||||
// convert pgx DSN → lib/pq DSN format for migrate
|
||||
m, err := migrate.NewWithSourceInstance("iofs", src, "postgres://"+dsnToURL(dsn))
|
||||
if err != nil {
|
||||
return fmt.Errorf("init migrate: %w", err)
|
||||
}
|
||||
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||
return fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dsnToURL converts "host=... port=... dbname=... user=... password=... sslmode=..."
|
||||
// to a postgres:// URL for golang-migrate.
|
||||
func dsnToURL(dsn string) string {
|
||||
// pgxpool DSN is already accepted by migrate if we prefix with postgres://
|
||||
// Actually golang-migrate accepts the postgres:// URL format.
|
||||
// Since our DSN is key=value, we need to convert it.
|
||||
// Simplest approach: parse key=value pairs.
|
||||
params := map[string]string{}
|
||||
for _, part := range splitDSN(dsn) {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
idx := indexByte(part, '=')
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
params[part[:idx]] = part[idx+1:]
|
||||
}
|
||||
host := params["host"]
|
||||
port := params["port"]
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@%s:%s/%s?sslmode=%s",
|
||||
params["user"], params["password"],
|
||||
host, port, params["dbname"],
|
||||
orDefault(params["sslmode"], "disable"),
|
||||
)
|
||||
}
|
||||
|
||||
func splitDSN(dsn string) []string {
|
||||
var parts []string
|
||||
cur := ""
|
||||
for _, r := range dsn {
|
||||
if r == ' ' {
|
||||
if cur != "" {
|
||||
parts = append(parts, cur)
|
||||
cur = ""
|
||||
}
|
||||
} else {
|
||||
cur += string(r)
|
||||
}
|
||||
}
|
||||
if cur != "" {
|
||||
parts = append(parts, cur)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func indexByte(s string, b byte) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == b {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func orDefault(s, def string) string {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in new issue
Block a user