198 lines
4.9 KiB
Go
198 lines
4.9 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()
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|