diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index a47741b..fe49989 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 be4be5d..4c59e62 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 ecfddff..7139c16 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/backend/postgres_database.go b/backend/postgres_database.go new file mode 100644 index 0000000..2af3695 --- /dev/null +++ b/backend/postgres_database.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/url" + "strings" + "time" +) + +func ensurePostgresDatabaseExists(dsn string) error { + dsn = strings.TrimSpace(dsn) + if dsn == "" { + return errors.New("databaseUrl ist leer") + } + + u, err := url.Parse(dsn) + if err != nil { + return fmt.Errorf("databaseUrl ungueltig: %w", err) + } + + dbName, err := postgresDatabaseNameFromURL(u) + if err != nil { + return err + } + + if err := pingPostgresDSN(u.String()); err == nil { + return nil + } else if !isMissingPostgresDatabaseError(err) { + return err + } + + var lastErr error + for _, maintenanceDB := range []string{"postgres", "template1"} { + if strings.EqualFold(maintenanceDB, dbName) { + continue + } + + maintenanceURL := *u + maintenanceURL.Path = "/" + maintenanceDB + maintenanceURL.RawPath = "" + + if err := createPostgresDatabaseIfMissing(maintenanceURL.String(), dbName); err != nil { + lastErr = err + continue + } + + return pingPostgresDSN(u.String()) + } + + if lastErr != nil { + return lastErr + } + return fmt.Errorf("Datenbank %s existiert nicht und konnte nicht angelegt werden", dbName) +} + +func postgresDatabaseNameFromURL(u *url.URL) (string, error) { + if u == nil { + return "", errors.New("databaseUrl ist leer") + } + + switch strings.ToLower(strings.TrimSpace(u.Scheme)) { + case "postgres", "postgresql": + default: + return "", errors.New("databaseUrl muss postgres:// oder postgresql:// verwenden") + } + + dbName, err := url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/")) + if err != nil { + return "", fmt.Errorf("Datenbankname ungueltig: %w", err) + } + dbName = strings.TrimSpace(dbName) + if dbName == "" { + return "", errors.New("Datenbankname fehlt in databaseUrl") + } + if strings.ContainsRune(dbName, 0) { + return "", errors.New("Datenbankname enthaelt ungueltige Zeichen") + } + return dbName, nil +} + +func pingPostgresDSN(dsn string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + defer db.Close() + + return db.PingContext(ctx) +} + +func createPostgresDatabaseIfMissing(maintenanceDSN string, dbName string) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + db, err := sql.Open("pgx", maintenanceDSN) + if err != nil { + return err + } + defer db.Close() + + if err := db.PingContext(ctx); err != nil { + return err + } + + var exists bool + if err := db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname=$1)`, dbName).Scan(&exists); err != nil { + return err + } + if exists { + return nil + } + + _, err = db.ExecContext(ctx, `CREATE DATABASE `+quotePostgresIdentifier(dbName)) + if err != nil && isDuplicatePostgresDatabaseError(err) { + return nil + } + return err +} + +func quotePostgresIdentifier(v string) string { + return `"` + strings.ReplaceAll(v, `"`, `""`) + `"` +} + +func isMissingPostgresDatabaseError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "sqlstate 3d000") || + strings.Contains(msg, "sqlstate 30000") || + strings.Contains(msg, "database") && strings.Contains(msg, "does not exist") || + strings.Contains(msg, "datenbank") && strings.Contains(msg, "existiert nicht") +} + +func isDuplicatePostgresDatabaseError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "sqlstate 42p04") || + strings.Contains(msg, "already exists") || + strings.Contains(msg, "existiert bereits") +} diff --git a/backend/routes.go b/backend/routes.go index f5525d0..75a1922 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -130,6 +130,10 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore { } appLogln("Models DB (Postgres):", sanitizeDSNForLog(storeConfig.DSN)) + if err := ensurePostgresDatabaseExists(storeConfig.DSN); err != nil { + appLogln("models DB ensure:", err) + } + store := NewModelStore(storeConfig) if err := store.Load(); err != nil { appLogln("⚠️ models load:", err) diff --git a/backend/settings.go b/backend/settings.go index 3cf66b9..3e9a63d 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -568,6 +568,11 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { return } + if err := ensurePostgresDatabaseExists(storeConfig.DSN); err != nil { + http.Error(w, "Datenbank konnte nicht angelegt/geprueft werden: "+err.Error(), http.StatusBadRequest) + return + } + newStore = NewModelStore(storeConfig) if err := newStore.Load(); err != nil { _ = newStore.Close()