// Package config provides centralized configuration management for RouteWatch package config import ( "fmt" "os" "path/filepath" "runtime" "strconv" "time" ) const ( // AppIdentifier is the reverse domain name identifier for the app AppIdentifier = "berlin.sneak.app.routewatch" // dirPermissions for creating directories dirPermissions = 0750 // rwxr-x--- // defaultRouteExpirationMinutes is the default route expiration timeout in minutes defaultRouteExpirationMinutes = 5 // defaultPort is the HTTP port used when PORT is not set defaultPort = 8080 // maxPort is the highest TCP port number maxPort = 65535 ) // Config holds configuration for the entire application type Config struct { // StateDir is the directory for all application state (database, snapshots) StateDir string // Port is the TCP port the HTTP server listens on Port int // MaxRuntime is the maximum runtime (0 = run forever) MaxRuntime time.Duration // EnableBatchedDatabaseWrites enables batched database operations for better performance EnableBatchedDatabaseWrites bool // RouteExpirationTimeout is how long a route can go without being refreshed before expiring // Default is 2 hours which is conservative for BGP (typical BGP hold time is 90-180 seconds) RouteExpirationTimeout time.Duration } // New creates a new Config with default paths based on the OS func New() (*Config, error) { stateDir, err := getStateDirectory() if err != nil { return nil, fmt.Errorf("failed to determine state directory: %w", err) } port, err := getPort() if err != nil { return nil, err } return &Config{ StateDir: stateDir, Port: port, MaxRuntime: 0, // Run forever by default EnableBatchedDatabaseWrites: true, // Enable batching by default RouteExpirationTimeout: defaultRouteExpirationMinutes * time.Minute, // For active route monitoring }, nil } // GetStateDir returns the state directory path func (c *Config) GetStateDir() string { return c.StateDir } // getStateDirectory returns the appropriate state directory based on the OS func getStateDirectory() (string, error) { switch runtime.GOOS { case "darwin": // macOS: ~/Library/Application Support/berlin.sneak.app.routewatch home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, "Library", "Application Support", AppIdentifier), nil case "linux", "freebsd", "openbsd", "netbsd": // The XDG spec requires an absolute path; a relative one would put // the database somewhere unexpected. xdgData := os.Getenv("XDG_DATA_HOME") if xdgData != "" && !filepath.IsAbs(xdgData) { return "", fmt.Errorf("XDG_DATA_HOME must be an absolute path, got %q", xdgData) } // Unix-like: /var/lib/berlin.sneak.app.routewatch if root, else XDG_DATA_HOME if os.Geteuid() == 0 { return filepath.Join("/var/lib", AppIdentifier), nil } // Check XDG_DATA_HOME first if xdgData != "" { return filepath.Join(xdgData, AppIdentifier), nil } // Fall back to ~/.local/share/berlin.sneak.app.routewatch home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, ".local", "share", AppIdentifier), nil default: return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) } } // getPort returns the HTTP port from PORT, or defaultPort when PORT is not set func getPort() (int, error) { value := os.Getenv("PORT") if value == "" { return defaultPort, nil } port, err := strconv.Atoi(value) if err != nil || port < 1 || port > maxPort { return 0, fmt.Errorf("PORT must be a whole number from 1 to %d, got %q", maxPort, value) } return port, nil } // EnsureDirectories creates all necessary directories if they don't exist func (c *Config) EnsureDirectories() error { // Ensure state directory exists if err := os.MkdirAll(c.StateDir, dirPermissions); err != nil { return fmt.Errorf("failed to create state directory: %w", err) } return nil }