diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-27 09:01:44 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-27 09:01:44 +0300 |
| commit | 626ff3ae7d43cfc2ec3f2554d340b40f4a5c0586 (patch) | |
| tree | 56840d4c9815de0a71a41bceac71136371978076 /cmd | |
| parent | 30e63df03544b94ebc5fcb2a004d18a0d32a4247 (diff) | |
Externalize sync targets from hardcoded constants to env/config
- Added SyncTargets and SyncRemoteDir to internal/config.Config.
- Replaced the package-level syncTargets and syncRemoteDir constants in
cmd/snonux/sync.go with default values, populated lazily via
resolveSyncConfig().
- Accept --sync-targets and --sync-remote-dir CLI flags; fall back to
SNONUX_SYNC_TARGETS and SNONUX_SYNC_REMOTE_DIR env vars; then fall back
to the previous hardcoded defaults (pi0/pi1, /var/www/html/snonux/).
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/snonux/main.go | 11 | ||||
| -rw-r--r-- | cmd/snonux/main_test.go | 41 | ||||
| -rw-r--r-- | cmd/snonux/sync.go | 51 | ||||
| -rw-r--r-- | cmd/snonux/sync_test.go | 101 |
4 files changed, 195 insertions, 9 deletions
diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go index 75e386e..659bd19 100644 --- a/cmd/snonux/main.go +++ b/cmd/snonux/main.go @@ -66,7 +66,7 @@ func main() { } if cfg.Sync { - if err := syncOutput(cfg.OutputDir); err != nil { + if err := syncOutput(cfg); err != nil { log.Fatalf("error: %v", err) } } @@ -91,12 +91,19 @@ func parseFlags(args []string) (*config.Config, cliMode, error) { fs.StringVar(&cfg.OutputDir, "output", "./dist", "root directory for generated static site output") fs.StringVar(&cfg.BaseURL, "base-url", "https://snonux.foo", "canonical base URL used in Atom feed links") fs.StringVar(&cfg.Theme, "theme", "random", "visual theme name, or \"random\" to pick one at random") - fs.BoolVar(&cfg.Sync, "sync", false, "after a successful run, rsync -output to pi0/pi1 when both are pingable (SSH user: SNONUX_SYNC_USER or login name)") + fs.BoolVar(&cfg.Sync, "sync", false, "after a successful run, rsync -output to mirror hosts when all are pingable (SSH user: SNONUX_SYNC_USER or login name)") + var syncTargets string + fs.StringVar(&syncTargets, "sync-targets", "", "comma-separated list of rsync target hosts (overrides SNONUX_SYNC_TARGETS and defaults)") + fs.StringVar(&cfg.SyncRemoteDir, "sync-remote-dir", "", "remote destination directory on target hosts (overrides SNONUX_SYNC_REMOTE_DIR and default)") if err := fs.Parse(args); err != nil { return nil, modeRun, fmt.Errorf("%w: %w", errParseFlags, err) } + if syncTargets != "" { + cfg.SyncTargets = splitAndTrim(syncTargets) + } + if showVersion { return nil, modeVersion, nil } diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go index a4fe22f..b8a5a82 100644 --- a/cmd/snonux/main_test.go +++ b/cmd/snonux/main_test.go @@ -121,6 +121,47 @@ func TestParseFlags_sync(t *testing.T) { if !cfg.Sync { t.Fatal("expected cfg.Sync") } + // Defaults should be empty until resolved at sync time. + if len(cfg.SyncTargets) != 0 { + t.Fatalf("expected no sync targets from flags alone, got %v", cfg.SyncTargets) + } + if cfg.SyncRemoteDir != "" { + t.Fatalf("expected empty remote dir from flags alone, got %q", cfg.SyncRemoteDir) + } +} + +func TestParseFlags_syncTargetsAndRemoteDir(t *testing.T) { + t.Parallel() + + cfg, mode, err := parseFlags([]string{ + "-input", "./in", + "-output", "./out", + "-theme", "neon", + "-sync", + "-sync-targets", "host1, host2,host3", + "-sync-remote-dir", "/var/www/custom/", + }) + if err != nil { + t.Fatal(err) + } + if mode != modeRun { + t.Fatalf("mode %v", mode) + } + if !cfg.Sync { + t.Fatal("expected cfg.Sync") + } + want := []string{"host1", "host2", "host3"} + if len(cfg.SyncTargets) != len(want) { + t.Fatalf("got targets %v, want %v", cfg.SyncTargets, want) + } + for i, v := range want { + if cfg.SyncTargets[i] != v { + t.Fatalf("target[%d] = %q, want %q", i, cfg.SyncTargets[i], v) + } + } + if cfg.SyncRemoteDir != "/var/www/custom/" { + t.Fatalf("got remote dir %q", cfg.SyncRemoteDir) + } } func TestResolvePaths(t *testing.T) { diff --git a/cmd/snonux/sync.go b/cmd/snonux/sync.go index 46993e2..f5d2e9e 100644 --- a/cmd/snonux/sync.go +++ b/cmd/snonux/sync.go @@ -8,23 +8,60 @@ import ( "os/exec" "os/user" "path/filepath" + "strings" "time" + + "codeberg.org/snonux/snonux/internal/config" ) // SNONUX_SYNC_USER overrides the SSH username for rsync (default: current login name). const envSyncUser = "SNONUX_SYNC_USER" -var syncTargets = []string{ +// defaultSyncTargets are the built-in mirror hosts used when no configuration overrides them. +var defaultSyncTargets = []string{ "pi0.lan.buetow.org", "pi1.lan.buetow.org", } -const syncRemoteDir = "/var/www/html/snonux/" +const defaultSyncRemoteDir = "/var/www/html/snonux/" + +// resolveSyncConfig populates cfg.SyncTargets and cfg.SyncRemoteDir from the +// environment if they are empty, applying sensible defaults. +func resolveSyncConfig(cfg *config.Config) { + if len(cfg.SyncTargets) == 0 { + if v := os.Getenv("SNONUX_SYNC_TARGETS"); v != "" { + cfg.SyncTargets = splitAndTrim(v) + } else { + cfg.SyncTargets = append([]string(nil), defaultSyncTargets...) + } + } + if cfg.SyncRemoteDir == "" { + if v := os.Getenv("SNONUX_SYNC_REMOTE_DIR"); v != "" { + cfg.SyncRemoteDir = strings.TrimSpace(v) + } else { + cfg.SyncRemoteDir = defaultSyncRemoteDir + } + } +} + +func splitAndTrim(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + return out +} // syncOutput rsyncs localOutput (trailing-slash source) to each sync target over SSH // port 22. It runs only if every target answers ICMP ping (Linux iputils: ping -c 1 -W …). -func syncOutput(localOutput string) error { - for _, host := range syncTargets { +func syncOutput(cfg *config.Config) error { + resolveSyncConfig(cfg) + + for _, host := range cfg.SyncTargets { if !hostPingable(host) { log.Printf("sync skipped: %q not pingable (all mirror hosts must be reachable)", host) return nil @@ -40,15 +77,15 @@ func syncOutput(localOutput string) error { sshUser = u.Username } - absOut, err := filepath.Abs(localOutput) + absOut, err := filepath.Abs(cfg.OutputDir) if err != nil { return fmt.Errorf("sync output dir: %w", err) } src := filepath.Clean(absOut) + string(filepath.Separator) ssh := "ssh -p 22 -o BatchMode=yes -o ConnectTimeout=15" - for _, host := range syncTargets { - dest := fmt.Sprintf("%s@%s:%s", sshUser, host, syncRemoteDir) + for _, host := range cfg.SyncTargets { + dest := fmt.Sprintf("%s@%s:%s", sshUser, host, cfg.SyncRemoteDir) log.Printf("rsync %s -> %s", src, dest) cmd := exec.Command("rsync", "-az", "-e", ssh, src, dest) cmd.Stdout = os.Stdout diff --git a/cmd/snonux/sync_test.go b/cmd/snonux/sync_test.go new file mode 100644 index 0000000..d45a916 --- /dev/null +++ b/cmd/snonux/sync_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "os" + "reflect" + "testing" + + "codeberg.org/snonux/snonux/internal/config" +) + +func TestSplitAndTrim(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + want []string + }{ + {"a,b,c", []string{"a", "b", "c"}}, + {" a , b ", []string{"a", "b"}}, + {"a,,c", []string{"a", "c"}}, + {"", []string{}}, + {",,", []string{}}, + } + + for _, c := range cases { + got := splitAndTrim(c.in) + if !reflect.DeepEqual(got, c.want) { + t.Errorf("splitAndTrim(%q) = %v; want %v", c.in, got, c.want) + } + } +} + +func TestResolveSyncConfig_defaults(t *testing.T) { + t.Parallel() + + cfg := &config.Config{} + resolveSyncConfig(cfg) + want := []string{"pi0.lan.buetow.org", "pi1.lan.buetow.org"} + if !reflect.DeepEqual(cfg.SyncTargets, want) { + t.Fatalf("got targets %v, want %v", cfg.SyncTargets, want) + } + if cfg.SyncRemoteDir != "/var/www/html/snonux/" { + t.Fatalf("got remote dir %q", cfg.SyncRemoteDir) + } +} + +func TestResolveSyncConfig_envTargets(t *testing.T) { + t.Parallel() + + orig := os.Getenv("SNONUX_SYNC_TARGETS") + defer os.Setenv("SNONUX_SYNC_TARGETS", orig) + + os.Setenv("SNONUX_SYNC_TARGETS", "h1, h2 ,h3") + cfg := &config.Config{} + resolveSyncConfig(cfg) + want := []string{"h1", "h2", "h3"} + if !reflect.DeepEqual(cfg.SyncTargets, want) { + t.Fatalf("got targets %v, want %v", cfg.SyncTargets, want) + } +} + +func TestResolveSyncConfig_envRemoteDir(t *testing.T) { + t.Parallel() + + orig := os.Getenv("SNONUX_SYNC_REMOTE_DIR") + defer os.Setenv("SNONUX_SYNC_REMOTE_DIR", orig) + + os.Setenv("SNONUX_SYNC_REMOTE_DIR", "/custom/path/") + cfg := &config.Config{} + resolveSyncConfig(cfg) + if cfg.SyncRemoteDir != "/custom/path/" { + t.Fatalf("got remote dir %q", cfg.SyncRemoteDir) + } +} + +func TestResolveSyncConfig_flagsOverrideEnv(t *testing.T) { + t.Parallel() + + origTargets := os.Getenv("SNONUX_SYNC_TARGETS") + origDir := os.Getenv("SNONUX_SYNC_REMOTE_DIR") + defer func() { + os.Setenv("SNONUX_SYNC_TARGETS", origTargets) + os.Setenv("SNONUX_SYNC_REMOTE_DIR", origDir) + }() + + os.Setenv("SNONUX_SYNC_TARGETS", "from-env") + os.Setenv("SNONUX_SYNC_REMOTE_DIR", "/env/dir/") + + cfg := &config.Config{ + SyncTargets: []string{"from-flag"}, + SyncRemoteDir: "/flag/dir/", + } + resolveSyncConfig(cfg) + + if !reflect.DeepEqual(cfg.SyncTargets, []string{"from-flag"}) { + t.Fatalf("flag targets should override env: got %v", cfg.SyncTargets) + } + if cfg.SyncRemoteDir != "/flag/dir/" { + t.Fatalf("flag dir should override env: got %q", cfg.SyncRemoteDir) + } +} |
