// Package config parses the clip-sync TOML configuration file and applies // environment variable overrides. // // Config file location: ~/.config/clip-sync/peers.toml (overridable with // the --config flag). Environment overrides: // // CLIP_SYNC_PORT — override daemon.port // CLIP_SYNC_POLL_MS — override daemon.poll_interval_ms // CLIP_SYNC_KEY — override security.shared_key // CLIP_SYNC_MAX_BODY_BYTES — override daemon.max_body_bytes package config import ( "fmt" "os" "path/filepath" "strconv" "time" "github.com/BurntSushi/toml" ) // Config represents the full clip-sync configuration. type Config struct { Daemon DaemonConfig `toml:"daemon"` Security SecurityConfig `toml:"security"` Peers []PeerConfig `toml:"peers"` } // DaemonConfig holds daemon-level settings. type DaemonConfig struct { Port int `toml:"port"` PollIntervalMs int `toml:"poll_interval_ms"` MaxBodyBytes int64 `toml:"max_body_bytes"` HistorySize int `toml:"history_size"` Discovery bool `toml:"discovery"` Notify bool `toml:"notify"` SyncImages bool `toml:"sync_images"` ReceiveFiles bool `toml:"receive_files"` ReceiveDir string `toml:"receive_dir"` } // SecurityConfig holds peer authentication and transport security settings. type SecurityConfig struct { SharedKey string `toml:"shared_key"` AllowedOrigins []string `toml:"allowed_origins"` TLS bool `toml:"tls"` CertFile string `toml:"cert_file"` KeyFile string `toml:"key_file"` InsecureSkipVerify bool `toml:"insecure_skip_verify"` } // PeerConfig represents one remote clip-sync peer. type PeerConfig struct { Name string `toml:"name"` Addr string `toml:"addr"` TLS bool `toml:"tls"` // per-peer override of security.tls } // Default values. const ( DefaultPort = 9137 DefaultPollIntervalMs = 500 DefaultMaxBodyBytes = 10 << 20 // 10 MiB DefaultHistorySize = 50 ConfigDir = ".config/clip-sync" ConfigFile = "peers.toml" ) // DefaultConfigPath returns the default config file path (~/.config/clip-sync/peers.toml). func DefaultConfigPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("config: cannot find home directory: %w", err) } return filepath.Join(home, ConfigDir, ConfigFile), nil } // DefaultCertFile returns the default TLS certificate path. func DefaultCertFile() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("config: cannot find home directory: %w", err) } return filepath.Join(home, ConfigDir, "cert.pem"), nil } // DefaultKeyFile returns the default TLS private key path. func DefaultKeyFile() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("config: cannot find home directory: %w", err) } return filepath.Join(home, ConfigDir, "key.pem"), nil } // Load reads the config file from the default location and applies // environment variable overrides. func Load() (*Config, error) { path, err := DefaultConfigPath() if err != nil { return nil, err } return LoadFrom(path) } // LoadFrom reads the config file at the given path and applies environment // variable overrides. If the file does not exist, defaults are used. func LoadFrom(path string) (*Config, error) { cfg := &Config{ Daemon: DaemonConfig{ Port: DefaultPort, PollIntervalMs: DefaultPollIntervalMs, MaxBodyBytes: DefaultMaxBodyBytes, HistorySize: DefaultHistorySize, }, } if _, err := toml.DecodeFile(path, cfg); err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("config: cannot parse %s: %w", path, err) } // File doesn't exist — use defaults (no peers configured). } if err := applyEnvOverrides(cfg); err != nil { return nil, err } return cfg, nil } func applyEnvOverrides(cfg *Config) error { if v := os.Getenv("CLIP_SYNC_PORT"); v != "" { port, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("config: invalid CLIP_SYNC_PORT: %w", err) } cfg.Daemon.Port = port } if v := os.Getenv("CLIP_SYNC_POLL_MS"); v != "" { ms, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("config: invalid CLIP_SYNC_POLL_MS: %w", err) } cfg.Daemon.PollIntervalMs = ms } if v := os.Getenv("CLIP_SYNC_KEY"); v != "" { cfg.Security.SharedKey = v } if v := os.Getenv("CLIP_SYNC_MAX_BODY_BYTES"); v != "" { n, err := strconv.ParseInt(v, 10, 64) if err != nil { return fmt.Errorf("config: invalid CLIP_SYNC_MAX_BODY_BYTES: %w", err) } cfg.Daemon.MaxBodyBytes = n } return nil } // PollInterval returns the poll interval as a time.Duration. func (c *Config) PollInterval() time.Duration { return time.Duration(c.Daemon.PollIntervalMs) * time.Millisecond } // ListenAddr returns the HTTP listen address for this daemon. func (c *Config) ListenAddr() string { return fmt.Sprintf(":%d", c.Daemon.Port) } // MaxBodyBytes returns the effective request body size limit, clamped to a // sane minimum so a zero/negative config value never disables the limit. func (c *Config) MaxBodyBytes() int64 { if c.Daemon.MaxBodyBytes > 0 { return c.Daemon.MaxBodyBytes } return DefaultMaxBodyBytes } // ReceiveDir returns the directory where received files are written, resolving // the default (~/Downloads/clip-sync) when not configured. func (c *Config) ReceiveDir() string { if c.Daemon.ReceiveDir != "" { return c.Daemon.ReceiveDir } home, err := os.UserHomeDir() if err != nil { return "clip-sync-received" } return filepath.Join(home, "Downloads", "clip-sync") }