diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/snonux/main.go | 11 | ||||
| -rw-r--r-- | cmd/snonux/main_test.go | 22 | ||||
| -rw-r--r-- | cmd/snonux/sync.go | 71 |
3 files changed, 102 insertions, 2 deletions
diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go index 1d00646..42a377f 100644 --- a/cmd/snonux/main.go +++ b/cmd/snonux/main.go @@ -4,7 +4,7 @@ // // Usage: // -// snonux --input ./inbox --output ./outdir [--base-url https://snonux.foo] +// snonux [--input ./inbox] [--output ./dist] [--base-url https://snonux.foo] package main import ( @@ -51,6 +51,12 @@ func main() { if err := run(cfg); err != nil { log.Fatalf("error: %v", err) } + + if cfg.Sync { + if err := syncOutput(cfg.OutputDir); err != nil { + log.Fatalf("error: %v", err) + } + } } // errParseFlags is returned when flag parsing fails (e.g. unknown flag). @@ -69,9 +75,10 @@ func parseFlags(args []string) (*config.Config, cliMode, error) { listThemes := fs.Bool("list-themes", false, "print all available theme names and exit") fs.StringVar(&cfg.InputDir, "input", "./inbox", "directory containing new source files to process") - fs.StringVar(&cfg.OutputDir, "output", "~/git/snonux.foo/dist", "root directory for generated static site output") + 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)") if err := fs.Parse(args); err != nil { return nil, modeRun, fmt.Errorf("%w: %w", errParseFlags, err) diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go index 212efc8..7f05c8b 100644 --- a/cmd/snonux/main_test.go +++ b/cmd/snonux/main_test.go @@ -104,6 +104,28 @@ func TestParseFlags_run(t *testing.T) { } } +func TestParseFlags_sync(t *testing.T) { + t.Parallel() + + in := t.TempDir() + out := t.TempDir() + cfg, mode, err := parseFlags([]string{ + "-input", in, + "-output", out, + "-theme", "neon", + "-sync", + }) + if err != nil { + t.Fatal(err) + } + if mode != modeRun { + t.Fatalf("mode %v", mode) + } + if !cfg.Sync { + t.Fatal("expected cfg.Sync") + } +} + func TestParseFlags_randomTheme(t *testing.T) { t.Parallel() diff --git a/cmd/snonux/sync.go b/cmd/snonux/sync.go new file mode 100644 index 0000000..46993e2 --- /dev/null +++ b/cmd/snonux/sync.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "os/user" + "path/filepath" + "time" +) + +// SNONUX_SYNC_USER overrides the SSH username for rsync (default: current login name). +const envSyncUser = "SNONUX_SYNC_USER" + +var syncTargets = []string{ + "pi0.lan.buetow.org", + "pi1.lan.buetow.org", +} + +const syncRemoteDir = "/var/www/html/snonux/" + +// 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 { + if !hostPingable(host) { + log.Printf("sync skipped: %q not pingable (all mirror hosts must be reachable)", host) + return nil + } + } + + sshUser := os.Getenv(envSyncUser) + if sshUser == "" { + u, err := user.Current() + if err != nil { + return fmt.Errorf("sync user: %w (set %s)", err, envSyncUser) + } + sshUser = u.Username + } + + absOut, err := filepath.Abs(localOutput) + 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) + log.Printf("rsync %s -> %s", src, dest) + cmd := exec.Command("rsync", "-az", "-e", ssh, src, dest) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("rsync to %s: %w", host, err) + } + } + return nil +} + +func hostPingable(host string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // Linux iputils-ping: -c 1 one packet, -W 3 wait up to 3s for reply. + cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "3", host) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} |
