summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/snonux/main.go11
-rw-r--r--cmd/snonux/main_test.go22
-rw-r--r--cmd/snonux/sync.go71
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
+}