Files

196 lines
5.6 KiB
Go

// 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")
}