fix(mackup): update backup path and enhance configuration handling
- Corrected the backup path in the `_in_backup` function to reflect the new structure under `mackup/`. - Improved error handling in `ssr::mackup::configure` to skip restore if the configuration file is not updated, providing user feedback. - Updated `ssr::sync` to conditionally execute backup based on configuration validity, enhancing robustness. - Revised documentation in configuration examples to clarify the new Mackup directory structure and its implications for users.
This commit is contained in:
+1
-1
@@ -71,7 +71,7 @@ cmd_scan() {
|
|||||||
done <<< "$declared"
|
done <<< "$declared"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
_in_backup() { [[ -n "$mackup_dir" && -e "$mackup_dir/$1" ]]; }
|
_in_backup() { [[ -n "$mackup_dir" && -e "$mackup_dir/mackup/$1" ]]; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Collect candidate items — same logic as generate_custom_cfg.
|
# Collect candidate items — same logic as generate_custom_cfg.
|
||||||
|
|||||||
+4
-1
@@ -90,7 +90,10 @@ ssr::icloud::prefetch "$mackup_dir"
|
|||||||
# ssr::mackup::restore already calls ssr::icloud::wait internally.
|
# ssr::mackup::restore already calls ssr::icloud::wait internally.
|
||||||
_ssr_mackup_restore_step() {
|
_ssr_mackup_restore_step() {
|
||||||
ssr::mackup::ensure_installed
|
ssr::mackup::ensure_installed
|
||||||
ssr::mackup::configure "$mackup_dir"
|
ssr::mackup::configure "$mackup_dir" || {
|
||||||
|
ssr::warn "Skipping mackup restore — ~/.mackup.cfg was not updated (see messages above)."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
ssr::mackup::restore
|
ssr::mackup::restore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -39,7 +39,8 @@ ssr::icloud::prefetch "$mackup_dir"
|
|||||||
|
|
||||||
ssr::brew::ensure_installed
|
ssr::brew::ensure_installed
|
||||||
ssr::mackup::ensure_installed
|
ssr::mackup::ensure_installed
|
||||||
ssr::mackup::configure "$mackup_dir"
|
_mackup_cfg_ok=true
|
||||||
|
ssr::mackup::configure "$mackup_dir" || _mackup_cfg_ok=false
|
||||||
|
|
||||||
ssr::brew::update
|
ssr::brew::update
|
||||||
ssr::brew::upgrade
|
ssr::brew::upgrade
|
||||||
@@ -48,7 +49,11 @@ ssr::brew::upgrade
|
|||||||
# Timeout (seconds) is configurable; default 180s.
|
# Timeout (seconds) is configurable; default 180s.
|
||||||
ssr::icloud::wait "$mackup_dir" "${SSR_ICLOUD_WAIT_TIMEOUT:-180}"
|
ssr::icloud::wait "$mackup_dir" "${SSR_ICLOUD_WAIT_TIMEOUT:-180}"
|
||||||
|
|
||||||
ssr::mackup::backup
|
if [[ "$_mackup_cfg_ok" == true ]]; then
|
||||||
|
ssr::mackup::backup
|
||||||
|
else
|
||||||
|
ssr::warn "Skipping mackup backup — ~/.mackup.cfg was not updated (see messages above)."
|
||||||
|
fi
|
||||||
ssr::brew::dump "$backup_dir"
|
ssr::brew::dump "$backup_dir"
|
||||||
ssr::brew::cleanup
|
ssr::brew::cleanup
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# Available variables:
|
# Available variables:
|
||||||
# SSR_ROOT — path to the ssr repository
|
# SSR_ROOT — path to the ssr repository
|
||||||
# backup_dir — cloud backup directory for this host
|
# backup_dir — cloud backup directory for this host
|
||||||
# mackup_dir — cloud Mackup directory
|
# mackup_dir — cloud path parent for Mackup (~/.mackup.cfg path=…; files under …/mackup/)
|
||||||
#
|
#
|
||||||
# EXAMPLES
|
# EXAMPLES
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,14 +12,18 @@ SSR_CLOUD_PROVIDER=icloud
|
|||||||
# Only used when SSR_CLOUD_PROVIDER=custom. Absolute path.
|
# Only used when SSR_CLOUD_PROVIDER=custom. Absolute path.
|
||||||
# SSR_CLOUD_PATH=/Volumes/MyNAS/macos-sync
|
# SSR_CLOUD_PATH=/Volumes/MyNAS/macos-sync
|
||||||
|
|
||||||
# Subfolders created inside the cloud root.
|
# Subfolder inside the cloud root for SSR data: per-host backups live under
|
||||||
|
# <cloud>/<SSR_BACKUP_SUBDIR>/<hostname> - <uuid>/. Mackup storage uses the
|
||||||
|
# same parent path; ~/.mackup.cfg sets directory=mackup (see ssr::mackup::configure).
|
||||||
SSR_BACKUP_SUBDIR=BACKUP
|
SSR_BACKUP_SUBDIR=BACKUP
|
||||||
SSR_MACKUP_SUBDIR=Mackup
|
|
||||||
|
|
||||||
# SSR uses `mackup backup` / `mackup restore` (copy, not symlinks) for reliability
|
# SSR uses `mackup backup` / `mackup restore` (copy, not symlinks) for reliability
|
||||||
# on newer macOS. If you still have Mackup symlinks from an older setup, run:
|
# on newer macOS. If you still have Mackup symlinks from an older setup, run:
|
||||||
# ssr mackup unlink
|
# ssr mackup unlink
|
||||||
|
|
||||||
|
# SSR_ALLOW_CONF_OVERWRITE=yes — apply ~/.mackup.cfg updates without a TTY prompt
|
||||||
|
# (launchd / CI). Interactive runs show a diff and ask before replacing.
|
||||||
|
|
||||||
# Subdirs (relative to $HOME) to delete before each mackup backup attempt.
|
# Subdirs (relative to $HOME) to delete before each mackup backup attempt.
|
||||||
# Handles transient dirs with dangling symlinks or sockets (debug sessions,
|
# Handles transient dirs with dangling symlinks or sockets (debug sessions,
|
||||||
# log dirs) that should not be copied to cloud.
|
# log dirs) that should not be copied to cloud.
|
||||||
|
|||||||
+6
-4
@@ -47,12 +47,14 @@ ssr::cloud::backup_dir() {
|
|||||||
printf '%s/%s/%s - %s\n' "$cloud" "$subdir" "$host" "$uuid"
|
printf '%s/%s/%s - %s\n' "$cloud" "$subdir" "$host" "$uuid"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mackup data lives at the cloud root in a fixed folder. Mackup itself
|
# Parent directory for Mackup file_system storage: same cloud subfolder as
|
||||||
# resolves its engine via ~/.mackup.cfg which the install routine seeds.
|
# per-host backups (SSR_BACKUP_SUBDIR). ~/.mackup.cfg sets path=<this> and
|
||||||
|
# directory=mackup — Mackup places files under <this>/mackup/ (not SSR-controlled).
|
||||||
ssr::cloud::mackup_dir() {
|
ssr::cloud::mackup_dir() {
|
||||||
local cloud
|
local cloud subdir
|
||||||
cloud="$(ssr::cloud::resolve_dir)"
|
cloud="$(ssr::cloud::resolve_dir)"
|
||||||
printf '%s/%s\n' "$cloud" "${SSR_MACKUP_SUBDIR:-Mackup}"
|
subdir="${SSR_BACKUP_SUBDIR:-BACKUP}"
|
||||||
|
printf '%s/%s\n' "$cloud" "$subdir"
|
||||||
}
|
}
|
||||||
|
|
||||||
ssr::cloud::ensure_available() {
|
ssr::cloud::ensure_available() {
|
||||||
|
|||||||
+129
@@ -52,6 +52,66 @@ ssr::confirm() {
|
|||||||
case "$reply" in y|Y) return 0 ;; *) return 1 ;; esac
|
case "$reply" in y|Y) return 0 ;; *) return 1 ;; esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Install or replace $1 with the contents of regular file $2 (staging path).
|
||||||
|
# - Missing path or dangling symlink: writes immediately (no overwrite).
|
||||||
|
# - Same bytes as staging: no-op, return 0.
|
||||||
|
# - Differs: prints unified diff (existing → proposed) on stderr, then either
|
||||||
|
# prompts on a TTY or requires SSR_ALLOW_CONF_OVERWRITE=yes (non-interactive).
|
||||||
|
# Returns 0 after a successful write or no-op; 1 if the user declined or
|
||||||
|
# non-interactive without SSR_ALLOW_CONF_OVERWRITE.
|
||||||
|
ssr::replace_file_with_confirm() {
|
||||||
|
local target="$1" staging="$2"
|
||||||
|
ssr::validate_path "$target"
|
||||||
|
ssr::validate_path "$staging"
|
||||||
|
[[ -f "$staging" ]] || ssr::die "Staging file not found: $staging"
|
||||||
|
[[ -d "$target" ]] && ssr::die "Refusing to replace a directory: $target"
|
||||||
|
|
||||||
|
# Nothing there yet (or dangling symlink) — create without prompt.
|
||||||
|
if [[ ! -e "$target" ]]; then
|
||||||
|
rm -f "$target"
|
||||||
|
cat "$staging" > "$target"
|
||||||
|
ssr::ok "Created $target"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ -f "$target" ]]; then
|
||||||
|
ssr::die "Refusing to replace non-regular file: $target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if cmp -s "$target" "$staging" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n' >&2
|
||||||
|
ssr::log "Proposed change to $target (unified diff: existing → proposed)"
|
||||||
|
diff -u "$target" "$staging" >&2 || true
|
||||||
|
printf '\n' >&2
|
||||||
|
|
||||||
|
case "${SSR_ALLOW_CONF_OVERWRITE:-}" in
|
||||||
|
yes|1|true|on)
|
||||||
|
ssr::log "SSR_ALLOW_CONF_OVERWRITE set — applying change to $target"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if ! [[ -t 0 ]]; then
|
||||||
|
ssr::warn "Refusing to overwrite $target (stdin is not a TTY)."
|
||||||
|
ssr::warn "Review the diff above, merge manually, or run once with:"
|
||||||
|
ssr::warn " SSR_ALLOW_CONF_OVERWRITE=yes ssr sync # or ssr restore"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ssr::confirm "Replace $target as shown in the diff above?" 0 || {
|
||||||
|
ssr::warn "Leaving $target unchanged."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cp -f "$target" "$target.bak.$(date +%s)" 2>/dev/null || true
|
||||||
|
rm -f "$target"
|
||||||
|
cat "$staging" > "$target"
|
||||||
|
ssr::ok "Updated $target (previous saved as ${target}.bak.*)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Validate a path is safe (no newlines, no `..` traversal). Used before any FS op.
|
# Validate a path is safe (no newlines, no `..` traversal). Used before any FS op.
|
||||||
# Note: bash strings cannot hold NUL bytes, so no separate NUL check needed.
|
# Note: bash strings cannot hold NUL bytes, so no separate NUL check needed.
|
||||||
ssr::validate_path() {
|
ssr::validate_path() {
|
||||||
@@ -69,12 +129,81 @@ ssr::ensure_dir() {
|
|||||||
[[ -d "$d" ]] || mkdir -p "$d"
|
[[ -d "$d" ]] || mkdir -p "$d"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# If ssr.conf lists SSR_CLOUD_PROVIDER more than once, rewrite
|
||||||
|
# the file to a single assignment: same value repeated is merged silently;
|
||||||
|
# different values prompt on a TTY (stores choice) or abort without TTY.
|
||||||
|
ssr::config::resolve_cloud_provider_duplicates() {
|
||||||
|
local cfg="$1"
|
||||||
|
local -a vals=()
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "$line" =~ ^SSR_CLOUD_PROVIDER=(.*)$ ]] || continue
|
||||||
|
vals+=("${BASH_REMATCH[1]}")
|
||||||
|
done < <(grep '^SSR_CLOUD_PROVIDER=' "$cfg" 2>/dev/null || true)
|
||||||
|
[[ ${#vals[@]} -le 1 ]] && return 0
|
||||||
|
|
||||||
|
local -a uniq=()
|
||||||
|
local v seen u
|
||||||
|
for v in "${vals[@]}"; do
|
||||||
|
seen=0
|
||||||
|
for u in "${uniq[@]:-}"; do [[ "$u" == "$v" ]] && { seen=1; break; }; done
|
||||||
|
[[ "$seen" -eq 0 ]] && uniq+=("$v")
|
||||||
|
done
|
||||||
|
|
||||||
|
local choice=""
|
||||||
|
if [[ ${#uniq[@]} -eq 1 ]]; then
|
||||||
|
choice="${uniq[0]}"
|
||||||
|
ssr::log "Consolidating ${#vals[@]} SSR_CLOUD_PROVIDER line(s) → $choice"
|
||||||
|
elif [[ -t 0 ]]; then
|
||||||
|
printf '\n' >&2
|
||||||
|
ssr::warn "Config $cfg defines multiple different SSR_CLOUD_PROVIDER values."
|
||||||
|
local i=1
|
||||||
|
for v in "${uniq[@]}"; do
|
||||||
|
printf ' %d) %s\n' "$i" "$v" >&2
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
local pick
|
||||||
|
while true; do
|
||||||
|
read -r -p "Select provider to keep [1-${#uniq[@]}]: " pick || pick=""
|
||||||
|
[[ "$pick" =~ ^[0-9]+$ ]] || { ssr::warn "Enter a number between 1 and ${#uniq[@]}."; continue; }
|
||||||
|
(( pick >= 1 && pick <= ${#uniq[@]} )) || { ssr::warn "Out of range."; continue; }
|
||||||
|
choice="${uniq[$((pick - 1))]}"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
ssr::ok "SSR_CLOUD_PROVIDER=$choice will be written to $cfg"
|
||||||
|
else
|
||||||
|
ssr::die "Config $cfg has conflicting SSR_CLOUD_PROVIDER lines (${uniq[*]}). Keep one value or run ssr from a terminal to choose."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp newtmp bak
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/ssr-cfg.XXXXXX")"
|
||||||
|
newtmp="$(mktemp "${TMPDIR:-/tmp}/ssr-cfg.XXXXXX")"
|
||||||
|
bak="${cfg}.bak.$(date +%s)"
|
||||||
|
cp -p "$cfg" "$bak" 2>/dev/null || cp "$cfg" "$bak"
|
||||||
|
grep -v '^SSR_CLOUD_PROVIDER=' "$cfg" > "$tmp"
|
||||||
|
local ins=0
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$ins" -eq 0 && "$line" == '# Provider:'* ]]; then
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
printf 'SSR_CLOUD_PROVIDER=%s\n' "$choice"
|
||||||
|
ins=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
done < "$tmp" > "$newtmp"
|
||||||
|
[[ "$ins" -eq 1 ]] || printf 'SSR_CLOUD_PROVIDER=%s\n' "$choice" >> "$newtmp"
|
||||||
|
mv "$newtmp" "$cfg"
|
||||||
|
rm -f "$tmp"
|
||||||
|
ssr::log "Config updated (previous copy: $bak)"
|
||||||
|
}
|
||||||
|
|
||||||
# Source config file if it exists. Validates that only known KEY=VALUE lines
|
# Source config file if it exists. Validates that only known KEY=VALUE lines
|
||||||
# are present to avoid arbitrary code execution from a tampered config.
|
# are present to avoid arbitrary code execution from a tampered config.
|
||||||
ssr::load_config() {
|
ssr::load_config() {
|
||||||
local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}"
|
local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}"
|
||||||
export SSR_CONFIG="$cfg"
|
export SSR_CONFIG="$cfg"
|
||||||
[[ -f "$cfg" ]] || return 0
|
[[ -f "$cfg" ]] || return 0
|
||||||
|
ssr::config::resolve_cloud_provider_duplicates "$cfg"
|
||||||
if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then
|
if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then
|
||||||
ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)"
|
ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+11
-12
@@ -238,7 +238,8 @@ ssr::mackup::generate_custom_cfg() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
# Return true when <path> is already present in the Mackup cloud backup.
|
# Return true when <path> is already present in the Mackup cloud backup.
|
||||||
_in_backup() { [[ -e "$mackup_dir/$1" ]]; }
|
# SSR seeds ~/.mackup.cfg with directory=mackup — data lives under that subdir.
|
||||||
|
_in_backup() { [[ -e "$mackup_dir/mackup/$1" ]]; }
|
||||||
|
|
||||||
local -a items=()
|
local -a items=()
|
||||||
|
|
||||||
@@ -288,23 +289,21 @@ ssr::mackup::ensure_installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Writes ~/.mackup.cfg pointing the engine at the cloud directory.
|
# Writes ~/.mackup.cfg pointing the engine at the cloud directory.
|
||||||
# Idempotent: rewrites only if content differs.
|
# Never overwrites an existing different file without confirmation: shows a
|
||||||
|
# unified diff, then prompts (TTY) or requires SSR_ALLOW_CONF_OVERWRITE=yes.
|
||||||
ssr::mackup::configure() {
|
ssr::mackup::configure() {
|
||||||
local mackup_dir="$1"
|
local mackup_dir="$1"
|
||||||
local cfg="$HOME/.mackup.cfg"
|
local cfg="$HOME/.mackup.cfg"
|
||||||
local desired
|
local desired tmp
|
||||||
desired="$(printf '[storage]\nengine = file_system\npath = %s\ndirectory = mackup\n' "$mackup_dir")"
|
desired="$(printf '[storage]\nengine = file_system\npath = %s\ndirectory = mackup\n' "$mackup_dir")"
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/ssr-mackup-cfg.XXXXXX")"
|
||||||
if [[ -f "$cfg" ]] && [[ "$(cat "$cfg")" == "$desired" ]]; then
|
printf '%s\n' "$desired" > "$tmp"
|
||||||
|
if ssr::replace_file_with_confirm "$cfg" "$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
if [[ -L "$cfg" || -f "$cfg" ]]; then
|
return 1
|
||||||
cp -f "$cfg" "$cfg.bak.$(date +%s)" 2>/dev/null || true
|
|
||||||
rm -f "$cfg"
|
|
||||||
fi
|
|
||||||
printf '%s\n' "$desired" > "$cfg"
|
|
||||||
ssr::ok "Wrote $cfg"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Force iCloud Drive to materialize placeholder (.icloud) files under the
|
# Force iCloud Drive to materialize placeholder (.icloud) files under the
|
||||||
|
|||||||
Reference in New Issue
Block a user