|
root / src / sync.reef
sync.reef Reef 139 lines 3.7 KB
  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