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 }