nsfwapp/backend/settings_database_migrate.go
2026-06-24 10:00:03 +02:00

243 lines
6.3 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func settingsDatabaseMigrateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
var in RecorderSettingsIn
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json: "+err.Error(), http.StatusBadRequest)
return
}
current := getSettings()
normalizePostgresSettings(&current)
target := current
target.DatabaseType = databaseTypePostgres
target.PostgresMode = in.PostgresMode
target.PostgresDataDir = in.PostgresDataDir
target.PostgresPort = in.PostgresPort
target.PostgresDatabase = in.PostgresDatabase
target.PostgresUser = in.PostgresUser
target.DatabaseURL = strings.TrimSpace(in.DatabaseURL)
normalizePostgresSettings(&target)
if target.PostgresMode == postgresModeURL && target.DatabaseURL == "" {
http.Error(w, "Database URL fehlt", http.StatusBadRequest)
return
}
if current.PostgresMode == postgresModeLocal && target.PostgresMode == postgresModeLocal {
moved, err := moveLocalPostgresDataDirIfNeeded(current, target)
if err != nil {
http.Error(w, "PostgreSQL-Datenordner konnte nicht verschoben werden: "+err.Error(), http.StatusInternalServerError)
return
}
if !moved {
if _, err := ensureLocalPostgresDataDir(target); err != nil {
http.Error(w, "konnte PostgreSQL-Datenordner nicht erstellen: "+err.Error(), http.StatusInternalServerError)
return
}
}
targetDSN, err := buildPostgresDSNFromRecorderSettings(target)
if err != nil {
http.Error(w, "Ziel-Datenbank ungueltig: "+err.Error(), http.StatusBadRequest)
return
}
newStore := NewModelStore(ModelStoreConfig{Type: databaseTypePostgres, DSN: targetDSN})
if err := newStore.Load(); err != nil {
_ = newStore.Close()
http.Error(w, "Ziel-Datenbank konnte nach Verschieben nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
settingsMu.Lock()
settings = target
settingsMu.Unlock()
saveSettingsToDisk()
setModelStore(newStore)
setCoverModelStore(newStore)
setChaturbateOnlineModelStore(newStore)
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"bytes": 0,
"mode": target.PostgresMode,
"moved": moved,
})
return
}
sanitizedURL, pwFromURL := stripPasswordFromPostgresURL(target.DatabaseURL)
pwFromURL = strings.TrimSpace(pwFromURL)
if pwFromURL == "****" {
pwFromURL = ""
}
if sanitizedURL != "" {
target.DatabaseURL = sanitizedURL
}
plainPW := strings.TrimSpace(in.DBPassword)
if plainPW == "" {
plainPW = pwFromURL
}
if plainPW != "" {
enc, err := encryptSettingString(plainPW)
if err != nil {
http.Error(w, "konnte DB-Passwort nicht verschluesseln: "+err.Error(), http.StatusInternalServerError)
return
}
target.EncryptedDBPassword = enc
}
sourceDSN, err := buildPostgresDSNFromRecorderSettings(current)
if err != nil {
http.Error(w, "Quell-Datenbank ungueltig: "+err.Error(), http.StatusBadRequest)
return
}
targetDSN, err := buildPostgresDSNFromRecorderSettings(target)
if err != nil {
http.Error(w, "Ziel-Datenbank ungueltig: "+err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(sourceDSN) == strings.TrimSpace(targetDSN) ||
sanitizeDSNForLog(sourceDSN) == sanitizeDSNForLog(targetDSN) {
http.Error(w, "Quelle und Ziel sind identisch", http.StatusBadRequest)
return
}
sourceDBURL, sourceEnv, err := postgresToolConnection(sourceDSN)
if err != nil {
http.Error(w, "Quell-Datenbank ungueltig: "+err.Error(), http.StatusBadRequest)
return
}
targetDBURL, targetEnv, err := postgresToolConnection(targetDSN)
if err != nil {
http.Error(w, "Ziel-Datenbank ungueltig: "+err.Error(), http.StatusBadRequest)
return
}
tmp, err := os.CreateTemp("", "nsfwapp-db-migrate-*.dump")
if err != nil {
http.Error(w, "Migration konnte nicht vorbereitet werden: "+err.Error(), http.StatusInternalServerError)
return
}
tmpPath := tmp.Name()
_ = tmp.Close()
defer os.Remove(tmpPath)
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Minute)
defer cancel()
if err := dumpPostgresDatabase(ctx, sourceDBURL, sourceEnv, tmpPath); err != nil {
http.Error(w, "pg_dump fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
return
}
if err := restorePostgresDatabase(ctx, targetDBURL, targetEnv, tmpPath); err != nil {
http.Error(w, "pg_restore fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
return
}
newStore := NewModelStore(ModelStoreConfig{Type: databaseTypePostgres, DSN: targetDSN})
if err := newStore.Load(); err != nil {
_ = newStore.Close()
http.Error(w, "Ziel-Datenbank konnte nach Migration nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
settingsMu.Lock()
settings = target
settingsMu.Unlock()
saveSettingsToDisk()
setModelStore(newStore)
setCoverModelStore(newStore)
setChaturbateOnlineModelStore(newStore)
info, _ := os.Stat(tmpPath)
var bytesWritten int64
if info != nil {
bytesWritten = info.Size()
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"bytes": bytesWritten,
"mode": target.PostgresMode,
})
}
func dumpPostgresDatabase(ctx context.Context, dbURL string, env []string, outPath string) error {
toolPath, err := postgresToolPath("pg_dump")
if err != nil {
return err
}
out, err := os.Create(outPath)
if err != nil {
return err
}
cmd := exec.CommandContext(
ctx,
toolPath,
"--format=custom",
"--blobs",
"--no-owner",
"--no-privileges",
"--dbname="+dbURL,
)
cmd.Env = env
cmd.Stdout = out
hideCommandWindow(cmd)
var stderr bytes.Buffer
cmd.Stderr = &stderr
runErr := cmd.Run()
closeErr := out.Close()
if runErr != nil {
msg := trimCommandOutput(stderr.String())
if msg == "" {
msg = runErr.Error()
}
return appErrorf("%s", msg)
}
return closeErr
}
func restorePostgresDatabase(ctx context.Context, dbURL string, env []string, dumpPath string) error {
stderr, err := runPostgresTool(
ctx,
"pg_restore",
postgresToolConfig{dbURL: dbURL, env: env},
"--clean",
"--if-exists",
"--no-owner",
"--no-privileges",
"--single-transaction",
"--dbname="+dbURL,
filepath.Clean(dumpPath),
)
if err != nil {
return appErrorf("%s", stderr)
}
return nil
}