- Moderationsseite und Melde-Funktion (report) komplett ausgebaut; folgt als eigenständiges Projekt - alle API-Endpunkte unter /api, dort ausschließlich JSON: auch 404, 405, Rate-Limit (429) und Panics (500) - Fehler nur über HTTP-Status; stille DB-Fehler in stats, logout und Vote-Zählern liefern jetzt 500 statt Nullen - /auth/headerbar entfernt (Frontend nutzt /api/user/info) - Frontend auf /api und statusbasierte Auswertung umgestellt - notes/api.md neu als Referenz der aktuellen API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// importTable beschreibt eine Tabelle für den einmaligen Umzug SQLite -> Postgres.
|
|
// Die Spalten heißen in beiden Datenbanken gleich; nur user heißt jetzt account.
|
|
type importTable struct {
|
|
src, dst string
|
|
cols []string
|
|
identity bool // hat eine id-Identity-Spalte, deren Sequenz nachgezogen werden muss
|
|
}
|
|
|
|
var importTables = []importTable{
|
|
{"user", "account", []string{"id", "uid", "username", "password", "created_at", "last_login", "avatar"}, true},
|
|
{"session", "session", []string{"id", "uid", "value", "created_at", "expires", "description"}, true},
|
|
{"entry", "entry", []string{"id", "pid", "uid", "created_at", "content", "filepath", "reply_to",
|
|
"reply_count", "last_activity", "deleted", "bump_count", "last_bump"}, true},
|
|
{"vote", "vote", []string{"id", "uid", "pid", "mode"}, true},
|
|
{"impression", "impression", []string{"day", "anon_ip", "path", "asn", "hits"}, false},
|
|
}
|
|
|
|
// importSQLite kopiert alle Daten aus der SQLite-Datei path in die (per initDB
|
|
// bereits verbundene und mit Schema versehene) Postgres-Datenbank. Alles in einer
|
|
// Transaktion: entweder ist der Umzug vollständig oder es ist nichts passiert.
|
|
// Bricht ab, wenn Zieltabellen schon Daten enthalten -- ein zweiter Lauf soll
|
|
// nichts doppeln.
|
|
func importSQLite(path string) error {
|
|
src, err := sql.Open("sqlite", "file:"+path+"?mode=ro&immutable=1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
if err := src.Ping(); err != nil {
|
|
return fmt.Errorf("sqlite öffnen: %w", err)
|
|
}
|
|
var check string
|
|
if err := src.QueryRow(`PRAGMA integrity_check`).Scan(&check); err != nil || check != "ok" {
|
|
return fmt.Errorf("sqlite integrity_check: %q %v", check, err)
|
|
}
|
|
|
|
for _, t := range importTables {
|
|
var n int64
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM ` + t.dst).Scan(&n); err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
return fmt.Errorf("zieltabelle %s ist nicht leer (%d zeilen) -- abbruch", t.dst, n)
|
|
}
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
for _, t := range importTables {
|
|
var exists int
|
|
src.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`, t.src).Scan(&exists)
|
|
if exists == 0 {
|
|
fmt.Printf("%-10s -> %-10s fehlt in sqlite, übersprungen\n", t.src, t.dst)
|
|
continue
|
|
}
|
|
|
|
n, err := copyTable(src, tx, t)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", t.src, err)
|
|
}
|
|
|
|
var got int64
|
|
if err := tx.QueryRow(`SELECT COUNT(*) FROM ` + t.dst).Scan(&got); err != nil {
|
|
return err
|
|
}
|
|
if got != n {
|
|
return fmt.Errorf("%s: %d gelesen, aber %d in postgres", t.src, n, got)
|
|
}
|
|
if t.identity {
|
|
if _, err := tx.Exec(fmt.Sprintf(
|
|
`SELECT setval(pg_get_serial_sequence('%s', 'id'), COALESCE(MAX(id), 0) + 1, false) FROM %s`,
|
|
t.dst, t.dst)); err != nil {
|
|
return fmt.Errorf("%s: sequenz: %w", t.dst, err)
|
|
}
|
|
}
|
|
fmt.Printf("%-10s -> %-10s %6d zeilen\n", t.src, t.dst, n)
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func copyTable(src *sql.DB, tx *sql.Tx, t importTable) (int64, error) {
|
|
cols := strings.Join(t.cols, ", ")
|
|
rows, err := src.Query(`SELECT ` + cols + ` FROM "` + t.src + `"`)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
ph := make([]string, len(t.cols))
|
|
for i := range ph {
|
|
ph[i] = fmt.Sprintf("$%d", i+1)
|
|
}
|
|
override := ""
|
|
if t.identity {
|
|
override = " OVERRIDING SYSTEM VALUE"
|
|
}
|
|
stmt, err := tx.Prepare(`INSERT INTO ` + t.dst + ` (` + cols + `)` + override +
|
|
` VALUES (` + strings.Join(ph, ", ") + `)`)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
var n int64
|
|
vals := make([]any, len(t.cols))
|
|
ptrs := make([]any, len(t.cols))
|
|
for i := range vals {
|
|
ptrs[i] = &vals[i]
|
|
}
|
|
for rows.Next() {
|
|
if err := rows.Scan(ptrs...); err != nil {
|
|
return n, err
|
|
}
|
|
if _, err := stmt.Exec(vals...); err != nil {
|
|
return n, fmt.Errorf("zeile %d: %w", n+1, err)
|
|
}
|
|
n++
|
|
}
|
|
return n, rows.Err()
|
|
}
|