#!/usr/bin/env bash
# shellcheck disable=SC1090,SC2154,SC2024,SC2053  # variables come from sourced host.conf / env files
# hostrepo-backup [--no-upload] [--dry-run]
#
# Gather everything that makes this host what it is into a repository-style
# directory, then upload it to B2 with restic (encrypted, deduplicated,
# versioned). hostrepo-restore rebuilds a replacement host from it.
#
#   /var/backups/hostrepo/<HOST_ID>/
#     files/<absolute path>   config, binaries, data, sites (rsync mirror)
#     db/postgres/<cluster>/  pg_dumpall per cluster + cluster settings
#     db/redis/<port>.rdb     a fresh BGSAVE of every running redis
#     db/sqlite/<abs path>    consistent .backup copies of every SQLite db
#     meta/                   packages, users, units, ports, network, versions
#     RESTORE.md              what to do with all of the above
#     manifest.json           summary; written last, so its presence = complete
#
# Exit codes: 0 clean, 1 finished with warnings, 2 failed.
set -euo pipefail
export LC_ALL=C
CONF="${HOSTREPO_CONF:-/etc/hostrepo/host.conf}"
ENV_FILE="${HOSTREPO_ENV:-/etc/hostrepo/hostrepo.env}"
# shellcheck source=/dev/null
. "$CONF"
if [[ -r "$ENV_FILE" ]]; then set -a; . "$ENV_FILE"; set +a; fi
UPLOAD=1; DRY=0
for a in "$@"; do case "$a" in --no-upload) UPLOAD=0 ;; --dry-run) DRY=1; UPLOAD=0 ;; *) echo "unknown arg $a" >&2; exit 2 ;; esac; done

REPO_DIR="${HOSTREPO_DIR:-/var/backups/hostrepo}/$HOST_ID"
m="$REPO_DIR/meta"
STATUS=0
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }
NWARN=0; NREPAIR=0   # counted apart: the status page shows a repair (blue) and a warning (yellow) differently
warn() { [[ $STATUS -lt 1 ]] && STATUS=1; NWARN=$((NWARN+1)); log "WARN: $*"; }
repaired() { [[ $STATUS -lt 1 ]] && STATUS=1; NREPAIR=$((NREPAIR+1)); log "REPAIRED: $*"; }
die() { log "FATAL: $*"; exit 2; }
[[ $EUID -eq 0 ]] || die "run as root"
command -v rsync >/dev/null || die "rsync missing"

mkdir -p "$REPO_DIR"/{files,db,meta}
chmod 0700 "${HOSTREPO_DIR:-/var/backups/hostrepo}" "$REPO_DIR"
rm -f "$REPO_DIR/manifest.json" "$REPO_DIR/meta/run.json" "$REPO_DIR/meta/heal-b2.log" "$REPO_DIR/meta/heal-lite.log"   # incomplete until rewritten at the end

# ---- 1. which trees ------------------------------------------------------------
paths=("${INCLUDE[@]}")
if [[ "${INCLUDE_VAR_LIB_UNOWNED:-0}" == 1 ]]; then
  for d in /var/lib/*; do
    [[ -d "$d" ]] || continue
    dpkg -S "$d" >/dev/null 2>&1 && continue
    # An explicitly named source is copied even when an --exclude pattern
    # matches it, so excluded trees must never enter the list.
    skip=0; for e in "${EXCLUDE[@]}"; do [[ "$d" == $e ]] && { skip=1; break; }; done
    [[ $skip -eq 1 ]] && continue
    paths+=("$d")
  done
fi
excl=()
for e in "${EXCLUDE[@]}"; do excl+=(--exclude="$e"); done
# Live SQLite files are copied consistently in step 4; the rsync copy of the
# live file is replaced by that snapshot.

# ---- 2. files ---------------------------------------------------------------------
log "files: ${#paths[@]} trees"
if [[ $DRY -eq 1 ]]; then printf '  %s\n' "${paths[@]}" >&2
else
  rc=0
  rsync -aHAX --numeric-ids --relative --delete --delete-excluded --one-file-system \
    "${excl[@]}" "${paths[@]}" "$REPO_DIR/files/" || rc=$?
  # 24: files vanished during the copy (normal on a live host)
  [[ $rc -eq 0 || $rc -eq 24 ]] || die "rsync failed ($rc)"
  # Trees that disappeared from the host must disappear from the mirror too.
  for top in "$REPO_DIR"/files/var/lib/*; do
    [[ -e "$top" ]] || continue
    [[ -e "/var/lib/${top##*/}" ]] || rm -rf -- "$top"
  done
