mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-28 07:42:51 +08:00
1039 lines
37 KiB
Bash
Executable File
1039 lines
37 KiB
Bash
Executable File
#!/bin/sh
|
|
# shellcheck shell=sh
|
|
#
|
|
# gitbackup CLI. Exit codes, shared with every module: 0 ok, 1 general error,
|
|
# 2 invalid configuration, 3 network or authentication, 4 refused for safety.
|
|
|
|
set -u
|
|
|
|
GB_SHARE="${GB_SHARE:-/usr/share/gitbackup}"
|
|
GB_STATE_DIR="${GB_STATE_DIR:-/var/run/gitbackup}"
|
|
|
|
# shellcheck disable=SC1091 # GB_SHARE is a runtime path, not resolvable statically
|
|
. "$GB_SHARE/lib.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/device.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/remoteurl.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/visibility.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/auth.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/collect.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/scrub.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/gitio.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/restore.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/paths.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/schedule.sh"
|
|
# shellcheck disable=SC1091
|
|
. "$GB_SHARE/card.sh"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: gitbackup <command> [options]
|
|
|
|
Commands:
|
|
run run a backup now
|
|
status print current status as JSON
|
|
collect build a backup set and manifest without pushing
|
|
diff show what changed since the last successful run, or [from to]
|
|
(two full commit shas) for the diff between two backups
|
|
history list this device's own backup commits as JSON
|
|
keygen generate the SSH deploy key ([--force [--confirm <fingerprint>]]
|
|
to replace an existing one -- --force alone only shows its
|
|
fingerprint and refuses; naming it back with --confirm proceeds)
|
|
pubkey print the deploy key's public key
|
|
test verify connectivity and remote configuration
|
|
hostkey show|accept <fingerprint> -- confirm the remote's SSH host key
|
|
without an interactive terminal (what the web UI uses)
|
|
paths list [--json]|add|del|audit -- edit /etc/sysupgrade.conf
|
|
restore restore configuration from a commit
|
|
card print a recovery card
|
|
log show recent gitbackup log entries
|
|
EOF
|
|
}
|
|
|
|
# Guards every caller-supplied commit before it reaches a bare git argument:
|
|
# an option-shaped value ("--upload-pack=...") must never get there.
|
|
_gb_valid_full_sha() {
|
|
case "$1" in
|
|
''|*[!0-9a-f]*) return 1 ;;
|
|
esac
|
|
# The charset check alone passes abbreviated or overlong shas.
|
|
[ "${#1}" -eq 40 ]
|
|
}
|
|
|
|
# gb_remote -- sets GB_SCHEME, GB_HOST, GB_PORT; exit 2 on an unsupported URL.
|
|
gb_remote() {
|
|
_gb_parsed=$(gb_parse_url "$GB_URL") ||
|
|
gb_die 2 "gitbackup.origin.url: '$GB_URL' does not match any supported remote URL form"
|
|
# shellcheck disable=SC2086 # word-splitting is the point: five fields from one line
|
|
set -- $_gb_parsed
|
|
GB_SCHEME="$1" GB_HOST="$2" GB_PORT="$3"
|
|
}
|
|
|
|
gb_branch() {
|
|
gb_expand "$(gb_uci_get gitbackup.origin.branch 'device/{device}')"
|
|
}
|
|
|
|
# gb_validate_config -- strict config check for commands that touch the
|
|
# remote: exit 2 on the first unusable field. Sets GB_DEVICE, GB_URL, GB_SCRUB.
|
|
gb_validate_config() {
|
|
gb_status_config
|
|
case " $GB_MISSING " in
|
|
*' device_id '*) GB_DEVICE=$(gb_device_id) || exit $? ;;
|
|
esac
|
|
case " $GB_MISSING " in
|
|
*' cron_expr '*) gb_die 2 "gitbackup.main.cron_expr: $(gb_cron_valid "$(gb_uci_get gitbackup.main.cron_expr)" 2>&1)" ;;
|
|
esac
|
|
[ -n "$GB_URL" ] || gb_die 2 'gitbackup.origin.url: empty, a repository url is required'
|
|
}
|
|
|
|
# _gb_effective_scrub -- a public remote is always scrubbed. Overrides only
|
|
# this run's decision, never the saved gitbackup.security.scrub.
|
|
_gb_effective_scrub() {
|
|
_gb_vis=$(gb_uci_get gitbackup.origin.visibility private)
|
|
_gb_scrub=$(gb_uci_get gitbackup.security.scrub 0)
|
|
if [ "$_gb_vis" = public ] && [ "$(gb_json_bool "$_gb_scrub")" != true ]; then
|
|
gb_log notice 'gitbackup.origin.visibility=public forces security.scrub on for this run'
|
|
_gb_scrub=1
|
|
fi
|
|
printf '%s\n' "$_gb_scrub"
|
|
}
|
|
|
|
# gb_status_config -- never exits: unusable fields go to GB_MISSING and
|
|
# GB_CONFIGURED is false.
|
|
gb_status_config() {
|
|
GB_MISSING=''
|
|
|
|
if ! GB_DEVICE=$(gb_device_id 2>/dev/null); then
|
|
GB_DEVICE=''
|
|
GB_MISSING="$GB_MISSING device_id"
|
|
fi
|
|
|
|
_gb_sched=$(gb_uci_get gitbackup.main.schedule daily)
|
|
if [ "$_gb_sched" = cron ]; then
|
|
_gb_cron=$(gb_uci_get gitbackup.main.cron_expr)
|
|
gb_cron_valid "$_gb_cron" >/dev/null 2>&1 || GB_MISSING="$GB_MISSING cron_expr"
|
|
fi
|
|
|
|
GB_URL=$(gb_uci_get gitbackup.origin.url)
|
|
[ -n "$GB_URL" ] || GB_MISSING="$GB_MISSING url"
|
|
|
|
GB_SCRUB=$(_gb_effective_scrub)
|
|
|
|
GB_CONFIGURED=true
|
|
[ -z "$GB_MISSING" ] || GB_CONFIGURED=false
|
|
}
|
|
|
|
cmd_status() {
|
|
_gb_enabled=$(gb_uci_get gitbackup.main.enabled 0)
|
|
_gb_token_file=$(gb_uci_get gitbackup.origin.token_file /etc/gitbackup/token)
|
|
_gb_token_set=false
|
|
[ -s "$_gb_token_file" ] && _gb_token_set=true
|
|
|
|
_gb_last_run='null'
|
|
[ -f "$GB_STATE_DIR/last_run" ] && _gb_last_run=$(cat "$GB_STATE_DIR/last_run")
|
|
|
|
printf '{\n'
|
|
printf ' "enabled": %s,\n' "$(gb_json_bool "$_gb_enabled")"
|
|
printf ' "configured": %s,\n' "$GB_CONFIGURED"
|
|
printf ' "device": %s,\n' "$(gb_json_str "$GB_DEVICE")"
|
|
printf ' "scrub": %s,\n' "$(gb_json_bool "$GB_SCRUB")"
|
|
printf ' "token_set": %s,\n' "$_gb_token_set"
|
|
printf ' "last_run": %s,\n' "$_gb_last_run"
|
|
printf ' "missing": ['
|
|
_gb_first=1
|
|
for _gb_m in $GB_MISSING; do
|
|
[ "$_gb_first" -eq 1 ] || printf ','
|
|
printf '%s' "$(gb_json_str "$_gb_m")"
|
|
_gb_first=0
|
|
done
|
|
printf ']\n'
|
|
printf '}\n'
|
|
}
|
|
|
|
# cmd_test -- exit 3 covers network and auth (one slot in the contract); the
|
|
# message tells them apart. No gb_have_net precheck: git's error classifies.
|
|
cmd_test() {
|
|
gb_remote
|
|
_gb_scheme="$GB_SCHEME" _gb_host="$GB_HOST" _gb_port="$GB_PORT"
|
|
|
|
# rc 3 means "could not verify", never "safe".
|
|
gb_visibility_ok "$GB_URL"
|
|
case $? in
|
|
4) gb_die 4 "$GB_URL is publicly visible to an anonymous request; push refused" ;;
|
|
3) gb_die 3 "could not verify whether $GB_URL is public (network or provider API unreachable); try again once connectivity is confirmed" ;;
|
|
esac
|
|
|
|
# Host keys are accepted only here, never in `run`.
|
|
if [ "$_gb_scheme" = ssh ]; then
|
|
# gb_accept_hostkey sets _gb_port to the effective port (0 -> 22).
|
|
gb_accept_hostkey "$_gb_host" "$_gb_port"
|
|
case $? in
|
|
0) ;;
|
|
1) gb_die 3 "$_gb_host:$_gb_port host key was not accepted" ;;
|
|
3)
|
|
# No stdin to ask on (e.g. rpcd): not the same as a typed decline.
|
|
gb_die 3 "$_gb_host:$_gb_port host key needs confirmation before this can proceed -- run 'gitbackup hostkey show' (or use the web UI) to see its fingerprint, then 'gitbackup hostkey accept <fingerprint>' to trust it"
|
|
;;
|
|
*) gb_die 3 "$_gb_host:$_gb_port host key could not be obtained -- is the network reachable?" ;;
|
|
esac
|
|
fi
|
|
|
|
# gb_git_env quotes its output for eval.
|
|
eval "$(gb_git_env)"
|
|
if _gb_err=$(git ls-remote "$GB_URL" 2>&1 >/dev/null); then
|
|
gb_log notice "gitbackup test: $GB_URL is reachable and authenticated"
|
|
# ls-remote proved the current key works: drop the `keygen --force` backup.
|
|
gb_keygen_forget_old
|
|
printf 'ok: %s is reachable and authenticated\n' "$GB_URL"
|
|
return 0
|
|
fi
|
|
|
|
_gb_test_classify "$_gb_host" "$_gb_err"
|
|
}
|
|
|
|
_gb_test_classify() {
|
|
_gb_h="$1"
|
|
_gb_e="$2"
|
|
case "$_gb_e" in
|
|
*'Could not resolve host'*|*'Failed to connect'*|*'Connection timed out'*| \
|
|
*'Connection refused'*|*'Network is unreachable'*|*'ssh: connect to host'*|*'No route to host'*)
|
|
gb_die 3 "cannot reach $_gb_h: $_gb_e"
|
|
;;
|
|
*)
|
|
gb_die 3 "authentication to $GB_URL failed: $_gb_e"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# cmd_hostkey show|accept <fingerprint> -- host key confirmation for the web
|
|
# UI (rpcd runs `test` with no stdin). ssh:// only.
|
|
cmd_hostkey() {
|
|
_gb_hk_sub="${1:-}"
|
|
[ -n "$_gb_hk_sub" ] && shift
|
|
_gb_hk_fp="${1:-}"
|
|
|
|
gb_remote
|
|
_gb_hk_scheme="$GB_SCHEME" _gb_hk_host="$GB_HOST" _gb_hk_port="$GB_PORT"
|
|
[ "$_gb_hk_port" != 0 ] || _gb_hk_port=22
|
|
|
|
if [ "$_gb_hk_scheme" != ssh ]; then
|
|
gb_die 2 "gitbackup.origin.url uses $_gb_hk_scheme://, not ssh:// -- there is no host key to show or accept"
|
|
fi
|
|
|
|
case "$_gb_hk_sub" in
|
|
show)
|
|
_gb_hk_out=$(gb_hostkey_show "$_gb_hk_host" "$_gb_hk_port")
|
|
_gb_hk_rc=$?
|
|
[ "$_gb_hk_rc" -eq 0 ] ||
|
|
gb_die 3 "could not reach $_gb_hk_host:$_gb_hk_port to obtain its host key -- is the network reachable?"
|
|
|
|
# shellcheck disable=SC2086 # word-splitting is the point
|
|
set -- $_gb_hk_out
|
|
_gb_hk_state="$1"
|
|
if [ "$_gb_hk_state" = trusted ]; then
|
|
printf '{ "host": %s, "port": %s, "trusted": true }\n' \
|
|
"$(gb_json_str "$_gb_hk_host")" "$_gb_hk_port"
|
|
else
|
|
shift 3
|
|
_gb_hk_fp_text="$*"
|
|
printf '{ "host": %s, "port": %s, "trusted": false, "fingerprint": %s }\n' \
|
|
"$(gb_json_str "$_gb_hk_host")" "$_gb_hk_port" "$(gb_json_str "$_gb_hk_fp_text")"
|
|
fi
|
|
;;
|
|
accept)
|
|
[ -n "$_gb_hk_fp" ] ||
|
|
gb_die 2 'gitbackup hostkey accept: a fingerprint argument is required (the exact text "hostkey show" printed)'
|
|
gb_hostkey_accept "$_gb_hk_host" "$_gb_hk_port" "$_gb_hk_fp"
|
|
case $? in
|
|
0) printf '{ "ok": true }\n' ;;
|
|
4) gb_die 4 "fingerprint for $_gb_hk_host:$_gb_hk_port does not match the pending host key; refusing to record it" ;;
|
|
*) gb_die 1 "no pending host key for $_gb_hk_host:$_gb_hk_port -- run 'gitbackup hostkey show' first" ;;
|
|
esac
|
|
;;
|
|
*)
|
|
gb_die 2 "gitbackup hostkey: unknown subcommand '$_gb_hk_sub' (expected show or accept)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run -- cmd_run does the gates; _gb_run_backup the git half, treating GB_URL
|
|
# as opaque so it can be tested against a local bare repository.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Basenames of /etc/config/* entries differing in any field. sort | uniq -u:
|
|
# the image ships no comm/diff.
|
|
_gb_run_changed_configs() {
|
|
_gb_rcc_old="$1"
|
|
_gb_rcc_new="$2"
|
|
{
|
|
[ -r "$_gb_rcc_old" ] && grep '"path":"/etc/config/' "$_gb_rcc_old"
|
|
grep '"path":"/etc/config/' "$_gb_rcc_new"
|
|
} 2>/dev/null | sort | uniq -u | sed -n 's#.*"path":"/etc/config/\([^"]*\)".*#\1#p' | sort -u
|
|
}
|
|
|
|
_gb_run_write_message() {
|
|
_gb_rwm_file="$1"
|
|
_gb_rwm_trigger="$2"
|
|
_gb_rwm_list="$3"
|
|
_gb_rwm_count="$4"
|
|
|
|
_gb_rwm_when=$(date -u +'%Y-%m-%d %H:%M')
|
|
|
|
_gb_rwm_short=$(printf '%s\n' "$_gb_rwm_list" | sed -n '1,3p' | gb_join_lines ', ')
|
|
[ -n "$_gb_rwm_short" ] || _gb_rwm_short='config'
|
|
if [ "$_gb_rwm_count" -gt 3 ]; then
|
|
_gb_rwm_short="$_gb_rwm_short and $((_gb_rwm_count - 3)) more"
|
|
fi
|
|
|
|
_gb_rwm_paths=$(printf '%s\n' "$_gb_rwm_list" | sed -n '/./ s#^#/etc/config/#p' | gb_join_lines ', ')
|
|
[ -n "$_gb_rwm_paths" ] || _gb_rwm_paths='(no /etc/config/* entry changed)'
|
|
|
|
# The router's own file (not a backup's), so sourcing it is safe.
|
|
_gb_rwm_release=$(
|
|
DISTRIB_RELEASE='' DISTRIB_REVISION=''
|
|
# shellcheck disable=SC1090,SC1091
|
|
[ -r "${GB_ROOT:-}/etc/openwrt_release" ] && . "${GB_ROOT:-}/etc/openwrt_release"
|
|
printf '%s %s' "$DISTRIB_RELEASE" "$DISTRIB_REVISION"
|
|
)
|
|
|
|
{
|
|
printf '%s %s: %s\n' "$_gb_rwm_when" "$GB_DEVICE" "$_gb_rwm_short"
|
|
printf '\n'
|
|
printf 'Changed: %s\n' "$_gb_rwm_paths"
|
|
printf 'Trigger: %s\n' "$_gb_rwm_trigger"
|
|
printf 'OpenWrt: %s\n' "$_gb_rwm_release"
|
|
printf 'Device: %s\n' "$GB_DEVICE"
|
|
} >"$_gb_rwm_file"
|
|
}
|
|
|
|
# last_run and state.json live on tmpfs; git remains the source of truth.
|
|
_gb_run_write_state() {
|
|
_gb_rws_result="$1"
|
|
_gb_rws_reason="$2"
|
|
_gb_rws_commit="${3:-}"
|
|
_gb_rws_timings="${4:-{\"collect_s\":0,\"compare_s\":0,\"push_s\":0}}"
|
|
|
|
mkdir -p "$GB_STATE_DIR" 2>/dev/null
|
|
_gb_rws_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
printf '%s\n' "$(gb_json_str "$_gb_rws_now")" >"$GB_STATE_DIR/last_run" 2>/dev/null
|
|
{
|
|
printf '{\n'
|
|
printf ' "generated": %s,\n' "$(gb_json_str "$_gb_rws_now")"
|
|
printf ' "result": %s,\n' "$(gb_json_str "$_gb_rws_result")"
|
|
printf ' "reason": %s,\n' "$(gb_json_str "$_gb_rws_reason")"
|
|
printf ' "branch": %s,\n' "$(gb_json_str "${_gb_branch:-}")"
|
|
printf ' "commit": %s,\n' "$(gb_json_str "$_gb_rws_commit")"
|
|
printf ' "timings": %s\n' "$_gb_rws_timings"
|
|
printf '}\n'
|
|
} >"$GB_STATE_DIR/state.json" 2>/dev/null
|
|
}
|
|
|
|
# Pushes an index README to "main" if it does not exist yet. Only on a
|
|
# device's first push (saves an ls-remote per run); never fails the backup.
|
|
_gb_run_ensure_main_index() {
|
|
_gb_rmi_repodir="$1"
|
|
_gb_rmi_branch="$2"
|
|
|
|
# Clear the device branch's parent: main gets its own root commit.
|
|
GB_PARENT=''
|
|
|
|
_gb_rmi_head=$(gb_remote_head main 2>/dev/null)
|
|
_gb_rmi_rc=$?
|
|
[ "$_gb_rmi_rc" -eq 0 ] || return 0
|
|
[ -z "$_gb_rmi_head" ] || return 0
|
|
|
|
_gb_rmi_readme="$WORK/main-readme.md"
|
|
{
|
|
printf '# gitbackup\n\n'
|
|
printf 'This repository holds automatic router configuration backups. Each\n'
|
|
printf 'router pushes to its own branch here -- by default named\n'
|
|
# shellcheck disable=SC2016 # backticks are markdown code spans in
|
|
# the generated document, not attempted command substitution
|
|
printf '`device/<id>`, e.g. `%s` for the router that just created this page.\n\n' "$_gb_rmi_branch"
|
|
# shellcheck disable=SC2016
|
|
printf 'Open one of the `device/*` branches above: its own README.md carries\n'
|
|
printf 'that router'\''s ready-made recovery instructions, including a one-line\n'
|
|
printf 'command to restore a freshly flashed replacement.\n'
|
|
} >"$_gb_rmi_readme" 2>/dev/null
|
|
[ -s "$_gb_rmi_readme" ] || return 0
|
|
|
|
_gb_rmi_msg="$WORK/main-readme-message"
|
|
printf 'gitbackup: initialize repository index\n\nSee device/* branches for each router'\''s own backup and recovery\ninstructions.\n' \
|
|
>"$_gb_rmi_msg" 2>/dev/null
|
|
|
|
# gb_build_tree still requires a non-empty GB_PREFIX; the README goes in via
|
|
# the extra-file pair.
|
|
_gb_rmi_emptydir="$WORK/main-readme-empty"
|
|
rm -rf "$_gb_rmi_emptydir"
|
|
mkdir -p "$_gb_rmi_emptydir" 2>/dev/null
|
|
|
|
_gb_rmi_tree=$(GB_PREFIX='.' gb_build_tree "$_gb_rmi_repodir" "$_gb_rmi_emptydir" "$_gb_rmi_readme" 'README.md' 2>/dev/null)
|
|
[ -n "$_gb_rmi_tree" ] || return 0
|
|
|
|
gb_commit_push "$_gb_rmi_repodir" "$_gb_rmi_tree" '' "$_gb_rmi_msg" main >/dev/null 2>&1
|
|
return 0
|
|
}
|
|
|
|
_gb_run_backup() {
|
|
_gb_trigger="$1"
|
|
|
|
_gb_branch=$(gb_branch)
|
|
_gb_prefix=$(gb_expand "$(gb_uci_get gitbackup.main.path_prefix 'devices/{device}')")
|
|
# shellcheck disable=SC2034 # read by gb_build_tree (gitio.sh) as an environment seam, not used in this file
|
|
GB_PREFIX="$_gb_prefix"
|
|
|
|
_gb_outdir="$WORK/tree"
|
|
_gb_repodir="$WORK/repo"
|
|
# A brand-new branch has no parent to fetch, but the plumbing still
|
|
# needs a .git here.
|
|
[ -d "$_gb_repodir/.git" ] || git init -q "$_gb_repodir" ||
|
|
gb_die 1 "gitbackup run: git init $_gb_repodir failed"
|
|
|
|
_gb_t_collect_start=$(date +%s)
|
|
gb_collect "$_gb_outdir" || gb_die 1 'gitbackup run: gb_collect failed'
|
|
if [ "$(gb_json_bool "$GB_SCRUB")" = true ]; then
|
|
gb_scrub "$_gb_outdir" 2>/dev/null | while IFS= read -r _gb_sl || [ -n "$_gb_sl" ]; do
|
|
[ -n "$_gb_sl" ] && gb_log info "gitbackup run: scrubbed $_gb_sl"
|
|
done
|
|
fi
|
|
_gb_t_collect_end=$(date +%s)
|
|
|
|
# GB_PARENT is an environment input to gb_build_tree.
|
|
_gb_t_compare_start=$(date +%s)
|
|
GB_PARENT=$(gb_remote_head "$_gb_branch")
|
|
_gb_head_rc=$?
|
|
[ "$_gb_head_rc" -eq 0 ] || gb_die 3 "gitbackup run: could not reach $GB_URL to resolve $_gb_branch"
|
|
# Captured before the retry loop can reassign GB_PARENT.
|
|
_gb_branch_was_new=""
|
|
[ -z "$GB_PARENT" ] && _gb_branch_was_new=1
|
|
|
|
_gb_old_manifest="$WORK/old_manifest.json"
|
|
_gb_changed=1
|
|
if [ -n "$GB_PARENT" ]; then
|
|
gb_fetch_meta "$_gb_branch" "$_gb_repodir" ||
|
|
gb_die 3 "gitbackup run: could not fetch $_gb_branch metadata from $GB_URL"
|
|
if git --git-dir="$_gb_repodir/.git" cat-file -p "$GB_PARENT:$_gb_prefix/manifest.json" \
|
|
>"$_gb_old_manifest" 2>/dev/null; then
|
|
if gb_manifest_equal "$_gb_old_manifest" "$(gb_manifest_path "$_gb_outdir")"; then
|
|
_gb_changed=0
|
|
fi
|
|
fi
|
|
fi
|
|
_gb_t_compare_end=$(date +%s)
|
|
_gb_t_collect_s=$((_gb_t_collect_end - _gb_t_collect_start))
|
|
_gb_t_compare_s=$((_gb_t_compare_end - _gb_t_compare_start))
|
|
|
|
if [ "$_gb_changed" -eq 0 ]; then
|
|
gb_log notice "gitbackup run: no changes since the last backup on $_gb_branch"
|
|
printf 'no changes\n'
|
|
_gb_run_write_state ok no-changes "$GB_PARENT" \
|
|
"{\"collect_s\":$_gb_t_collect_s,\"compare_s\":$_gb_t_compare_s,\"push_s\":0}"
|
|
gb_log notice "gitbackup run: timings -- collect ${_gb_t_collect_s}s, compare ${_gb_t_compare_s}s, push 0s"
|
|
exit 0
|
|
fi
|
|
|
|
# Archive only after the compare: sysupgrade -b's gzip is not
|
|
# byte-deterministic. Never when scrubbing: it reads the LIVE filesystem and
|
|
# would carry every value the scrub just removed.
|
|
if [ "$(gb_json_bool "$(gb_uci_get gitbackup.main.archive 1)")" = true ]; then
|
|
if [ "$(gb_json_bool "$GB_SCRUB")" = true ]; then
|
|
gb_log notice 'gitbackup run: scrubbing is on, so no backup.tar.gz is pushed -- sysupgrade -b reads the live filesystem and would carry exactly the values just scrubbed out of the tree'
|
|
else
|
|
sysupgrade -b "$_gb_outdir/backup.tar.gz" >/dev/null 2>&1 ||
|
|
gb_log warning 'gitbackup run: sysupgrade -b failed, continuing without backup.tar.gz'
|
|
fi
|
|
fi
|
|
|
|
# One gb_card call for both RECOVERY.md and the branch-root README.md, so
|
|
# the copies cannot drift.
|
|
_gb_readme_root=''
|
|
gb_card "$_gb_outdir/RECOVERY.md" 2>/dev/null
|
|
if [ -s "$_gb_outdir/RECOVERY.md" ]; then
|
|
_gb_readme_root="$WORK/branch-readme.md"
|
|
cp -f "$_gb_outdir/RECOVERY.md" "$_gb_readme_root" 2>/dev/null || _gb_readme_root=''
|
|
fi
|
|
|
|
_gb_changed_list=$(_gb_run_changed_configs "$_gb_old_manifest" "$(gb_manifest_path "$_gb_outdir")")
|
|
_gb_changed_count=$(printf '%s\n' "$_gb_changed_list" | grep -c .)
|
|
_gb_msgfile="$WORK/commit-message"
|
|
_gb_run_write_message "$_gb_msgfile" "$_gb_trigger" "$_gb_changed_list" "$_gb_changed_count"
|
|
|
|
# On non-fast-forward retry once on the new parent, reusing the collected set.
|
|
_gb_t_push_start=$(date +%s)
|
|
_gb_attempt=1
|
|
while :; do
|
|
_gb_tree=$(gb_build_tree "$_gb_repodir" "$_gb_outdir" "$_gb_readme_root" 'README.md')
|
|
[ -n "$_gb_tree" ] || gb_die 1 'gitbackup run: gb_build_tree failed'
|
|
|
|
_gb_commit=$(gb_commit_push "$_gb_repodir" "$_gb_tree" "$GB_PARENT" "$_gb_msgfile" "$_gb_branch")
|
|
_gb_push_rc=$?
|
|
case "$_gb_push_rc" in
|
|
0) break ;;
|
|
2)
|
|
if [ "$_gb_attempt" -ge 2 ]; then
|
|
gb_die 1 "gitbackup run: $_gb_branch changed again during the retry, giving up after one retry"
|
|
fi
|
|
gb_log notice "gitbackup run: $_gb_branch moved under us, retrying once"
|
|
_gb_attempt=2
|
|
GB_PARENT=$(gb_remote_head "$_gb_branch")
|
|
_gb_head_rc=$?
|
|
[ "$_gb_head_rc" -eq 0 ] || gb_die 3 "gitbackup run: could not re-resolve $_gb_branch for the retry"
|
|
gb_fetch_meta "$_gb_branch" "$_gb_repodir" ||
|
|
gb_die 3 "gitbackup run: could not re-fetch $_gb_branch metadata for the retry"
|
|
;;
|
|
1) gb_die 1 'gitbackup run: gb_commit_push could not build the commit, see log' ;;
|
|
*) gb_die 3 'gitbackup run: push failed, see log' ;;
|
|
esac
|
|
done
|
|
_gb_t_push_end=$(date +%s)
|
|
_gb_t_push_s=$((_gb_t_push_end - _gb_t_push_start))
|
|
|
|
gb_log notice "gitbackup run: pushed $_gb_commit to $_gb_branch"
|
|
printf 'pushed %s to %s\n' "$_gb_commit" "$_gb_branch"
|
|
_gb_run_write_state ok pushed "$_gb_commit" \
|
|
"{\"collect_s\":$_gb_t_collect_s,\"compare_s\":$_gb_t_compare_s,\"push_s\":$_gb_t_push_s}"
|
|
gb_log notice "gitbackup run: timings -- collect ${_gb_t_collect_s}s, compare ${_gb_t_compare_s}s, push ${_gb_t_push_s}s"
|
|
|
|
if [ -n "$_gb_branch_was_new" ]; then
|
|
_gb_run_ensure_main_index "$_gb_repodir" "$_gb_branch"
|
|
fi
|
|
}
|
|
|
|
# cmd_run [--trigger cron|manual|procd|luci] -- the crontab line passes no
|
|
# flag, hence the cron default.
|
|
cmd_run() {
|
|
_gb_trigger=cron
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--trigger)
|
|
shift
|
|
[ "$#" -gt 0 ] && { _gb_trigger="$1"; shift; }
|
|
;;
|
|
--trigger=*) _gb_trigger="${1#--trigger=}"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
# Non-blocking flock on an fd. Busy -> skipped, exit 0: cron must not
|
|
# report an overlap that resolved itself.
|
|
_gb_lockfile="${GB_LOCK_FILE:-/var/run/gitbackup.lock}"
|
|
mkdir -p "$(dirname "$_gb_lockfile")" 2>/dev/null
|
|
exec 9>"$_gb_lockfile" || gb_die 1 "gitbackup run: cannot open lock file $_gb_lockfile"
|
|
if ! flock -n 9; then
|
|
gb_log notice 'gitbackup run: another run already holds the lock, skipped'
|
|
printf 'skipped: another run is already in progress\n'
|
|
exit 0
|
|
fi
|
|
|
|
gb_remote
|
|
_gb_scheme="$GB_SCHEME" _gb_host="$GB_HOST" _gb_port="$GB_PORT"
|
|
_gb_netport="$_gb_port"
|
|
if [ "$_gb_netport" = 0 ]; then
|
|
if [ "$_gb_scheme" = ssh ]; then _gb_netport=22; else _gb_netport=443; fi
|
|
fi
|
|
if ! gb_have_net "$_gb_host" "$_gb_netport"; then
|
|
gb_log notice "gitbackup run: $_gb_host:$_gb_netport unreachable, skipped"
|
|
printf 'skipped: %s is unreachable\n' "$_gb_host"
|
|
exit 0
|
|
fi
|
|
|
|
# $WORK holds a plaintext backup set and briefly credentials: /tmp only
|
|
# (never flash/USB), removed on any exit.
|
|
WORK=$(mktemp -d "${TMPDIR:-/tmp}/gitbackup.XXXXXX") ||
|
|
gb_die 1 'gitbackup run: cannot create a working directory under /tmp'
|
|
trap 'rm -rf "$WORK"' EXIT INT TERM
|
|
|
|
_gb_set_kb=$(gb_paths_size_kb)
|
|
_gb_need_kb=$((_gb_set_kb * 2 + 1024))
|
|
# TMPDIR may override /tmp: check the filesystem $WORK lives on.
|
|
_gb_free_kb=$(gb_free_kb "${TMPDIR:-/tmp}")
|
|
case "$_gb_free_kb" in '' | *[!0-9]*) _gb_free_kb=0 ;; esac
|
|
if [ "$_gb_free_kb" -lt "$_gb_need_kb" ]; then
|
|
gb_die 1 "gitbackup run: not enough space in /tmp -- need ~${_gb_need_kb} KB (backup set ~${_gb_set_kb} KB x2 + 1024 KB headroom), have ${_gb_free_kb} KB free; reduce the backup set (e.g. gitbackup.main.archive='0', or trim the sysupgrade path list) or free up /tmp"
|
|
fi
|
|
|
|
# Visibility gate: 3 (could not verify) is skipped like no network, never
|
|
# treated as private; 4 (public) always fails loudly.
|
|
gb_visibility_ok "$GB_URL"
|
|
case $? in
|
|
4) gb_die 4 "$GB_URL is publicly visible to an anonymous request; push refused" ;;
|
|
3)
|
|
gb_log notice "gitbackup run: could not verify whether $GB_URL is public, skipped"
|
|
printf 'skipped: could not verify repository visibility\n'
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
eval "$(gb_git_env)"
|
|
|
|
_gb_run_backup "$_gb_trigger"
|
|
}
|
|
|
|
# cmd_restore -- no gb_validate_config: a bare router may not resolve its
|
|
# device id. Only the URL is required.
|
|
cmd_restore() {
|
|
_gb_r_device=''
|
|
_gb_r_commit=''
|
|
_gb_r_flags=''
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--device) shift; _gb_r_device="${1:-}"; shift ;;
|
|
--device=*) _gb_r_device="${1#--device=}"; shift ;;
|
|
--commit) shift; _gb_r_commit="${1:-}"; shift ;;
|
|
--commit=*) _gb_r_commit="${1#--commit=}"; shift ;;
|
|
--dry-run|--force|--yes|--with-packages) _gb_r_flags="$_gb_r_flags $1"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
# No --device: restore this router's own backup.
|
|
[ -n "$_gb_r_device" ] || _gb_r_device=$(gb_device_id) || exit $?
|
|
|
|
GB_URL=$(gb_uci_get gitbackup.origin.url)
|
|
[ -n "$GB_URL" ] || gb_die 2 'gitbackup.origin.url: empty, a repository url is required'
|
|
|
|
gb_remote
|
|
|
|
# --commit reaches bare git arguments: allow only empty/HEAD or a full sha.
|
|
if [ -n "$_gb_r_commit" ] && [ "$_gb_r_commit" != HEAD ]; then
|
|
_gb_valid_full_sha "$_gb_r_commit" ||
|
|
gb_die 2 "gitbackup restore: --commit '$_gb_r_commit' must be a full 40-character commit sha, or omitted/HEAD for the branch tip"
|
|
fi
|
|
|
|
eval "$(gb_git_env)"
|
|
|
|
# shellcheck disable=SC2086 # word-splitting is the point: $_gb_r_flags is zero or more bare flag tokens
|
|
gb_restore "$_gb_r_device" "$_gb_r_commit" $_gb_r_flags
|
|
exit $?
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# history -- the branch's commits as JSON.
|
|
# ---------------------------------------------------------------------------
|
|
cmd_history() {
|
|
_gb_h_limit=20
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--limit) shift; _gb_h_limit="${1:-20}"; shift ;;
|
|
--limit=*) _gb_h_limit="${1#--limit=}"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
case "$_gb_h_limit" in ''|*[!0-9]*) _gb_h_limit=20 ;; esac
|
|
|
|
gb_remote
|
|
|
|
_gb_h_branch=$(gb_branch) ||
|
|
gb_die 2 'gitbackup.origin.branch could not be expanded'
|
|
|
|
eval "$(gb_git_env)"
|
|
|
|
_gb_h_head=$(gb_remote_head "$_gb_h_branch")
|
|
_gb_h_head_rc=$?
|
|
[ "$_gb_h_head_rc" -eq 0 ] || gb_die 3 "gitbackup history: could not reach $GB_URL to resolve $_gb_h_branch"
|
|
if [ -z "$_gb_h_head" ]; then
|
|
printf '{ "branch": %s, "commits": [] }\n' "$(gb_json_str "$_gb_h_branch")"
|
|
return 0
|
|
fi
|
|
|
|
_gb_h_dir=$(mktemp -d "${TMPDIR:-/tmp}/gitbackup-history.XXXXXX") ||
|
|
gb_die 1 'gitbackup history: cannot create a scratch directory under /tmp'
|
|
trap 'rm -rf "$_gb_h_dir"' EXIT INT TERM
|
|
|
|
# Full history (empty depth), not gb_fetch_meta's --depth=1 default.
|
|
gb_fetch_meta "$_gb_h_branch" "$_gb_h_dir" '' ||
|
|
gb_die 3 "gitbackup history: could not fetch $_gb_h_branch from $GB_URL"
|
|
|
|
_gb_h_tab=$(printf '\t')
|
|
printf '{ "branch": %s, "commits": [' "$(gb_json_str "$_gb_h_branch")"
|
|
_gb_h_first=1
|
|
git -C "$_gb_h_dir" log -n "$_gb_h_limit" --format='%H%x09%s' FETCH_HEAD 2>/dev/null |
|
|
while IFS= read -r _gb_h_line || [ -n "$_gb_h_line" ]; do
|
|
_gb_h_sha="${_gb_h_line%%"$_gb_h_tab"*}"
|
|
_gb_h_subject="${_gb_h_line#*"$_gb_h_tab"}"
|
|
[ "$_gb_h_first" -eq 1 ] || printf ','
|
|
_gb_h_first=0
|
|
printf '{ "sha": %s, "subject": %s, "changed": [' \
|
|
"$(gb_json_str "$_gb_h_sha")" "$(gb_json_str "$_gb_h_subject")"
|
|
_gb_h_cfirst=1
|
|
# --root: without it diff-tree prints nothing for a root commit.
|
|
git -C "$_gb_h_dir" diff-tree --no-commit-id --name-only -r --root "$_gb_h_sha" 2>/dev/null |
|
|
while IFS= read -r _gb_h_path || [ -n "$_gb_h_path" ]; do
|
|
[ -n "$_gb_h_path" ] || continue
|
|
[ "$_gb_h_cfirst" -eq 1 ] || printf ','
|
|
_gb_h_cfirst=0
|
|
printf '%s' "$(gb_json_str "$_gb_h_path")"
|
|
done
|
|
printf '] }'
|
|
done
|
|
printf '] }\n'
|
|
|
|
rm -rf "$_gb_h_dir"
|
|
trap - EXIT INT TERM
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# collect -- gb_collect into --out for inspection; nothing pushed or scrubbed.
|
|
# ---------------------------------------------------------------------------
|
|
cmd_collect() {
|
|
_gb_c_out=''
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--out) shift; _gb_c_out="${1:-}"; shift ;;
|
|
--out=*) _gb_c_out="${1#--out=}"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[ -n "$_gb_c_out" ] || gb_die 2 'gitbackup collect: --out DIR is required'
|
|
|
|
mkdir -p "$_gb_c_out" || gb_die 1 "gitbackup collect: cannot create $_gb_c_out"
|
|
gb_collect "$_gb_c_out" || gb_die 1 'gitbackup collect: gb_collect failed'
|
|
printf 'collected backup set into %s\n' "$_gb_c_out"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# diff -- live config vs the last commit by manifest.json, never git: git
|
|
# tracks only three file modes and misses a chmod.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# A manifest path never holds a raw tab.
|
|
_gb_diff_dump_line() {
|
|
_gb_dd_path=$(gb_manifest_field "$1" path)
|
|
[ -z "$_gb_dd_path" ] || printf '%s\t%s\n' "$_gb_dd_path" "$1"
|
|
}
|
|
|
|
_gb_diff_dump() {
|
|
if [ -r "$1" ]; then gb_manifest_each "$1" entries _gb_diff_dump_line; fi | sort >"$2"
|
|
}
|
|
|
|
_gb_diff_describe_change() {
|
|
_gb_ddc_old="$1"
|
|
_gb_ddc_new="$2"
|
|
_gb_ddc_path="$3"
|
|
_gb_ddc_parts=''
|
|
|
|
_gb_ddc_ot=$(gb_manifest_field "$_gb_ddc_old" type)
|
|
_gb_ddc_nt=$(gb_manifest_field "$_gb_ddc_new" type)
|
|
[ "$_gb_ddc_ot" = "$_gb_ddc_nt" ] || _gb_ddc_parts="$_gb_ddc_parts type $_gb_ddc_ot->$_gb_ddc_nt;"
|
|
|
|
_gb_ddc_om=$(gb_manifest_field "$_gb_ddc_old" mode)
|
|
_gb_ddc_nm=$(gb_manifest_field "$_gb_ddc_new" mode)
|
|
[ "$_gb_ddc_om" = "$_gb_ddc_nm" ] || _gb_ddc_parts="$_gb_ddc_parts mode $_gb_ddc_om->$_gb_ddc_nm;"
|
|
|
|
_gb_ddc_ou=$(gb_manifest_field "$_gb_ddc_old" uid)
|
|
_gb_ddc_nu=$(gb_manifest_field "$_gb_ddc_new" uid)
|
|
[ "$_gb_ddc_ou" = "$_gb_ddc_nu" ] || _gb_ddc_parts="$_gb_ddc_parts uid $_gb_ddc_ou->$_gb_ddc_nu;"
|
|
|
|
_gb_ddc_og=$(gb_manifest_field "$_gb_ddc_old" gid)
|
|
_gb_ddc_ng=$(gb_manifest_field "$_gb_ddc_new" gid)
|
|
[ "$_gb_ddc_og" = "$_gb_ddc_ng" ] || _gb_ddc_parts="$_gb_ddc_parts gid $_gb_ddc_og->$_gb_ddc_ng;"
|
|
|
|
_gb_ddc_osha=$(gb_manifest_field "$_gb_ddc_old" sha256)
|
|
_gb_ddc_nsha=$(gb_manifest_field "$_gb_ddc_new" sha256)
|
|
[ "$_gb_ddc_osha" = "$_gb_ddc_nsha" ] || _gb_ddc_parts="$_gb_ddc_parts content changed;"
|
|
|
|
_gb_ddc_otgt=$(gb_manifest_field "$_gb_ddc_old" target)
|
|
_gb_ddc_ntgt=$(gb_manifest_field "$_gb_ddc_new" target)
|
|
[ "$_gb_ddc_otgt" = "$_gb_ddc_ntgt" ] || _gb_ddc_parts="$_gb_ddc_parts symlink target $_gb_ddc_otgt->$_gb_ddc_ntgt;"
|
|
|
|
[ -n "$_gb_ddc_parts" ] && printf '~ %s:%s\n' "$_gb_ddc_path" "$_gb_ddc_parts"
|
|
}
|
|
|
|
# No join(1) on the image. No output = no changes.
|
|
_gb_diff_compare() {
|
|
_gb_dc_old="$1"
|
|
_gb_dc_new="$2"
|
|
{ cut -f1 "$_gb_dc_old" 2>/dev/null; cut -f1 "$_gb_dc_new" 2>/dev/null; } | sort -u |
|
|
while IFS= read -r _gb_dc_p || [ -n "$_gb_dc_p" ]; do
|
|
[ -n "$_gb_dc_p" ] || continue
|
|
_gb_dc_o=$(awk -F'\t' -v p="$_gb_dc_p" '$1==p{print $2; exit}' "$_gb_dc_old" 2>/dev/null)
|
|
_gb_dc_n=$(awk -F'\t' -v p="$_gb_dc_p" '$1==p{print $2; exit}' "$_gb_dc_new" 2>/dev/null)
|
|
if [ -z "$_gb_dc_o" ] && [ -n "$_gb_dc_n" ]; then
|
|
printf '+ %s (%s)\n' "$_gb_dc_p" "$(gb_manifest_field "$_gb_dc_n" type)"
|
|
elif [ -n "$_gb_dc_o" ] && [ -z "$_gb_dc_n" ]; then
|
|
printf -- '- %s (%s)\n' "$_gb_dc_p" "$(gb_manifest_field "$_gb_dc_o" type)"
|
|
elif [ "$_gb_dc_o" != "$_gb_dc_n" ]; then
|
|
_gb_diff_describe_change "$_gb_dc_o" "$_gb_dc_n" "$_gb_dc_p"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# cmd_diff [<from> <to>] -- no args: live vs last commit. Two full shas:
|
|
# `git diff` between two backups (History).
|
|
cmd_diff() {
|
|
gb_remote
|
|
|
|
if [ "$#" -eq 2 ]; then
|
|
_gb_d_from="$1"
|
|
_gb_d_to="$2"
|
|
_gb_valid_full_sha "$_gb_d_from" ||
|
|
gb_die 2 "gitbackup diff: 'from' must be a full 40-character commit sha, got '$_gb_d_from'"
|
|
_gb_valid_full_sha "$_gb_d_to" ||
|
|
gb_die 2 "gitbackup diff: 'to' must be a full 40-character commit sha, got '$_gb_d_to'"
|
|
|
|
_gb_dc_branch=$(gb_branch) ||
|
|
gb_die 2 'gitbackup.origin.branch could not be expanded'
|
|
|
|
_gb_dc_work=$(mktemp -d "${TMPDIR:-/tmp}/gitbackup-diffc.XXXXXX") ||
|
|
gb_die 1 'gitbackup diff: cannot create a working directory under /tmp'
|
|
trap 'rm -rf "$_gb_dc_work"' EXIT INT TERM
|
|
|
|
eval "$(gb_git_env)"
|
|
|
|
# Full history: either commit may be far back on the branch.
|
|
gb_fetch_meta "$_gb_dc_branch" "$_gb_dc_work" '' ||
|
|
gb_die 3 "gitbackup diff: could not fetch $_gb_dc_branch from $GB_URL"
|
|
|
|
# --end-of-options, not "--": for git diff "--" would turn the shas
|
|
# into pathspecs. Needs git >= 2.24.
|
|
_gb_dc_out=$(git -C "$_gb_dc_work" diff --end-of-options "$_gb_d_from" "$_gb_d_to" 2>&1)
|
|
_gb_dc_rc=$?
|
|
if [ "$_gb_dc_rc" -ne 0 ]; then
|
|
gb_die 3 "gitbackup diff: $_gb_dc_out"
|
|
fi
|
|
printf '%s\n' "$_gb_dc_out"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$#" -ne 0 ]; then
|
|
gb_die 2 'gitbackup diff: expected either no arguments (live config vs the last commit) or exactly two (from-sha to-sha)'
|
|
fi
|
|
|
|
_gb_d_branch=$(gb_branch)
|
|
_gb_d_prefix=$(gb_expand "$(gb_uci_get gitbackup.main.path_prefix 'devices/{device}')")
|
|
|
|
_gb_d_work=$(mktemp -d "${TMPDIR:-/tmp}/gitbackup-diff.XXXXXX") ||
|
|
gb_die 1 'gitbackup diff: cannot create a working directory under /tmp'
|
|
trap 'rm -rf "$_gb_d_work"' EXIT INT TERM
|
|
|
|
eval "$(gb_git_env)"
|
|
|
|
_gb_d_parent=$(gb_remote_head "$_gb_d_branch")
|
|
_gb_d_head_rc=$?
|
|
[ "$_gb_d_head_rc" -eq 0 ] || gb_die 3 "gitbackup diff: could not reach $GB_URL to resolve $_gb_d_branch"
|
|
|
|
_gb_d_outdir="$_gb_d_work/tree"
|
|
gb_collect "$_gb_d_outdir" || gb_die 1 'gitbackup diff: gb_collect failed'
|
|
if [ "$(gb_json_bool "$GB_SCRUB")" = true ]; then
|
|
# Scrub like `run` does, or every scrubbed field would show as changed.
|
|
gb_scrub "$_gb_d_outdir" >/dev/null 2>&1
|
|
fi
|
|
|
|
_gb_d_old_manifest="$_gb_d_work/old_manifest.json"
|
|
: >"$_gb_d_old_manifest"
|
|
if [ -n "$_gb_d_parent" ]; then
|
|
_gb_d_repodir="$_gb_d_work/repo"
|
|
gb_fetch_meta "$_gb_d_branch" "$_gb_d_repodir" ||
|
|
gb_die 3 "gitbackup diff: could not fetch $_gb_d_branch metadata from $GB_URL"
|
|
git --git-dir="$_gb_d_repodir/.git" cat-file -p "$_gb_d_parent:$_gb_d_prefix/manifest.json" \
|
|
>"$_gb_d_old_manifest" 2>/dev/null
|
|
fi
|
|
|
|
_gb_d_old_tsv="$_gb_d_work/old.tsv"
|
|
_gb_d_new_tsv="$_gb_d_work/new.tsv"
|
|
_gb_diff_dump "$_gb_d_old_manifest" "$_gb_d_old_tsv"
|
|
_gb_diff_dump "$(gb_manifest_path "$_gb_d_outdir")" "$_gb_d_new_tsv"
|
|
|
|
_gb_diff_compare "$_gb_d_old_tsv" "$_gb_d_new_tsv"
|
|
return 0
|
|
}
|
|
|
|
_gb_paths_json_array() {
|
|
_gb_pja_first=1
|
|
printf '%s\n' "$1" | while IFS= read -r _gb_pja_p || [ -n "$_gb_pja_p" ]; do
|
|
[ -n "$_gb_pja_p" ] || continue
|
|
[ "$_gb_pja_first" -eq 1 ] || printf ','
|
|
printf '%s' "$(gb_json_str "$_gb_pja_p")"
|
|
_gb_pja_first=0
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# paths -- list|add|del|audit over /etc/sysupgrade.conf via paths.sh. Not
|
|
# gated by gb_validate_config: works before a remote is configured.
|
|
# ---------------------------------------------------------------------------
|
|
cmd_paths() {
|
|
_gb_p_sub="${1:-list}"
|
|
[ "$#" -gt 0 ] && shift
|
|
|
|
case "$_gb_p_sub" in
|
|
list)
|
|
if [ "${1:-}" = '--json' ]; then
|
|
# "paths": the file verbatim. "entries": what to list and send back.
|
|
# "effective": read-only; saving it back would shrink a directory entry
|
|
# to the files present at save time.
|
|
_gb_p_raw=$(gb_paths_list)
|
|
_gb_p_entries=$(gb_paths_entries)
|
|
_gb_p_eff=$(sysupgrade -l 2>/dev/null)
|
|
printf '{ "paths": [%s], "entries": [%s], "effective": [%s], "size_kb": %s }\n' \
|
|
"$(_gb_paths_json_array "$_gb_p_raw")" "$(_gb_paths_json_array "$_gb_p_entries")" "$(_gb_paths_json_array "$_gb_p_eff")" "$(gb_paths_size_kb)"
|
|
else
|
|
gb_paths_list
|
|
printf '# total: %s KB (effective sysupgrade set, sysupgrade -l)\n' "$(gb_paths_size_kb)"
|
|
fi
|
|
;;
|
|
add)
|
|
_gb_p_path="${1:-}"
|
|
[ -n "$_gb_p_path" ] || gb_die 2 'gitbackup paths add: a path is required'
|
|
_gb_p_err=$(gb_paths_add "$_gb_p_path" 2>&1)
|
|
_gb_p_rc=$?
|
|
if [ "$_gb_p_rc" -eq 0 ]; then
|
|
printf 'added %s\n' "$_gb_p_path"
|
|
else
|
|
gb_die "$_gb_p_rc" "gitbackup paths add: $_gb_p_err"
|
|
fi
|
|
;;
|
|
del)
|
|
_gb_p_path="${1:-}"
|
|
[ -n "$_gb_p_path" ] || gb_die 2 'gitbackup paths del: a path is required'
|
|
gb_paths_del "$_gb_p_path"
|
|
printf 'removed %s\n' "$_gb_p_path"
|
|
;;
|
|
audit)
|
|
# Files changed since the base image (/overlay/upper) that are
|
|
# neither in `sysupgrade -l` nor gitbackup's own secrets.
|
|
# VERIFY: not checked against a real overlay mount; an absent
|
|
# directory yields an empty list.
|
|
_gb_p_upper="${GB_ROOT:-}/overlay/upper"
|
|
if [ ! -d "$_gb_p_upper" ]; then
|
|
printf '{ "paths": [] }\n'
|
|
return 0
|
|
fi
|
|
_gb_p_backed=$(sysupgrade -l 2>/dev/null)
|
|
printf '{ "paths": ['
|
|
_gb_p_first=1
|
|
( cd "$_gb_p_upper" 2>/dev/null && find . \( -type f -o -type l \) ) |
|
|
while IFS= read -r _gb_p_rel || [ -n "$_gb_p_rel" ]; do
|
|
_gb_p_abs="/${_gb_p_rel#./}"
|
|
case "$_gb_p_abs" in
|
|
/etc/gitbackup | /etc/gitbackup/*) continue ;;
|
|
esac
|
|
printf '%s\n' "$_gb_p_backed" | grep -qxF "$_gb_p_abs" && continue
|
|
[ "$_gb_p_first" -eq 1 ] || printf ','
|
|
_gb_p_first=0
|
|
printf '%s' "$(gb_json_str "$_gb_p_abs")"
|
|
done
|
|
printf '] }\n'
|
|
;;
|
|
*)
|
|
gb_die 2 "gitbackup paths: unknown subcommand '$_gb_p_sub' (expected list, add, del or audit)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# card -- write the recovery card to --out; gb_card never fails.
|
|
# ---------------------------------------------------------------------------
|
|
cmd_card() {
|
|
_gb_cd_out=''
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--out) shift; _gb_cd_out="${1:-}"; shift ;;
|
|
--out=*) _gb_cd_out="${1#--out=}"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[ -n "$_gb_cd_out" ] || gb_die 2 'gitbackup card: --out FILE is required'
|
|
gb_card "$_gb_cd_out"
|
|
printf 'wrote recovery card to %s\n' "$_gb_cd_out"
|
|
}
|
|
|
|
# cmd_log [lines] -- tag-filtered syslog (logread survives a reboot).
|
|
cmd_log() {
|
|
_gb_lines="${1:-50}"
|
|
case "$_gb_lines" in '' | *[!0-9]*) _gb_lines=50 ;; esac
|
|
logread -e gitbackup 2>/dev/null | tail -n "$_gb_lines"
|
|
}
|
|
|
|
[ "$#" -ge 1 ] || { usage; exit 0; }
|
|
|
|
cmd="$1"
|
|
shift
|
|
|
|
case "$cmd" in
|
|
status)
|
|
# Lenient: a fresh install's default config must not make status exit 2.
|
|
gb_status_config
|
|
cmd_status
|
|
;;
|
|
keygen)
|
|
# No gb_validate_config: the key is created before a remote URL exists.
|
|
_gb_force=''
|
|
_gb_confirm=''
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--force) _gb_force=1; shift ;;
|
|
--confirm) shift; _gb_confirm="${1:-}"; shift ;;
|
|
--confirm=*) _gb_confirm="${1#--confirm=}"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
_gb_kg_out=$(gb_keygen "$_gb_force" "$_gb_confirm")
|
|
_gb_kg_rc=$?
|
|
if [ "$_gb_kg_rc" -eq 4 ]; then
|
|
# Relay the "confirm-required <fingerprint>" line on stdout (rpcd
|
|
# parses it), then explain on stderr.
|
|
printf '%s\n' "$_gb_kg_out"
|
|
# shellcheck disable=SC2086 # word-splitting is the point: the fixed leading token, then the fingerprint text
|
|
set -- $_gb_kg_out
|
|
shift
|
|
gb_die 4 "$(gb_uci_get gitbackup.origin.key_file /etc/gitbackup/id_ed25519) already exists (fingerprint: $*) -- keygen --force destroys the private half beyond recovery, and the deploy key already added at your git provider stops working the instant it does; re-run 'gitbackup keygen --force --confirm \"$*\"' to proceed, then add the NEW public key at the provider"
|
|
fi
|
|
[ "$_gb_kg_rc" -eq 0 ] || exit 1
|
|
printf 'generated %s\n' "$(gb_uci_get gitbackup.origin.key_file /etc/gitbackup/id_ed25519)"
|
|
;;
|
|
pubkey)
|
|
# Needs no remote configuration either.
|
|
gb_pubkey || exit 1
|
|
;;
|
|
test)
|
|
gb_validate_config
|
|
cmd_test
|
|
;;
|
|
hostkey)
|
|
gb_validate_config
|
|
cmd_hostkey "$@"
|
|
;;
|
|
run)
|
|
gb_validate_config
|
|
cmd_run "$@"
|
|
;;
|
|
log)
|
|
# No gb_validate_config: must work on a half-configured router.
|
|
cmd_log "$@"
|
|
;;
|
|
restore)
|
|
cmd_restore "$@"
|
|
;;
|
|
collect|diff|card)
|
|
gb_validate_config
|
|
case "$cmd" in
|
|
collect) cmd_collect "$@" ;;
|
|
diff) cmd_diff "$@" ;;
|
|
card) cmd_card "$@" ;;
|
|
esac
|
|
;;
|
|
history)
|
|
gb_validate_config
|
|
cmd_history "$@"
|
|
;;
|
|
paths)
|
|
# No gb_validate_config: works before a remote is configured.
|
|
cmd_paths "$@"
|
|
;;
|
|
*)
|
|
usage >&2
|
|
gb_die 1 "unknown command '$cmd'"
|
|
;;
|
|
esac
|