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
|
module sync
import core.str
import core.result_generic as rg
import sys.process as p
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): 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
fn ensure_nfs_mounted(backup_root: string): rg.Result[bool, string]
// Step 1: stat triggers autofs
let pid1: int = p.process_run("stat", [backup_root])
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 = p.process_run("mountpoint", ["-q", backup_root])
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 = p.process_run("findmnt", ["-t", "nfs4", backup_root])
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
|