Files
move-data-nas/internal/config/config.go
T
darroyo bfa006f4ab Add SSH key management, job history persistence, and live streaming
- SSH key management: generate ed25519 keypairs or import public keys
  from UI (/ssh-keys), per-machine key selection in Machines form,
  one-time private key download with hash verification
- Fix engine to use machine-specific SSH key (was hardcoded to server key)
- Job log persistence: write to job_logs table (DB) with batched inserts,
  buffer of 50 lines; GetAllFiltered with status/pair/date range filters
- EventBus refactor: per-job subscriber channels, global channel, non-blocking
- SSE endpoints: /jobs/stream (all), /jobs/:id/log/stream (per-job live)
- JobDetail page: live log streaming, auto-scroll, cancel, duration
- JobHistory: filters (pair, status, date range), pagination, link to detail
- Cleanup scheduler: daily purge of job_logs and finished jobs older than
  SYNCSERVER_RETENTION_DAYS (default 30)
- Migration 0002: indexes on job_logs(job_id), jobs(status,created_at),
  jobs(sync_pair_id)
2026-07-07 20:36:11 -04:00

152 lines
3.4 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type Config struct {
Version string `yaml:"-" json:"-"`
DataDir string `yaml:"data_dir" env:"SYNCSERVER_DATA_DIR" default:"./data"`
ConfigDir string `yaml:"config_dir" env:"SYNCSERVER_CONFIG_DIR" default:"./etc/syncserver"`
Addr string `yaml:"addr" env:"SYNCSERVER_ADDR" default:":8080"`
Auth AuthConfig `yaml:"auth"`
Scheduler SchedulerConfig `yaml:"scheduler"`
}
type AuthConfig struct {
JWTSecret string `yaml:"jwt_secret" env:"SYNCSERVER_JWT_SECRET"`
JWTExpiryH int `yaml:"jwt_expiry_hours" env:"SYNCSERVER_JWT_EXPIRY_HOURS" default:"24"`
AdminUser string `yaml:"-" env:"SYNCSERVER_ADMIN_USER"`
AdminPass string `yaml:"-" env:"SYNCSERVER_ADMIN_PASSWORD"`
}
type SchedulerConfig struct {
Timezone string `yaml:"timezone" env:"SYNCSERVER_SCHEDULER_TZ" default:"UTC"`
RetentionDays int `yaml:"retention_days" env:"SYNCSERVER_RETENTION_DAYS" default:"30"`
}
var globalCfg *Config
func Load(configPath, dataDir, addr string) (*Config, error) {
cfg := &Config{
DataDir: dataDir,
ConfigDir: "./etc/syncserver",
Addr: addr,
Auth: AuthConfig{
JWTExpiryH: 24,
},
Scheduler: SchedulerConfig{
Timezone: "UTC",
RetentionDays: 30,
},
}
if configPath != "" {
data, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("reading config: %w", err)
}
if err == nil {
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
}
}
prefix := "SYNCSERVER_"
envs := []struct {
key *string
envName string
required bool
}{
{&cfg.Auth.JWTSecret, prefix + "JWT_SECRET", false},
{&cfg.Auth.AdminUser, prefix + "ADMIN_USER", false},
{&cfg.Auth.AdminPass, prefix + "ADMIN_PASSWORD", false},
{&cfg.DataDir, prefix + "DATA_DIR", false},
{&cfg.Addr, prefix + "ADDR", false},
{&cfg.Scheduler.Timezone, prefix + "SCHEDULER_TZ", false},
}
for _, e := range envs {
if v := os.Getenv(e.envName); v != "" {
*e.key = v
}
}
if cfg.Auth.JWTSecret == "" {
b := make([]byte, 32)
f, err := os.Open("/dev/urandom")
if err == nil {
defer f.Close()
n, _ := f.Read(b)
if n == 32 {
cfg.Auth.JWTSecret = fmt.Sprintf("%x", b)
}
}
if cfg.Auth.JWTSecret == "" {
cfg.Auth.JWTSecret = "insecure-dev-secret-change-in-production"
}
}
if dataDir := os.Getenv("SYNCSERVER_DATA_DIR"); dataDir != "" {
cfg.DataDir = dataDir
}
if addr := os.Getenv("SYNCSERVER_ADDR"); addr != "" {
cfg.Addr = addr
}
absDataDir, err := filepath.Abs(cfg.DataDir)
if err != nil {
return nil, err
}
cfg.DataDir = absDataDir
globalCfg = cfg
return cfg, nil
}
func Get() *Config {
return globalCfg
}
func (c *Config) DBPath() string {
return filepath.Join(c.DataDir, "app.db")
}
func (c *Config) SSHDir() string {
return filepath.Join(c.DataDir, "ssh")
}
func (c *Config) LogsDir() string {
return filepath.Join(c.DataDir, "logs")
}
func (c *Config) EnsureDirs() error {
dirs := []string{c.DataDir, c.SSHDir(), c.LogsDir()}
for _, d := range dirs {
if err := os.MkdirAll(d, 0700); err != nil {
return fmt.Errorf("creating dir %s: %w", d, err)
}
}
return nil
}
func (c *Config) LogPath() string {
return filepath.Join(c.LogsDir(), "app.log")
}
func NormalizeAddr(addr string) string {
addr = strings.TrimSpace(addr)
if !strings.Contains(addr, ":") {
addr = ":" + addr
}
return addr
}