fi

# ---- 2b. structure a restored host needs but the trees above do not carry ------
if [[ $DRY -eq 0 ]]; then
  # /var/log directories (not contents): units write logs there and fail to
  # start when their directory is missing.
  # (journal/ is keyed to the machine id and belongs to the machine.)
  rsync -aHAX --numeric-ids --delete --delete-excluded -f '- /journal/' -f '+ */' -f '- *' /var/log/ "$REPO_DIR/files/var/log/"
  # ...and the names of the log files in them, with owner and mode. Some
  # daemons refuse to start when a log they watch is missing (fail2ban: "Have
  # not found any log file for <jail>"). The restore recreates them empty.
  find /var/log -path /var/log/journal -prune -o -type f -printf '%u\t%g\t%m\t%p\n' 2>/dev/null |
    grep -v -E '\.([0-9]+|gz|xz|bz2|zst|old)$|\.[0-9]+\.gz$' | sort -k4 > "$m/var-log-files.tsv" || true
  # Files under /usr that no package owns (hand-installed binaries such as
  # /usr/bin/anubis). /usr/local is copied whole already.
  # Owned paths, with /bin /sbin /lib /lib64 also listed as their /usr twins
  # (dpkg records the pre-usr-merge names). Symlinks are skipped: they are
  # alternatives and diversions that packages recreate. Kernel modules,
  # firmware and generated caches belong to the machine, not the service.
  owned=$(mktemp)
  cat /var/lib/dpkg/info/*.list 2>/dev/null | awk '{print} /^\/(bin|sbin|lib|lib64)\// {print "/usr" $0}' | sort -u > "$owned"
  find /usr -xdev \( -path /usr/local -o -path /usr/share/keyrings -o -path /usr/lib/modules -o -path /usr/lib/firmware \
      -o -path /usr/lib/recovery-mode -o -path /usr/share/mime -o -path /usr/share/man -o -path /usr/share/info \
      -o -path /usr/share/icons -o -path /usr/share/fonts -o -path /usr/share/plymouth -o -name __pycache__ \) -prune \
      -o -type f -print 2>/dev/null |
    sort | comm -23 - "$owned" > "$m/usr-unowned.txt"
  rm -f "$owned"
  if [[ -s "$m/usr-unowned.txt" ]]; then
    rsync -aHAX --numeric-ids --files-from="$m/usr-unowned.txt" / "$REPO_DIR/files/"
    log "usr: $(wc -l < "$m/usr-unowned.txt") files no package owns"
  fi
fi

# ---- 3. postgres: every cluster, dumped whole ----------------------------------------
if command -v pg_lsclusters >/dev/null; then
  rm -rf "$REPO_DIR/db/postgres"; mkdir -p "$REPO_DIR/db/postgres"
  pg_lsclusters -h > "$REPO_DIR/db/postgres/clusters.txt"
  while read -r ver name port status owner datadir _; do
    [[ "$status" == online* ]] || { warn "postgres $ver/$name is $status; not dumped"; continue; }
    out="$REPO_DIR/db/postgres/$ver-$name"; mkdir -p "$out"
    printf 'version=%s\nname=%s\nport=%s\nowner=%s\ndatadir=%s\n' "$ver" "$name" "$port" "$owner" "$datadir" > "$out/cluster.env"
    if [[ $DRY -eq 1 ]]; then log "[dry-run] pg_dumpall $ver/$name :$port"; continue; fi
    if sudo -u postgres pg_dumpall -p "$port" --clean --if-exists > "$out/dumpall.sql.tmp" 2> "$out/dump.err"; then
      mv "$out/dumpall.sql.tmp" "$out/dumpall.sql"; rm -f "$out/dump.err"
      log "postgres $ver/$name: $(du -h "$out/dumpall.sql" | cut -f1)"
    else
      warn "pg_dumpall $ver/$name failed: $(head -c 200 "$out/dump.err")"
    fi
  done < "$REPO_DIR/db/postgres/clusters.txt"
fi

# ---- 4. redis: fresh snapshot of every persistent instance ---------------------------
# Instances with persistence switched off (save "" and no AOF) are ephemeral by
# design (queues, one-time secrets) and are deliberately not captured.
if command -v redis-cli >/dev/null; then
  rm -rf "$REPO_DIR/db/redis"; mkdir -p "$REPO_DIR/db/redis"
  while read -r port pid; do
    unit=$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')
    conf=$(systemctl show -p ExecStart --value "$unit" 2>/dev/null | grep -oE '/[^ ;]+\.conf' | head -1)
    auth=""; [[ -n "$conf" ]] && auth=$(awk '$1=="requirepass" {print $2}' "$conf" | tr -d '"')
    rc() { if [[ -n "$auth" ]]; then REDISCLI_AUTH="$auth" redis-cli --no-auth-warning -p "$port" "$@"; else redis-cli -p "$port" "$@"; fi; }
    if [[ -n "$conf" ]] && grep -qE '^save ""' "$conf" && ! grep -qE '^appendonly yes' "$conf"; then
      log "redis :$port ($unit): persistence off by design; not captured"; continue
    fi
    [[ $DRY -eq 1 ]] && { log "[dry-run] redis :$port ($unit)"; continue; }
    before=$(rc LASTSAVE 2>/dev/null) || { warn "redis :$port ($unit): cannot connect"; continue; }
    rc BGSAVE >/dev/null 2>&1 || { warn "redis :$port ($unit): BGSAVE refused"; continue; }
    for _ in $(seq 1 120); do [[ "$(rc LASTSAVE)" != "$before" ]] && break; sleep 1; done
    dir=$(rc CONFIG GET dir | sed -n 2p); file=$(rc CONFIG GET dbfilename | sed -n 2p)
    if [[ -n "$dir" && -f "$dir/$file" ]]; then
      cp -p "$dir/$file" "$REPO_DIR/db/redis/$port.rdb"
      printf 'port=%s\nunit=%s\ndir=%s\ndbfilename=%s\n' "$port" "$unit" "$dir" "$file" > "$REPO_DIR/db/redis/$port.env"
      log "redis :$port ($unit): $(du -h "$dir/$file" | cut -f1)"
    else warn "redis :$port ($unit): no dump found"; fi
  done < <(ss -ltnpH 2>/dev/null | awk '/redis-server/ {n=split($4,a,":"); match($0,/pid=[0-9]+/); print a[n], substr($0,RSTART+4,RLENGTH-4)}' | sort -u -k1,1)
fi

# ---- 5. sqlite: consistent copies of every database in the copied trees --------------
if command -v sqlite3 >/dev/null; then
  rm -rf "$REPO_DIR/db/sqlite"; mkdir -p "$REPO_DIR/db/sqlite"
  n=0
  while IFS= read -r -d '' f; do
    head -c 16 "$f" 2>/dev/null | grep -q '^SQLite format 3' || continue
    [[ $DRY -eq 1 ]] && { n=$((n+1)); continue; }
    dst="$REPO_DIR/db/sqlite$f"; mkdir -p "$(dirname "$dst")"
    if sqlite3 "file:$f?mode=ro" ".timeout 10000" ".backup '$dst'" 2>/dev/null; then
      chown --reference="$f" "$dst"; chmod --reference="$f" "$dst"; n=$((n+1))
    else warn "sqlite backup failed: $f"; fi
  done < <(find "${paths[@]}" -xdev -type f \( -name '*.db' -o -name '*.sqlite' -o -name '*.sqlite3' \) -size +0 -print0 2>/dev/null)
  log "sqlite: $n databases"
else
  warn "sqlite3 not installed; SQLite files are only copied live (may be inconsistent)"
fi

# ---- 6. meta: what the host is ------------------------------------------------------------
# shellcheck source=/dev/null
{ . /etc/os-release; printf 'id=%q\nversion_id=%q\npretty=%q\n' "$ID" "$VERSION_ID" "$PRETTY_NAME"; } > "$m/os.env"
uname -a > "$m/uname.txt"; hostname -f > "$m/hostname.txt" 2>/dev/null || hostname > "$m/hostname.txt"
apt-mark showmanual | sort > "$m/apt-manual.txt"
dpkg --get-selections > "$m/dpkg-selections.txt"
dpkg-query -W -f='${Package}\t${Version}\n' | sort > "$m/dpkg-versions.tsv"
# Full channel from snap info (snap list truncates it with "...").
: > "$m/snaps.tsv"
for n in $(snap list 2>/dev/null | awk 'NR>1 {print $1}'); do
  printf '%s\t%s\n' "$n" "$(snap info "$n" 2>/dev/null | awk '/^tracking:/ {print $2}')" >> "$m/snaps.tsv"
done
apt-mark showhold > "$m/apt-hold.txt" 2>/dev/null || true
uname -r > "$m/kernel.txt"
getent passwd > "$m/passwd.txt"; getent group > "$m/group.txt"
cp -p /etc/shadow "$m/shadow.txt"; cp -p /etc/gshadow "$m/gshadow.txt"; chmod 0600 "$m/shadow.txt" "$m/gshadow.txt"
systemctl list-unit-files --no-legend --no-pager | awk '{print $1 "\t" $2}' > "$m/unit-files.tsv"
systemctl list-units --type=service --state=running --no-legend --no-pager | awk '{print $1}' > "$m/running-services.txt"
systemctl list-timers --all --no-legend --no-pager | awk '{print $NF}' > "$m/timers.txt"
ss -ltnupH > "$m/listening.txt" 2>/dev/null || true
ip -brief addr > "$m/ip-addr.txt"; ip route > "$m/ip-route.txt"; ip -6 route > "$m/ip6-route.txt" 2>/dev/null || true
(nft list ruleset > "$m/nft.txt") 2>/dev/null || true
(iptables-save > "$m/iptables.txt") 2>/dev/null || true
crontab -l > "$m/root-crontab.txt" 2>/dev/null || true
printf '%s\n' "${paths[@]}" > "$m/included-paths.txt"

# ---- 7. coverage: every running custom service's files must be in files/ ------------
: > "$m/coverage.txt"
covered() { local p; for p in "${paths[@]}"; do [[ "$1" == "$p" || "$1" == "$p"/* ]] && return 0; done; return 1; }
while read -r u; do
  frag=$(systemctl show -p FragmentPath --value "$u")
  drop=$(systemctl show -p DropInPaths --value "$u")
  [[ "$frag" == /etc/* || -n "$drop" ]] || continue
  for p in $(systemctl show "$u" -p WorkingDirectory -p EnvironmentFiles -p ReadWritePaths --value | tr ' ' '\n' | grep '^/' ) \
           $(systemctl show "$u" -p ExecStart --value | grep -oE 'path=[^ ;]+' | cut -d= -f2); do
    p="${p%%(*}"
    [[ "$p" == /usr/bin/* || "$p" == /usr/sbin/* || "$p" == /bin/* || "$p" == /lib/* || "$p" == /usr/lib/* ]] && continue
    # Logs and runtime state are not restored; a unit writing there is fine.
    [[ "$p" == /var/log || "$p" == /var/log/* || "$p" == /run || "$p" == /proc/* || "$p" == /sys/* ]] && continue
    # Runtime state: nothing to restore.
    [[ "$p" == /run/* || "$p" == /tmp/* ]] && continue
    if [[ "$p" == /dev/shm/* ]]; then
      # A binary run from memory is fine if ExecStartPre copies it there from
      # a backed-up path (possibly inside a bash -c "...").
      src=$( { systemctl cat "$u" 2>/dev/null | grep -E '^ExecStartPre=.*\bcp\b' | grep -F "$p" | grep -oE '/[^ "]+' |
               grep -vE '^/(usr/)?bin/|^/dev/shm/|^/run/|^/tmp/' | head -1; } || true)
      if [[ -n "$src" ]] && covered "$src"; then continue; fi
      echo "$u: $p is in memory and not copied from a backed-up path" >> "$m/coverage.txt"; warn "coverage: $u runs from memory ($p)"; continue
    fi
    covered "$p" || { echo "$u: NOT COVERED $p" >> "$m/coverage.txt"; warn "coverage: $u uses $p, which is not backed up"; }
  done
done < "$m/running-services.txt"

# ---- 8. RESTORE.md and manifest --------------------------------------------------------------
cat > "$REPO_DIR/RESTORE.md" <<EOF
# Restoring $HOST_ID

Made $(date -u +%FT%TZ) on $(cat "$m/hostname.txt") ($(. "$m/os.env"; echo "$pretty")).

1. Provision a fresh host with the same OS: $(. "$m/os.env"; echo "$pretty").
2. Install restic, fetch this repository, and run the restore:

       restic -r <repo> restore latest --tag $HOST_ID --target /var/restore
       /var/restore$REPO_DIR/hostrepo-restore --from /var/restore$REPO_DIR

   The restore runs in order: accounts, packages, files, ownership,
   databases, services.
   \`--dry-run\` shows every step; \`--only <step>\` runs one.
3. Not restored automatically (they describe the old machine, not the service):
$(printf '   - %s\n' "${RESTORE_SKIP[@]}")
   Addresses in meta/ip-addr.txt and firewall rules in meta/nft.txt,
   meta/iptables.txt are for reference.
4. Point DNS at the new host. Compare meta/listening.txt with \`ss -ltnup\`.
5. If restic reports damaged files, repair them from the parity sets in the
   bucket (the same B2 key and RESTIC_REPOSITORY as restic, plus par2 and
   rclone installed), then check again:

       /var/restore$REPO_DIR/hostrepo-heal b2-repair $HOST_ID <id>...
EOF
cp -p "$0" "$REPO_DIR/hostrepo-backup"
for a in hostrepo-restore hostrepo-heal hostrepo-b2put; do
  [[ -f "$(dirname "$0")/$a" ]] && cp -p "$(dirname "$0")/$a" "$REPO_DIR/$a"
done
cp -p "$CONF" "$REPO_DIR/host.conf"
bytes=$(du -sb "$REPO_DIR" | cut -f1)
cat > "$REPO_DIR/manifest.json" <<EOF
{"host": "$HOST_ID", "made": "$(date -u +%FT%TZ)", "bytes": $bytes,
 "trees": ${#paths[@]}, "postgres_clusters": $(find "$REPO_DIR/db/postgres" -name dumpall.sql 2>/dev/null | wc -l),
 "redis": $(find "$REPO_DIR/db/redis" -name '*.rdb' 2>/dev/null | wc -l), "sqlite": $(find "$REPO_DIR/db/sqlite" -type f 2>/dev/null | wc -l),
 "warnings": $STATUS}
EOF
log "repository ready: $REPO_DIR ($(numfmt --to=iec "$bytes"))"

# ---- 9. upload -------------------------------------------------------------------------------
if [[ $UPLOAD -eq 1 ]]; then
  : "${RESTIC_REPOSITORY:?set in $ENV_FILE}" "${RESTIC_PASSWORD_FILE:?set in $ENV_FILE}"
  restic snapshots --latest 1 >/dev/null 2>&1 || restic init
  restic backup --tag "$HOST_ID" --host "$HOST_ID" --one-file-system "$REPO_DIR" \
    || die "restic backup failed"
  restic forget --tag "$HOST_ID" --host "$HOST_ID" "${KEEP[@]}" --prune >/dev/null || warn "restic forget/prune failed"
  # Parity in the bucket (par2/<HOST_ID>/): a set for every new repository
  # file, each file's hash checked on the way; sets of pruned files deleted.
  HEAL="$(dirname "$0")/hostrepo-heal"
  if command -v par2 >/dev/null && [[ -x "$HEAL" ]]; then
    rc=0; timeout 7200 "$HEAL" b2-sync "$HOST_ID" 2>"$m/heal-b2.log" || rc=$?
    cat "$m/heal-b2.log" >&2
    [[ $rc -ne 0 ]] && warn "par2 sync in B2 exited $rc (see above)"
    # The lite check: every file's SHA-1 as B2 reports it in a listing,
    # against the SHA-1 recorded when the file was proven good. Nothing is
    # downloaded unless a file fails, and then only to repair it.
    rc=0; timeout 3600 "$HEAL" b2-lite "$HOST_ID" --repair 2>"$m/heal-lite.log" || rc=$?
    cat "$m/heal-lite.log" >&2
    case $rc in
      0) ;;
      1) repaired "the SHA-1 check found damage in B2; repaired from par2" ;;
      *) STATUS=2; log "FAIL: the SHA-1 check found damage in B2 that par2 could not repair; see $m/heal-lite.log" ;;
    esac
  else
    warn "par2 or hostrepo-heal missing: no parity sets in B2"
  fi
  # Monthly (on the 1st), read back one twelfth of the stored data, a different
  # twelfth each month: every byte is re-verified once a year, for about 1/12
  # of the repository in download per month. Other nights, structure only.
  check=(check); what="restic check"; check_result=ok
  if [[ "$(date -u +%d)" == 01 ]]; then check+=(--read-data-subset "$(date -u +%-m)/12"); what+=" (data subset $(date -u +%-m)/12)"; fi
  if ! restic "${check[@]}" > "$m/restic-check.log" 2>&1; then
    # Repair what the check names (every restic file is named by its hash),
    # from the parity sets in the bucket, then check again.
    mapfile -t ids < <(grep -oE '\b[0-9a-f]{64}\b' "$m/restic-check.log" | sort -u)
    rc=0; [[ ${#ids[@]} -gt 0 && -x "$HEAL" ]] && { timeout 3600 "$HEAL" b2-repair "$HOST_ID" "${ids[@]}" || rc=$?; }
    if [[ $rc -eq 1 ]] && restic "${check[@]}" >> "$m/restic-check.log" 2>&1; then
      check_result=repaired; repaired "$what found damage; repaired from par2 and re-checked clean"
    elif [[ $rc -ge 2 ]]; then
      check_result=damaged; STATUS=2; log "FAIL: $what found damaged files that par2 could not repair; see $m/restic-check.log"
    else
      check_result=error; warn "$what failed with no damaged file found (network or lock?); see $m/restic-check.log"
    fi
  fi
  # Numbers for the status page (hostrepo-run stores them with the result).
  # Counts and sizes only: no names, paths or warning text.
  sync_line=$(grep -m1 'b2-sync ' "$m/heal-b2.log" 2>/dev/null || true)
  lite_line=$(grep -m1 'b2-lite ' "$m/heal-lite.log" 2>/dev/null || true)
  lnum() { grep -oE "[0-9]+ $1" <<<"$lite_line" | cut -d' ' -f1 || true; }
  num() { grep -oE "[0-9]+ $1" <<<"$sync_line" | cut -d' ' -f1 || true; }
  jq -n --argjson snap "$(restic snapshots --json --latest 1 --host "$HOST_ID" 2>/dev/null | jq '.[0] // {}' || echo '{}')" \
        --argjson stats "$(restic stats --mode raw-data --json 2>/dev/null || echo '{}')" \
        --arg check "${check[*]:1}" --arg check_result "$check_result" \
        --arg files "$(num files)" --arg made "$(num 'sets created')" --arg gone "$(num 'stale')" \
        --argjson postgres "$(find "$REPO_DIR/db/postgres" -name dumpall.sql 2>/dev/null | wc -l)" \
        --argjson sqlite "$(find "$REPO_DIR/db/sqlite" -type f 2>/dev/null | wc -l)" \
        --argjson redis "$(find "$REPO_DIR/db/redis" -name '*.rdb' 2>/dev/null | wc -l)" \
        --argjson warnings "$NWARN" --argjson repairs "$NREPAIR" \
        --arg l_match "$(lnum match)" --arg l_bad "$(lnum mismatched)" --arg l_unver "$(lnum 'not yet verified')" --arg l_missing "$(lnum missing)" \
    '{snapshot: ($snap.short_id // null), summary: ($snap.summary // null),
      repo: {stored_bytes: $stats.total_size, raw_bytes: $stats.total_uncompressed_size, snapshots: $stats.snapshots_count},
      databases: {postgres: $postgres, sqlite: $sqlite, redis: $redis},
      check: {kind: (if $check == "" then "structure" else $check end), result: $check_result},
      warnings: $warnings, repairs: $repairs,
      par2: {repo_files: ($files | tonumber? // null), sets_created: ($made | tonumber? // null), sets_deleted: ($gone | tonumber? // null)},
      sha1_check: {match: ($l_match | tonumber? // null), mismatched: ($l_bad | tonumber? // null),
                   unverified: ($l_unver | tonumber? // null), missing: ($l_missing | tonumber? // null)}}' \
    > "$m/run.json" 2>/dev/null || warn "could not write the status numbers"
  log "uploaded to $RESTIC_REPOSITORY"
fi
exit $STATUS
