/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2026, Leafscale, LLC - https://www.leafscale.com Project: repoman Filename: src/sync.reef Authors: Chris Tusa License: Description: rsync-to-NFS backup driver (cmd_sync) with autofs/mount awareness ******************************************************************************/ module sync import core.str import core.result_generic as rg import sys.process as p import sys.fd as fd export fn build_rsync_args(src: string, dst: string, dry_run: bool, no_delete: bool, is_tty: bool, excluded_repos: [string]): [string] fn ensure_nfs_mounted(backup_root: string, verbose: bool): rg.Result[bool, string] fn run_rsync(args: [string]): int end export // Hardcoded excludes matching bash prototype. fn standard_excludes(): [string] return [ "node_modules/", "target/", "build/", "dist/", ".next/", "__pycache__/", "*.pyc", ".venv/", "venv/", ".cache/", ".tox/", ".pytest_cache/", ".mypy_cache/", ".ruff_cache/" ] end standard_excludes fn build_rsync_args(src: string, dst: string, dry_run: bool, no_delete: bool, is_tty: bool, excluded_repos: [string]): [string] let std: [string] = standard_excludes() let std_n: int = std.length() let ex_n: int = excluded_repos.length() // Capacity: 1 (-aHAX) + up to 3 info flags + 1 (--delete) + std_n excludes + ex_n excludes + 2 positionals let cap: int = 1 + 3 + 1 + std_n + ex_n + 2 mut buf: [string] = new [string](cap) mut k: int = 0 buf[k] = "-aHAX" k = k + 1 if dry_run buf[k] = "--dry-run" k = k + 1 buf[k] = "--itemize-changes" k = k + 1 buf[k] = "--info=stats2" k = k + 1 elif is_tty buf[k] = "--info=stats2,progress2" k = k + 1 else buf[k] = "--info=stats2" k = k + 1 end if if not no_delete buf[k] = "--delete" k = k + 1 end if mut i: int = 0 while i < std_n buf[k] = "--exclude=" + std[i] k = k + 1 i = i + 1 end while mut j: int = 0 while j < ex_n buf[k] = "--exclude=" + excluded_repos[j] + "/" k = k + 1 j = j + 1 end while buf[k] = src k = k + 1 buf[k] = dst k = k + 1 // Trim to actual size mut out: [string] = new [string](k) mut m: int = 0 while m < k out[m] = buf[m] m = m + 1 end while return out end build_rsync_args // Silent variant of process_run: child's stdout + stderr go to /dev/null. // Used for the NFS preflight probes where stat/findmnt would otherwise dump // file metadata and mount tables to the user's terminal on success, or // noisy "cannot stat" messages on failure (we provide our own diagnostic). fn process_run_silent(program: string, args: [string]): int let pid: int = p.process_fork() if pid < 0 return -1 end if if pid == 0 let dev_null: int = fd.fd_open("/dev/null", fd.O_WRONLY(), 0) if dev_null >= 0 let _o: int = fd.fd_dup2(dev_null, fd.STDOUT()) let _e: int = fd.fd_dup2(dev_null, fd.STDERR()) let _c: int = fd.fd_close(dev_null) end if let _x: int = p.process_run_exec(program, args) p.exit_now(127) end if return pid end process_run_silent // Helper: spawn `program args` either silent (quiet mode) or with stdio // passing through (verbose mode). Returns the child PID. fn spawn_probe(program: string, args: [string], verbose: bool): int if verbose return p.process_run(program, args) end if return process_run_silent(program, args) end spawn_probe fn ensure_nfs_mounted(backup_root: string, verbose: bool): rg.Result[bool, string] // Step 1: stat triggers autofs let pid1: int = spawn_probe("stat", [backup_root], verbose) if pid1 < 0 return @Result[bool, string].Err("cannot spawn stat") end if if p.process_wait(pid1) != 0 return @Result[bool, string].Err("cannot stat " + backup_root + " — autofs misconfigured or server unreachable") end if // Step 2: mountpoint let pid2: int = spawn_probe("mountpoint", ["-q", backup_root], verbose) if pid2 < 0 return @Result[bool, string].Err("cannot spawn mountpoint") end if if p.process_wait(pid2) != 0 return @Result[bool, string].Err(backup_root + " exists but is not a mount — NFS server unreachable?") end if // Step 3: findmnt -t nfs4 let pid3: int = spawn_probe("findmnt", ["-t", "nfs4", backup_root], verbose) if pid3 < 0 return @Result[bool, string].Err("cannot spawn findmnt") end if if p.process_wait(pid3) != 0 return @Result[bool, string].Err(backup_root + " is mounted but not as nfs4 — check /etc/auto.nfs") end if return @Result[bool, string].Ok(true) end ensure_nfs_mounted // Spawn rsync with the given argv. Inherits parent stdio (no capture). // Returns rsync's exit code, or -1 if spawn failed. // Named `run_rsync` because bare `run` collides with Reef's active-object // lifecycle keyword. fn run_rsync(args: [string]): int let pid: int = p.process_run("rsync", args) if pid < 0 return -1 end if return p.process_wait(pid) end run_rsync end module