1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
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
|