diff --git a/backend/build_deb.sh b/backend/build_deb.sh index 0d06758..51f9de5 100644 --- a/backend/build_deb.sh +++ b/backend/build_deb.sh @@ -285,11 +285,11 @@ User-Daten: Die installierte Binary liegt unter /opt/nsfwapp/nsfwapp. Beim Start wird sie nach ${NSFWAPP_HOME:-$HOME/nsfwapp}/nsfwapp kopiert und von dort ausgefuehrt. Dadurch bleiben recorder.log, recorder_settings.json, - recorder_settings.json, PostgreSQL-Datenordner, generated/, record/ und record/done im User-Bereich. + generated/, record/ und record/done im User-Bereich. Wichtige Laufzeitprogramme: ffmpeg/ffprobe muessen fuer Recording, Teaser, Analyse und Videofunktionen verfuegbar sein. - PostgreSQL ist die Datenbank. Fuer lokale Datenordner werden initdb/pg_ctl und fuer Migration/Backup pg_dump/pg_restore benoetigt. + PostgreSQL ist die Datenbank. Fuer Migration/Backup werden pg_dump/pg_restore benoetigt. Der Launcher erstellt beim ersten Start automatisch eine ML-Python-Umgebung unter ${TMPDIR:-/tmp}/nsfwapp-ml-venv- und installiert die mitgelieferte requirements-ml.txt. Details stehen in recorder.log. Setze AI_SERVER_PYTHON, wenn du eine eigene Python-Umgebung verwenden willst. @@ -302,7 +302,7 @@ Section: $SECTION Priority: $PRIORITY Architecture: $ARCH Maintainer: $MAINTAINER -Depends: ca-certificates, ffmpeg, python3, python3-venv, xdg-utils, libnotify-bin, procps, postgresql +Depends: ca-certificates, ffmpeg, python3, python3-venv, xdg-utils, libnotify-bin, procps, postgresql-client Description: $DESCRIPTION_SHORT $DESCRIPTION_LONG EOF diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index 0b640b1..15400ac 100644 Binary files a/backend/dist/nsfwapp-linux-amd64 and b/backend/dist/nsfwapp-linux-amd64 differ diff --git a/backend/dist/nsfwapp.exe b/backend/dist/nsfwapp.exe index 2820cfc..b180390 100644 Binary files a/backend/dist/nsfwapp.exe and b/backend/dist/nsfwapp.exe differ diff --git a/backend/dist/nsfwapp_amd64.deb b/backend/dist/nsfwapp_amd64.deb index 14f28c2..bc353f7 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/backend/postgres_local.go b/backend/postgres_local.go deleted file mode 100644 index ed57072..0000000 --- a/backend/postgres_local.go +++ /dev/null @@ -1,554 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "io" - "net" - "net/url" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "time" -) - -const ( - postgresModeURL = "url" - postgresModeLocal = "local" - - postgresDataDirName = "nsfwapp-postgres-data" - legacyPostgresDataDir = "postgres-data" - defaultPostgresDataDir = postgresDataDirName - defaultPostgresPort = 55432 - defaultPostgresDatabase = "nsfwapp" - defaultPostgresUser = "nsfwapp" -) - -func normalizePostgresMode(raw string) string { - switch strings.ToLower(strings.TrimSpace(raw)) { - case postgresModeURL: - return postgresModeURL - case postgresModeLocal: - return postgresModeLocal - default: - return postgresModeLocal - } -} - -func normalizePostgresDataDir(raw string) string { - value := strings.TrimSpace(raw) - if value == "" { - value = defaultPostgresDataDir - } - value = filepath.Clean(filepath.FromSlash(value)) - - base := filepath.Base(value) - if strings.EqualFold(base, postgresDataDirName) { - return value - } - if strings.EqualFold(base, legacyPostgresDataDir) { - parent := filepath.Dir(value) - if parent == "." || parent == string(filepath.Separator) { - return postgresDataDirName - } - return filepath.Join(parent, postgresDataDirName) - } - return filepath.Join(value, postgresDataDirName) -} - -func normalizePostgresPort(port int) int { - if port < 1 || port > 65535 { - return defaultPostgresPort - } - return port -} - -func normalizePostgresName(raw, fallback string) string { - raw = strings.TrimSpace(raw) - if raw == "" { - raw = fallback - } - - var b strings.Builder - for _, r := range raw { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { - b.WriteRune(r) - } else if b.Len() > 0 { - b.WriteByte('_') - } - } - out := strings.Trim(b.String(), "_") - if out == "" { - return fallback - } - if len(out) > 63 { - out = out[:63] - } - return out -} - -func normalizePostgresSettings(s *RecorderSettings) { - if s == nil { - return - } - s.DatabaseType = normalizeDatabaseType(s.DatabaseType) - s.DatabaseURL = strings.TrimSpace(s.DatabaseURL) - if strings.TrimSpace(s.PostgresMode) == "" && s.DatabaseURL != "" { - s.PostgresMode = postgresModeURL - } else { - s.PostgresMode = normalizePostgresMode(s.PostgresMode) - } - s.PostgresDataDir = normalizePostgresDataDir(s.PostgresDataDir) - s.PostgresPort = normalizePostgresPort(s.PostgresPort) - s.PostgresDatabase = normalizePostgresName(s.PostgresDatabase, defaultPostgresDatabase) - s.PostgresUser = normalizePostgresName(s.PostgresUser, defaultPostgresUser) -} - -func postgresSettingsKey(s RecorderSettings) string { - normalizePostgresSettings(&s) - if s.PostgresMode == postgresModeLocal { - return strings.Join([]string{ - s.PostgresMode, - filepath.ToSlash(s.PostgresDataDir), - strconv.Itoa(s.PostgresPort), - s.PostgresDatabase, - s.PostgresUser, - }, "\x00") - } - return strings.Join([]string{s.PostgresMode, strings.TrimSpace(s.DatabaseURL)}, "\x00") -} - -func buildLocalPostgresDSN(s RecorderSettings) (string, error) { - normalizePostgresSettings(&s) - dataDir, err := localPostgresDataDirAbs(s) - if err != nil { - return "", err - } - if strings.TrimSpace(dataDir) == "" { - return "", appErrorf("postgresDataDir ist leer") - } - - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - - if err := ensureLocalPostgresCluster(ctx, dataDir, s.PostgresPort, s.PostgresUser, s.PostgresDatabase); err != nil { - return "", err - } - - u := url.URL{ - Scheme: "postgres", - User: url.User(s.PostgresUser), - Host: "127.0.0.1:" + strconv.Itoa(s.PostgresPort), - Path: "/" + s.PostgresDatabase, - } - q := u.Query() - q.Set("sslmode", "disable") - u.RawQuery = q.Encode() - return u.String(), nil -} - -func ensureLocalPostgresDataDir(s RecorderSettings) (string, error) { - normalizePostgresSettings(&s) - if s.PostgresMode != postgresModeLocal { - return "", nil - } - - dataDir, err := localPostgresDataDirAbs(s) - if err != nil { - return "", err - } - if err := ensureLocalPostgresDataDirPath(dataDir); err != nil { - return "", err - } - return dataDir, nil -} - -func localPostgresDataDirAbs(s RecorderSettings) (string, error) { - normalizePostgresSettings(&s) - return resolvePathRelativeToApp(s.PostgresDataDir) -} - -func ensureLocalPostgresDataDirPath(dataDir string) error { - dataDir = filepath.Clean(strings.TrimSpace(dataDir)) - if dataDir == "" { - return appErrorf("postgresDataDir ist leer") - } - if err := os.MkdirAll(dataDir, 0755); err != nil { - return err - } - info, err := os.Stat(dataDir) - if err != nil { - return err - } - if !info.IsDir() { - return appErrorf("postgresDataDir ist kein Ordner: %s", dataDir) - } - return nil -} - -func moveLocalPostgresDataDirIfNeeded(current, next RecorderSettings) (bool, error) { - normalizePostgresSettings(¤t) - normalizePostgresSettings(&next) - if current.PostgresMode != postgresModeLocal || next.PostgresMode != postgresModeLocal { - return false, nil - } - - oldDir, err := localPostgresDataDirAbs(current) - if err != nil { - return false, err - } - newDir, err := localPostgresDataDirAbs(next) - if err != nil { - return false, err - } - oldDir = filepath.Clean(oldDir) - newDir = filepath.Clean(newDir) - if samePath(oldDir, newDir) { - return false, nil - } - - oldHasCluster, err := localPostgresDataDirHasCluster(oldDir) - if err != nil { - return false, err - } - if !oldHasCluster { - return false, nil - } - - newHasCluster, err := localPostgresDataDirHasCluster(newDir) - if err != nil { - return false, err - } - if newHasCluster { - return false, appErrorf("Ziel enthält bereits einen PostgreSQL-Datenordner: %s", newDir) - } - if exists, empty, err := dirExistsEmpty(newDir); err != nil { - return false, err - } else if exists && !empty { - return false, appErrorf("Zielordner ist nicht leer: %s", newDir) - } else if exists && empty { - if err := os.Remove(newDir); err != nil { - return false, err - } - } - - appLogln("PostgreSQL-Datenordner wird verschoben:", oldDir, "->", newDir) - if store := getModelStore(); store != nil { - _ = store.Close() - } - - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - if err := stopLocalPostgresCluster(ctx, oldDir); err != nil { - return false, err - } - - if err := moveDir(oldDir, newDir); err != nil { - return false, err - } - appLogln("PostgreSQL-Datenordner verschoben:", newDir) - return true, nil -} - -func localPostgresDataDirHasCluster(dataDir string) (bool, error) { - dataDir = filepath.Clean(strings.TrimSpace(dataDir)) - if dataDir == "" { - return false, nil - } - versionFile := filepath.Join(dataDir, "PG_VERSION") - info, err := os.Stat(versionFile) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return info != nil && !info.IsDir(), nil -} - -func dirExistsEmpty(path string) (bool, bool, error) { - info, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return false, false, nil - } - return false, false, err - } - if !info.IsDir() { - return true, false, appErrorf("Ziel ist kein Ordner: %s", path) - } - entries, err := os.ReadDir(path) - if err != nil { - return true, false, err - } - return true, len(entries) == 0, nil -} - -func samePath(a, b string) bool { - aa, errA := filepath.Abs(filepath.Clean(a)) - bb, errB := filepath.Abs(filepath.Clean(b)) - if errA == nil { - a = aa - } - if errB == nil { - b = bb - } - if strings.EqualFold(a, b) { - return true - } - return a == b -} - -func stopLocalPostgresCluster(ctx context.Context, dataDir string) error { - pgCtl, err := postgresToolPath("pg_ctl") - if err != nil { - return appErrorf("pg_ctl wurde nicht gefunden. Installiere PostgreSQL-Server-Tools oder nimm pg_ctl in PATH auf.") - } - out, err := runLocalPostgresTool(ctx, pgCtl, "-D", dataDir, "-w", "stop", "-m", "fast") - if err == nil { - return nil - } - lower := strings.ToLower(out) - if strings.Contains(lower, "no server running") || strings.Contains(lower, "pid file") && strings.Contains(lower, "does not exist") { - return nil - } - return appErrorf("lokaler PostgreSQL-Server konnte nicht gestoppt werden: %s", out) -} - -func moveDir(src, dst string) error { - src = filepath.Clean(src) - dst = filepath.Clean(dst) - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return err - } - if err := os.Rename(src, dst); err == nil { - return nil - } - - tmp := dst + ".moving-" + strconv.FormatInt(time.Now().UnixNano(), 10) - if err := copyDir(src, tmp); err != nil { - _ = os.RemoveAll(tmp) - return err - } - if err := os.Rename(tmp, dst); err != nil { - _ = os.RemoveAll(tmp) - return err - } - return os.RemoveAll(src) -} - -func copyDir(src, dst string) error { - src = filepath.Clean(src) - dst = filepath.Clean(dst) - return filepath.WalkDir(src, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - rel, err := filepath.Rel(src, path) - if err != nil { - return err - } - target := filepath.Join(dst, rel) - info, err := entry.Info() - if err != nil { - return err - } - mode := info.Mode() - if entry.IsDir() { - return os.MkdirAll(target, mode.Perm()) - } - if mode&os.ModeSymlink != 0 { - linkTarget, err := os.Readlink(path) - if err != nil { - return err - } - return os.Symlink(linkTarget, target) - } - return copyRegularFile(path, target, mode.Perm()) - }) -} - -func copyRegularFile(src, dst string, perm os.FileMode) error { - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return err - } - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) - if err != nil { - return err - } - if _, err := io.Copy(out, in); err != nil { - _ = out.Close() - return err - } - return out.Close() -} - -func ensureLocalPostgresCluster(ctx context.Context, dataDir string, port int, user, database string) error { - dataDir = filepath.Clean(strings.TrimSpace(dataDir)) - if err := ensureLocalPostgresDataDirPath(dataDir); err != nil { - return err - } - - versionFile := filepath.Join(dataDir, "PG_VERSION") - if _, err := os.Stat(versionFile); err != nil { - if !os.IsNotExist(err) { - return err - } - if err := initLocalPostgresCluster(ctx, dataDir, user); err != nil { - return err - } - } - - if err := startLocalPostgresCluster(ctx, dataDir, port); err != nil { - return err - } - return ensureLocalPostgresDatabase(ctx, port, user, database) -} - -func initLocalPostgresCluster(ctx context.Context, dataDir, user string) error { - initdb, err := postgresToolPath("initdb") - if err != nil { - return appErrorf("initdb wurde nicht gefunden. Installiere PostgreSQL-Server-Tools oder nimm initdb in PATH auf.") - } - if err := os.MkdirAll(dataDir, 0755); err != nil { - return err - } - - out, err := runLocalPostgresTool(ctx, initdb, - "-D", dataDir, - "-U", user, - "--encoding=UTF8", - "--auth-local=trust", - "--auth-host=trust", - ) - if err != nil { - return appErrorf("PostgreSQL-Datenordner konnte nicht initialisiert werden: %s", out) - } - return nil -} - -func startLocalPostgresCluster(ctx context.Context, dataDir string, port int) error { - pgCtl, err := postgresToolPath("pg_ctl") - if err != nil { - return appErrorf("pg_ctl wurde nicht gefunden. Installiere PostgreSQL-Server-Tools oder nimm pg_ctl in PATH auf.") - } - - statusOut, statusErr := runLocalPostgresTool(ctx, pgCtl, "-D", dataDir, "status") - if statusErr == nil { - return nil - } - if strings.Contains(strings.ToLower(statusOut), "server is running") || strings.Contains(strings.ToLower(statusOut), "server running") { - return nil - } - - if err := ensureLocalPostgresPortAvailable(port); err != nil { - return err - } - - logPath := filepath.Join(dataDir, "postgresql.log") - opts := fmt.Sprintf("-p %d -h 127.0.0.1", port) - out, err := runLocalPostgresTool(ctx, pgCtl, "-D", dataDir, "-l", logPath, "-o", opts, "-w", "start") - if err == nil { - return nil - } - if strings.Contains(strings.ToLower(out), "server is running") || strings.Contains(strings.ToLower(out), "server running") { - return nil - } - logTail := tailTextFile(logPath, 6000) - if logTail != "" { - appLogln("postgresql.log:", logTail) - return appErrorf("lokaler PostgreSQL-Server konnte nicht gestartet werden: %s\n\npostgresql.log:\n%s", out, logTail) - } - return appErrorf("lokaler PostgreSQL-Server konnte nicht gestartet werden: %s", out) -} - -func ensureLocalPostgresPortAvailable(port int) error { - addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) - conn, err := net.DialTimeout("tcp", addr, 300*time.Millisecond) - if err != nil { - return nil - } - _ = conn.Close() - return appErrorf("Port %d ist bereits belegt. Bitte in den Einstellungen einen anderen PostgreSQL-Port wählen.", port) -} - -func tailTextFile(path string, maxBytes int) string { - if maxBytes <= 0 { - maxBytes = 4000 - } - b, err := os.ReadFile(path) - if err != nil || len(b) == 0 { - return "" - } - if len(b) > maxBytes { - b = b[len(b)-maxBytes:] - if idx := bytes.IndexByte(b, '\n'); idx >= 0 && idx+1 < len(b) { - b = b[idx+1:] - } - } - return strings.TrimSpace(string(b)) -} - -func ensureLocalPostgresDatabase(ctx context.Context, port int, user, database string) error { - psql, err := postgresToolPath("psql") - if err != nil { - return appErrorf("psql wurde nicht gefunden. Installiere postgresql-client oder nimm psql in PATH auf.") - } - - sql := "SELECT 1 FROM pg_database WHERE datname = '" + strings.ReplaceAll(database, "'", "''") + "';" - out, err := runLocalPostgresTool(ctx, psql, - "-h", "127.0.0.1", - "-p", strconv.Itoa(port), - "-U", user, - "-d", "postgres", - "-tAc", sql, - ) - if err == nil && strings.TrimSpace(out) == "1" { - return nil - } - - createdb, err := postgresToolPath("createdb") - if err != nil { - return appErrorf("createdb wurde nicht gefunden. Installiere postgresql-client oder nimm createdb in PATH auf.") - } - out, err = runLocalPostgresTool(ctx, createdb, - "-h", "127.0.0.1", - "-p", strconv.Itoa(port), - "-U", user, - database, - ) - if err != nil { - return appErrorf("lokale PostgreSQL-Datenbank konnte nicht angelegt werden: %s", out) - } - return nil -} - -func runLocalPostgresTool(ctx context.Context, tool string, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, tool, args...) - cmd.Env = upsertEnv(os.Environ(), "PGCONNECT_TIMEOUT", "10") - hideCommandWindow(cmd) - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - - out := strings.TrimSpace(stdout.String()) - errOut := strings.TrimSpace(stderr.String()) - if errOut != "" { - if out != "" { - out += "\n" - } - out += errOut - } - return trimCommandOutput(out), err -} diff --git a/backend/postgres_settings.go b/backend/postgres_settings.go new file mode 100644 index 0000000..b088e57 --- /dev/null +++ b/backend/postgres_settings.go @@ -0,0 +1,16 @@ +package main + +import "strings" + +func normalizePostgresSettings(s *RecorderSettings) { + if s == nil { + return + } + s.DatabaseType = normalizeDatabaseType(s.DatabaseType) + s.DatabaseURL = strings.TrimSpace(s.DatabaseURL) +} + +func postgresSettingsKey(s RecorderSettings) string { + normalizePostgresSettings(&s) + return strings.TrimSpace(s.DatabaseURL) +} diff --git a/backend/routes.go b/backend/routes.go index 22d4f12..f5525d0 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -57,7 +57,6 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore { api.HandleFunc("/api/settings/cleanup", settingsCleanupHandler) api.HandleFunc("/api/settings/database/backup", settingsDatabaseBackupHandler) api.HandleFunc("/api/settings/database/restore", settingsDatabaseRestoreHandler) - api.HandleFunc("/api/settings/database/migrate", settingsDatabaseMigrateHandler) api.HandleFunc("/api/pending-autostart", handlePendingAutoStart(auth)) @@ -172,9 +171,6 @@ func buildPostgresDSNFromSettings() (string, error) { // Settings sind bereits durch loadSettings() in main() geladen. s := getSettings() normalizePostgresSettings(&s) - if s.PostgresMode == postgresModeLocal { - return buildLocalPostgresDSN(s) - } dbURL := strings.TrimSpace(s.DatabaseURL) if dbURL == "" { @@ -230,9 +226,6 @@ func buildPostgresDSNFromSettings() (string, error) { func buildPostgresDSNFromRecorderSettings(s RecorderSettings) (string, error) { normalizePostgresSettings(&s) - if s.PostgresMode == postgresModeLocal { - return buildLocalPostgresDSN(s) - } dbURL := strings.TrimSpace(s.DatabaseURL) if dbURL == "" { diff --git a/backend/settings.go b/backend/settings.go index 4a98bb9..3cf66b9 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -15,11 +15,6 @@ import ( type RecorderSettings struct { DatabaseType string `json:"databaseType"` - PostgresMode string `json:"postgresMode"` // url | local - PostgresDataDir string `json:"postgresDataDir"` - PostgresPort int `json:"postgresPort"` - PostgresDatabase string `json:"postgresDatabase"` - PostgresUser string `json:"postgresUser"` DatabaseURL string `json:"databaseUrl"` EncryptedDBPassword string `json:"encryptedDbPassword,omitempty"` // base64(nonce+ciphertext) @@ -72,11 +67,6 @@ var ( settingsMu sync.Mutex settings = RecorderSettings{ DatabaseType: databaseTypePostgres, - PostgresMode: postgresModeLocal, - PostgresDataDir: defaultPostgresDataDir, - PostgresPort: defaultPostgresPort, - PostgresDatabase: defaultPostgresDatabase, - PostgresUser: defaultPostgresUser, DatabaseURL: "", EncryptedDBPassword: "", @@ -291,14 +281,9 @@ type RecorderSettingsPublic struct { DoneDir string `json:"doneDir"` FFmpegPath string `json:"ffmpegPath"` - DatabaseType string `json:"databaseType"` - PostgresMode string `json:"postgresMode"` - PostgresDataDir string `json:"postgresDataDir"` - PostgresPort int `json:"postgresPort"` - PostgresDatabase string `json:"postgresDatabase"` - PostgresUser string `json:"postgresUser"` - DatabaseURL string `json:"databaseUrl"` - HasDBPassword bool `json:"hasDbPassword"` + DatabaseType string `json:"databaseType"` + DatabaseURL string `json:"databaseUrl"` + HasDBPassword bool `json:"hasDbPassword"` AutoAddToDownloadList bool `json:"autoAddToDownloadList"` AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"` @@ -342,14 +327,9 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic { DoneDir: s.DoneDir, FFmpegPath: s.FFmpegPath, - DatabaseType: normalizeDatabaseType(s.DatabaseType), - PostgresMode: s.PostgresMode, - PostgresDataDir: s.PostgresDataDir, - PostgresPort: s.PostgresPort, - PostgresDatabase: s.PostgresDatabase, - PostgresUser: s.PostgresUser, - DatabaseURL: strings.TrimSpace(s.DatabaseURL), - HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "", + DatabaseType: normalizeDatabaseType(s.DatabaseType), + DatabaseURL: strings.TrimSpace(s.DatabaseURL), + HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "", AutoAddToDownloadList: s.AutoAddToDownloadList, AutoStartAddedDownloads: s.AutoStartAddedDownloads, @@ -491,7 +471,7 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { in.DatabaseType = normalizeDatabaseType(in.DatabaseType) normalizePostgresSettings(&in.RecorderSettings) in.DatabaseURL = strings.TrimSpace(in.DatabaseURL) - if in.PostgresMode == postgresModeURL && in.DatabaseURL == "" { + if in.DatabaseURL == "" { http.Error(w, "Database URL fehlt", http.StatusBadRequest) return } @@ -535,11 +515,6 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { next.FFmpegPath = in.FFmpegPath next.DatabaseType = in.DatabaseType - next.PostgresMode = in.PostgresMode - next.PostgresDataDir = in.PostgresDataDir - next.PostgresPort = in.PostgresPort - next.PostgresDatabase = in.PostgresDatabase - next.PostgresUser = in.PostgresUser next.DatabaseURL = in.DatabaseURL next.EncryptedDBPassword = in.EncryptedDBPassword @@ -577,24 +552,11 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { next.TrainingRecognitionEnabled = in.TrainingRecognitionEnabled next.TrainingDetectorEpochs = in.TrainingDetectorEpochs - postgresDataMoved, err := moveLocalPostgresDataDirIfNeeded(current, next) - if err != nil { - http.Error(w, "PostgreSQL-Datenordner konnte nicht verschoben werden: "+err.Error(), http.StatusBadRequest) - return - } - if next.PostgresMode == postgresModeLocal && !postgresDataMoved { - if _, err := ensureLocalPostgresDataDir(next); err != nil { - http.Error(w, "konnte PostgreSQL-Datenordner nicht erstellen: "+err.Error(), http.StatusBadRequest) - return - } - } - dbChanged := normalizeDatabaseType(next.DatabaseType) != normalizeDatabaseType(current.DatabaseType) || postgresSettingsKey(next) != postgresSettingsKey(current) || strings.TrimSpace(next.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) || - strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword) || - postgresDataMoved + strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword) var newStore *ModelStore @@ -663,8 +625,8 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { func settingsBrowse(w http.ResponseWriter, r *http.Request) { target := r.URL.Query().Get("target") - if target != "record" && target != "done" && target != "ffmpeg" && target != "postgresData" { - http.Error(w, "target muss record, done, ffmpeg oder postgresData sein", http.StatusBadRequest) + if target != "record" && target != "done" && target != "ffmpeg" { + http.Error(w, "target muss record, done oder ffmpeg sein", http.StatusBadRequest) return } @@ -678,10 +640,6 @@ func settingsBrowse(w http.ResponseWriter, r *http.Request) { p, err = dialog.File(). Title("ffmpeg.exe auswählen"). Load() - } else if target == "postgresData" { - p, err = dialog.Directory(). - Title("PostgreSQL-Datenordner auswaehlen"). - Browse() } else { // Ordnerauswahl für record/done p, err = dialog.Directory(). diff --git a/backend/settings_database_migrate.go b/backend/settings_database_migrate.go deleted file mode 100644 index 52889f3..0000000 --- a/backend/settings_database_migrate.go +++ /dev/null @@ -1,242 +0,0 @@ -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(¤t) - 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 -} diff --git a/frontend/src/components/ui/RecorderSettings.tsx b/frontend/src/components/ui/RecorderSettings.tsx index 1c98228..fed7213 100644 --- a/frontend/src/components/ui/RecorderSettings.tsx +++ b/frontend/src/components/ui/RecorderSettings.tsx @@ -14,11 +14,6 @@ import { startRegistration } from '@simplewebauthn/browser' type RecorderSettings = { databaseType?: 'postgres' - postgresMode?: 'url' | 'local' - postgresDataDir?: string - postgresPort?: number - postgresDatabase?: string - postgresUser?: string databaseUrl?: string hasDbPassword?: boolean recordDir: string @@ -284,16 +279,8 @@ function SettingsSection(props: { ) } -const POSTGRES_DATA_DIR_NAME = 'nsfwapp-postgres-data' -const LEGACY_POSTGRES_DATA_DIR_NAME = 'postgres-data' - const DEFAULTS: RecorderSettings = { databaseType: 'postgres', - postgresMode: 'local', - postgresDataDir: POSTGRES_DATA_DIR_NAME, - postgresPort: 55432, - postgresDatabase: 'nsfwapp', - postgresUser: 'nsfwapp', databaseUrl: '', hasDbPassword: false, recordDir: 'record', @@ -553,53 +540,8 @@ function normalizeDatabaseType(_value: unknown): 'postgres' { return 'postgres' } -function normalizePostgresMode(value: unknown, databaseUrl?: unknown): 'url' | 'local' { - const mode = String(value ?? '').trim().toLowerCase() - if (mode === 'url') return 'url' - if (mode === 'local') return 'local' - return String(databaseUrl ?? '').trim() ? 'url' : 'local' -} - -function normalizePostgresDataDir(value: unknown) { - let s = String(value ?? '').trim() - if (!s) return POSTGRES_DATA_DIR_NAME - - s = s.replace(/[\\/]+$/g, '') - if (!s) return POSTGRES_DATA_DIR_NAME - - const sep = s.includes('\\') && !s.includes('/') ? '\\' : '/' - const parts = s.split(/[\\/]+/) - const base = parts[parts.length - 1] || '' - if (base.toLowerCase() === POSTGRES_DATA_DIR_NAME.toLowerCase()) return s - if (base.toLowerCase() === LEGACY_POSTGRES_DATA_DIR_NAME.toLowerCase()) { - const parent = parts.slice(0, -1).join(sep) - return parent ? `${parent}${sep}${POSTGRES_DATA_DIR_NAME}` : POSTGRES_DATA_DIR_NAME - } - return `${s}${sep}${POSTGRES_DATA_DIR_NAME}` -} - -function normalizePostgresPort(value: unknown) { - const n = Math.floor(Number(value)) - return Number.isFinite(n) && n >= 1 && n <= 65535 ? n : 55432 -} - -function normalizePostgresName(value: unknown, fallback: string) { - const s = String(value ?? '').trim() - return s || fallback -} - function postgresSourceKey(v: any) { - const mode = normalizePostgresMode(v?.postgresMode, v?.databaseUrl) - if (mode === 'local') { - return [ - mode, - normalizePostgresDataDir(v?.postgresDataDir), - normalizePostgresPort(v?.postgresPort), - normalizePostgresName(v?.postgresDatabase, 'nsfwapp'), - normalizePostgresName(v?.postgresUser, 'nsfwapp'), - ].join('\u0000') - } - return [mode, String(v?.databaseUrl ?? '').trim()].join('\u0000') + return String(v?.databaseUrl ?? '').trim() } function formatDatabaseBackupBytes(bytes: unknown) { @@ -743,7 +685,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { const saveSuccessTimerRef = useRef(null) const appLogPreRef = useRef(null) const dbRestoreInputRef = useRef(null) - const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | 'postgresData' | null>(null) + const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null) const [msg, setMsg] = useState(null) const [err, setErr] = useState(null) const [diskStatus, setDiskStatus] = useState(null) @@ -753,45 +695,26 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { const [appLogOpen, setAppLogOpen] = useState(false) const [dbModalOpen, setDbModalOpen] = useState(false) const [loadedDatabaseType, setLoadedDatabaseType] = useState<'postgres'>('postgres') - const [loadedPostgresMode, setLoadedPostgresMode] = useState<'url' | 'local'>('local') - const [loadedPostgresDataDir, setLoadedPostgresDataDir] = useState(POSTGRES_DATA_DIR_NAME) - const [loadedPostgresPort, setLoadedPostgresPort] = useState(55432) - const [loadedPostgresDatabase, setLoadedPostgresDatabase] = useState('nsfwapp') - const [loadedPostgresUser, setLoadedPostgresUser] = useState('nsfwapp') const [loadedDatabaseUrl, setLoadedDatabaseUrl] = useState('') const [pendingDbPassword, setPendingDbPassword] = useState('') // wird nur beim Speichern gesendet const [dbBackupBusy, setDbBackupBusy] = useState(false) const [dbRestoreBusy, setDbRestoreBusy] = useState(false) - const [dbMigrationBusy, setDbMigrationBusy] = useState(false) const [dbMaintenanceMsg, setDbMaintenanceMsg] = useState(null) const [dbMaintenanceErr, setDbMaintenanceErr] = useState(null) const now = Date.now() const saveSucceeded = saveSuccessUntilMs > now const saveFailed = Boolean(err) && !saving && !saveSucceeded - const currentPostgresMode = normalizePostgresMode((value as any).postgresMode, (value as any).databaseUrl) - const currentDatabaseUrl = String((value as any).databaseUrl ?? '').trim() const loadedPostgresSourceKey = postgresSourceKey({ databaseType: loadedDatabaseType, databaseUrl: loadedDatabaseUrl, - postgresMode: loadedPostgresMode, - postgresDataDir: loadedPostgresDataDir, - postgresPort: loadedPostgresPort, - postgresDatabase: loadedPostgresDatabase, - postgresUser: loadedPostgresUser, }) const currentPostgresSourceKey = postgresSourceKey(value) const databaseLocationDirty = currentPostgresSourceKey !== loadedPostgresSourceKey const databaseSettingsDirty = databaseLocationDirty || Boolean((pendingDbPassword || '').trim()) - const savedDatabaseConfigured = - loadedPostgresMode === 'local' ? Boolean(loadedPostgresDataDir) : Boolean(loadedDatabaseUrl) - const targetDatabaseConfigured = - currentPostgresMode === 'local' - ? Boolean(normalizePostgresDataDir((value as any).postgresDataDir)) - : Boolean(currentDatabaseUrl) + const savedDatabaseConfigured = Boolean(loadedDatabaseUrl) const databaseConfigured = savedDatabaseConfigured - const canMigrateDatabase = savedDatabaseConfigured && targetDatabaseConfigured && databaseLocationDirty - const dbMaintenanceBusy = dbBackupBusy || dbRestoreBusy || dbMigrationBusy + const dbMaintenanceBusy = dbBackupBusy || dbRestoreBusy const [authStatus, setAuthStatus] = useState(null) const [securityBusy, setSecurityBusy] = useState(false) @@ -930,18 +853,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { .then((data: RecorderSettings) => { if (!alive) return const databaseType = normalizeDatabaseType((data as any).databaseType) - const postgresMode = normalizePostgresMode((data as any).postgresMode, (data as any).databaseUrl) - const postgresDataDir = normalizePostgresDataDir((data as any).postgresDataDir) - const postgresPort = normalizePostgresPort((data as any).postgresPort) - const postgresDatabase = normalizePostgresName((data as any).postgresDatabase, 'nsfwapp') - const postgresUser = normalizePostgresName((data as any).postgresUser, 'nsfwapp') setValue({ databaseType, - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, databaseUrl: String((data as any).databaseUrl ?? ''), hasDbPassword: Boolean((data as any).hasDbPassword ?? false), recordDir: (data.recordDir || DEFAULTS.recordDir).toString(), @@ -993,11 +906,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { (data as any).trainingDetectorEpochs ?? DEFAULTS.trainingDetectorEpochs, }) setLoadedDatabaseType(databaseType) - setLoadedPostgresMode(postgresMode) - setLoadedPostgresDataDir(postgresDataDir) - setLoadedPostgresPort(postgresPort) - setLoadedPostgresDatabase(postgresDatabase) - setLoadedPostgresUser(postgresUser) setLoadedDatabaseUrl(String((data as any).databaseUrl ?? '').trim()) }) .catch(() => { @@ -1314,7 +1222,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { scrollAppLogToBottom() }, [appLogOpen, appLog?.log, appLogLoading]) - async function browse(target: 'record' | 'done' | 'ffmpeg' | 'postgresData') { + async function browse(target: 'record' | 'done' | 'ffmpeg') { setErr(null) setMsg(null) setBrowsing(target) @@ -1335,7 +1243,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { setValue((v) => { if (target === 'record') return { ...v, recordDir: p } if (target === 'done') return { ...v, doneDir: p } - if (target === 'postgresData') return { ...v, postgresMode: 'local', postgresDataDir: normalizePostgresDataDir(p) } return { ...v, ffmpegPath: p } }) } catch (e: any) { @@ -1436,11 +1343,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { new CustomEvent('models-db-changed', { detail: { databaseType: loadedDatabaseType, - postgresMode: loadedPostgresMode, - postgresDataDir: loadedPostgresDataDir, - postgresPort: loadedPostgresPort, - postgresDatabase: loadedPostgresDatabase, - postgresUser: loadedPostgresUser, databaseUrl: loadedDatabaseUrl, restored: true, }, @@ -1453,108 +1355,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { } } - async function migrateDatabase() { - setDbMaintenanceMsg(null) - setDbMaintenanceErr(null) - - const postgresMode = currentPostgresMode - const postgresDataDir = normalizePostgresDataDir((value as any).postgresDataDir) - const postgresPort = normalizePostgresPort((value as any).postgresPort) - const postgresDatabase = normalizePostgresName((value as any).postgresDatabase, 'nsfwapp') - const postgresUser = normalizePostgresName((value as any).postgresUser, 'nsfwapp') - const databaseUrl = currentDatabaseUrl - - if (!savedDatabaseConfigured) { - setDbMaintenanceErr('Bitte zuerst eine Quell-Datenbank speichern.') - return - } - if (!targetDatabaseConfigured) { - setDbMaintenanceErr('Bitte zuerst die Ziel-Datenbank angeben.') - return - } - if (!databaseLocationDirty) { - setDbMaintenanceErr('Quelle und Ziel sind identisch.') - return - } - - const targetLabel = - postgresMode === 'local' - ? `lokalen Datenordner "${postgresDataDir}"` - : `Datenbank "${databaseUrl}"` - const ok = window.confirm( - `Aktive PostgreSQL-Datenbank nach ${targetLabel} migrieren?\n\n` + - 'Die Ziel-Datenbank wird mit den aktuellen Model-Daten überschrieben. Danach nutzt die App das Ziel.' - ) - if (!ok) return - - setDbMigrationBusy(true) - try { - const res = await fetch('/api/settings/database/migrate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - cache: 'no-store', - body: JSON.stringify({ - databaseType: 'postgres', - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, - databaseUrl, - dbPassword: pendingDbPassword || undefined, - }), - }) - if (!res.ok) { - const t = await res.text().catch(() => '') - throw new Error(t || `HTTP ${res.status}`) - } - - const data = await res.json().catch(() => null) - const size = formatDatabaseBackupBytes(data?.bytes ?? 0) - setDbMaintenanceMsg(size ? `Migration abgeschlossen (${size}).` : 'Migration abgeschlossen.') - - setLoadedDatabaseType('postgres') - setLoadedPostgresMode(postgresMode) - setLoadedPostgresDataDir(postgresDataDir) - setLoadedPostgresPort(postgresPort) - setLoadedPostgresDatabase(postgresDatabase) - setLoadedPostgresUser(postgresUser) - setLoadedDatabaseUrl(databaseUrl) - setPendingDbPassword('') - setValue((v) => ({ - ...v, - databaseType: 'postgres', - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, - databaseUrl, - })) - - window.dispatchEvent(new CustomEvent('recorder-settings-updated')) - window.dispatchEvent(new Event('models-changed')) - window.dispatchEvent( - new CustomEvent('models-db-changed', { - detail: { - databaseType: 'postgres', - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, - databaseUrl, - migrated: true, - }, - }) - ) - } catch (e: any) { - setDbMaintenanceErr(e?.message ?? String(e)) - } finally { - setDbMigrationBusy(false) - } - } - async function save() { setErr(null) setMsg(null) @@ -1563,11 +1363,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { const doneDir = value.doneDir.trim() const ffmpegPath = (value.ffmpegPath ?? '').trim() const databaseType = normalizeDatabaseType((value as any).databaseType) - const postgresMode = normalizePostgresMode((value as any).postgresMode, (value as any).databaseUrl) - const postgresDataDir = normalizePostgresDataDir((value as any).postgresDataDir) - const postgresPort = normalizePostgresPort((value as any).postgresPort) - const postgresDatabase = normalizePostgresName((value as any).postgresDatabase, 'nsfwapp') - const postgresUser = normalizePostgresName((value as any).postgresUser, 'nsfwapp') const databaseUrl = String((value as any).databaseUrl ?? '').trim() const hadPendingDbPassword = Boolean((pendingDbPassword || '').trim()) @@ -1575,14 +1370,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { setErr('Bitte Aufnahme-Ordner und Ziel-Ordner angeben.') return } - if (postgresMode === 'url' && !databaseUrl) { + if (!databaseUrl) { setErr('Bitte Database URL angeben.') return } - if (postgresMode === 'local' && !postgresDataDir) { - setErr('Bitte PostgreSQL-Datenordner angeben.') - return - } // ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist const autoAddToDownloadList = !!value.autoAddToDownloadList @@ -1635,11 +1426,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ databaseType, - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, databaseUrl, dbPassword: pendingDbPassword || undefined, recordDir, @@ -1691,22 +1477,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { setValue((v) => ({ ...v, databaseType, - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, databaseUrl, })) const databaseChanged = postgresSourceKey({ databaseType, - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, databaseUrl, }) !== loadedPostgresSourceKey || hadPendingDbPassword @@ -1716,21 +1492,11 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) { new CustomEvent('models-db-changed', { detail: { databaseType, - postgresMode, - postgresDataDir, - postgresPort, - postgresDatabase, - postgresUser, databaseUrl, }, }) ) setLoadedDatabaseType(databaseType) - setLoadedPostgresMode(postgresMode) - setLoadedPostgresDataDir(postgresDataDir) - setLoadedPostgresPort(postgresPort) - setLoadedPostgresDatabase(postgresDatabase) - setLoadedPostgresUser(postgresUser) setLoadedDatabaseUrl(databaseUrl) setPendingDbPassword('') } @@ -2403,44 +2169,11 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
Datenbank
- PostgreSQL als externe Datenbank oder lokal verwalteter Datenordner. + PostgreSQL-Verbindung über eine Database URL.
-
- - -
-
- {([ - ['url', 'Datenbank'], - ['local', 'Datenordner'], - ] as const).map(([mode, label]) => { - const active = currentPostgresMode === mode - return ( - - ) - })} -
-
-
- - {currentPostgresMode === 'url' ? (
- ) : ( -
-
- - -
- setValue((v) => ({ ...v, postgresMode: 'local', postgresDataDir: e.target.value }))} - placeholder={POSTGRES_DATA_DIR_NAME} - className="min-w-0 flex-1 rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200 - focus:outline-none focus:ring-2 focus:ring-indigo-500 - dark:bg-white/10 dark:text-white dark:ring-white/10" - /> - -
-
- -
- - -
- setValue((v) => ({ ...v, postgresMode: 'local', postgresPort: Number(e.target.value || 55432) }))} - placeholder="Port" - className="rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200 - focus:outline-none focus:ring-2 focus:ring-indigo-500 - dark:bg-white/10 dark:text-white dark:ring-white/10" - /> - setValue((v) => ({ ...v, postgresMode: 'local', postgresDatabase: e.target.value }))} - placeholder="Datenbank" - className="rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200 - focus:outline-none focus:ring-2 focus:ring-indigo-500 - dark:bg-white/10 dark:text-white dark:ring-white/10" - /> - setValue((v) => ({ ...v, postgresMode: 'local', postgresUser: e.target.value }))} - placeholder="User" - className="rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200 - focus:outline-none focus:ring-2 focus:ring-indigo-500 - dark:bg-white/10 dark:text-white dark:ring-white/10" - /> -
-
-
- )}
@@ -2538,16 +2212,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
- -