196 lines
5.6 KiB
Go
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")
|
|
}
|