feat: implement security, discovery, history, file transfer, web UI (P0-P3)
This commit is contained in:
+124
-19
@@ -1,10 +1,13 @@
|
||||
// Package config parses the clip-sync TOML configuration file and applies
|
||||
// environment variable overrides.
|
||||
//
|
||||
// Config file location: ~/.config/clip-sync/peers.toml
|
||||
// Environment overrides:
|
||||
// CLIP_SYNC_PORT — override daemon.port
|
||||
// CLIP_SYNC_POLL_MS — override daemon.poll_interval_ms
|
||||
// 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 (
|
||||
@@ -19,59 +22,118 @@ import (
|
||||
|
||||
// Config represents the full clip-sync configuration.
|
||||
type Config struct {
|
||||
Daemon DaemonConfig `toml:"daemon"`
|
||||
Peers []PeerConfig `toml:"peers"`
|
||||
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"`
|
||||
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"`
|
||||
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"
|
||||
)
|
||||
|
||||
// Load reads the config file from ~/.config/clip-sync/peers.toml and applies
|
||||
// environment variable overrides.
|
||||
func Load() (*Config, error) {
|
||||
// DefaultConfigPath returns the default config file path (~/.config/clip-sync/peers.toml).
|
||||
func DefaultConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: cannot find home directory: %w", err)
|
||||
return "", fmt.Errorf("config: cannot find home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ConfigDir, ConfigFile), nil
|
||||
}
|
||||
|
||||
cfgPath := filepath.Join(home, ConfigDir, ConfigFile)
|
||||
// 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(cfgPath, cfg); err != nil {
|
||||
if _, err := toml.DecodeFile(path, cfg); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config: cannot parse %s: %w", cfgPath, err)
|
||||
return nil, fmt.Errorf("config: cannot parse %s: %w", path, err)
|
||||
}
|
||||
// File doesn't exist — use defaults (no peers configured).
|
||||
}
|
||||
|
||||
// Environment variable overrides.
|
||||
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 nil, fmt.Errorf("config: invalid CLIP_SYNC_PORT: %w", err)
|
||||
return fmt.Errorf("config: invalid CLIP_SYNC_PORT: %w", err)
|
||||
}
|
||||
cfg.Daemon.Port = port
|
||||
}
|
||||
@@ -79,12 +141,24 @@ func Load() (*Config, error) {
|
||||
if v := os.Getenv("CLIP_SYNC_POLL_MS"); v != "" {
|
||||
ms, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: invalid CLIP_SYNC_POLL_MS: %w", err)
|
||||
return fmt.Errorf("config: invalid CLIP_SYNC_POLL_MS: %w", err)
|
||||
}
|
||||
cfg.Daemon.PollIntervalMs = ms
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
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.
|
||||
@@ -96,3 +170,34 @@ func (c *Config) PollInterval() time.Duration {
|
||||
func (c *Config) ListenAddr() string {
|
||||
return fmt.Sprintf(":%d", c.Daemon.Port)
|
||||
}
|
||||
|
||||
// PeerUsesTLS reports whether the given peer should be contacted over TLS.
|
||||
// A per-peer override wins over the global security.tls setting.
|
||||
func (c *Config) PeerUsesTLS(p PeerConfig) bool {
|
||||
if p.TLS {
|
||||
return true
|
||||
}
|
||||
return c.Security.TLS
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user