diff options
Diffstat (limited to 'templates/usr_local_bin_backup.sh.j2')
| -rw-r--r-- | templates/usr_local_bin_backup.sh.j2 | 73 |
1 files changed, 68 insertions, 5 deletions
diff --git a/templates/usr_local_bin_backup.sh.j2 b/templates/usr_local_bin_backup.sh.j2 index a79cdb7..f6a0e2c 100644 --- a/templates/usr_local_bin_backup.sh.j2 +++ b/templates/usr_local_bin_backup.sh.j2 @@ -7,6 +7,7 @@ KEEP_REMOTE=10 KEYFILE="/root/.ssh/backup" HOST_DIR="backup" DATASET="{% endraw %}{{ backup_zfs_dataset }}{% raw %}" +BACKUP_EXCLUDE_PROP="{% endraw %}{{ backup_zfs_exclude_property | default('com.pursotin:backup') }}{% raw %}" USER="{% endraw %}{{ backup_ssh_user }}{% raw %}" HOST="{% endraw %}{{ backup_ssh_host }}{% raw %}" EMAIL_TO=root @@ -39,6 +40,29 @@ timestamp() { date +%Y-%m-%d-%H-%M-%S%z | tr '+' '-' } +humanize_bytes() { + local bytes="$1" + if [[ "${bytes}" =~ ^[0-9]+$ ]]; then + awk -v b="${bytes}" ' + BEGIN { + unit_count = split("B KiB MiB GiB TiB PiB EiB", units, " ") + i = 1 + while (b >= 1024 && i < unit_count) { + b = b / 1024 + i++ + } + if (i == 1) { + printf "%.0f %s\n", b, units[i] + } else { + printf "%.2f %s\n", b, units[i] + } + } + ' + else + echo "unknown" + fi +} + latest_snapshot_for_dataset() { # Latest snapshot on the TOP dataset only (newest first). Returns e.g. zroot@2025-09-30-... zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ @@ -51,6 +75,12 @@ list_top_snapshots_newest_first() { | awk 'NR>1{print $1}' } +list_excluded_datasets() { + # List datasets (including inherited values) where backup property is explicitly false. + zfs get -r -H -o name,value "${BACKUP_EXCLUDE_PROP}" "${DATASET}" 2>/dev/null \ + | awk 'tolower($2)=="false"{print $1}' +} + list_remote_backups_sorted() { # We store files under ${HOST_DIR}/<dataset>@<timestamp>.enc echo "ls ${HOST_DIR}" \ @@ -116,9 +146,10 @@ cmd_snapshot() { cmd_send_to_remote() { echo "" echo "[send-to-remote]" - require_cmds zfs age sftp mktemp stat + require_cmds zfs age sftp mktemp stat awk sort - local snap base target tmp size_bytes + local snap base target tmp size_bytes ds parent skip + local -a send_args excluded_raw excluded snap="$(latest_snapshot_for_dataset)" [[ -n "${snap}" ]] || die "No snapshot found to send. Run 'snapshot' first or ensure the dataset has snapshots." @@ -127,9 +158,36 @@ cmd_send_to_remote() { tmp="$(mktemp -t backup_send.XXXXXX)" trap 'rm -f "${tmp}"' EXIT + send_args=(-Rvc) + mapfile -t excluded_raw < <(list_excluded_datasets | sort) + excluded=() + for ds in "${excluded_raw[@]}"; do + skip=0 + [[ -n "${ds}" ]] || continue + for parent in "${excluded[@]}"; do + if [[ "${ds}" == "${parent}" || "${ds}" == "${parent}/"* ]]; then + skip=1 + break + fi + done + [[ "${skip}" -eq 1 ]] || excluded+=("${ds}") + done + + if [[ "${#excluded[@]}" -gt 0 ]]; then + echo "Excluding datasets where ${BACKUP_EXCLUDE_PROP}=false:" + for ds in "${excluded[@]}"; do + if [[ "${ds}" == "${DATASET}" ]]; then + die "Top dataset ${DATASET} is excluded via ${BACKUP_EXCLUDE_PROP}=false; refusing to create an empty backup stream." + fi + echo " - ${ds}" + send_args+=(-X "${ds}") + done + log_note "send-to-remote: excluded ${#excluded[@]} dataset(s) using ${BACKUP_EXCLUDE_PROP}=false" + fi + echo "Creating encrypted replication stream to temp file: ${tmp}" # -R: recursive hierarchy, -v: progress to stderr, -c: send compressed (keeps on-disk compression) - (set -x; zfs send -Rvc "${snap}" | age -e -i "${KEYFILE}" > "${tmp}") + (set -x; zfs send "${send_args[@]}" "${snap}" | age -e -i "${KEYFILE}" > "${tmp}") size_bytes="$(stat -f %z "${tmp}" 2>/dev/null || stat -c %s "${tmp}" 2>/dev/null || echo "unknown")" echo "Local stream size: ${size_bytes} bytes" @@ -215,7 +273,8 @@ cmd_prune_local() { cmd_notify() { echo "" echo "[notify]" - require_cmds mail date + require_cmds mail date awk + local backup_size_human FINISHED_AT="$(date '+%Y-%m-%dT%H:%M:%S%z')" # Derive snapshot name if not set yet (best-effort) @@ -229,6 +288,8 @@ cmd_notify() { fi fi + backup_size_human="$(humanize_bytes "${BACKUP_SIZE_BYTES:-unknown}")" + # Compose subject and body local subject="Backup completed: ${BACKUP_NAME}" local body="" @@ -238,7 +299,7 @@ cmd_notify() { body+="Dataset: ${DATASET}"$'\n' body+="Remote: ${USER}@${HOST}:${HOST_DIR}"$'\n' body+="Name: ${BACKUP_NAME}"$'\n' - body+="Size: ${BACKUP_SIZE_BYTES:-unknown} bytes"$'\n' + body+="Size: ${backup_size_human} (${BACKUP_SIZE_BYTES:-unknown} bytes)"$'\n' body+=$'\n' body+="Steps:"$'\n' body+="${MESSAGE_LOG:-<no steps recorded>}"$'\n' @@ -263,6 +324,8 @@ Subcommands (executed in order): Notes: - Run 'init-remote' once before the first upload, or anytime after changing \$HOST_DIR. - Local pruning destroys older snapshots **recursively** to maintain consistency across descendants. + - Set \${BACKUP_EXCLUDE_PROP}=false on a dataset to exclude it (and inherited descendants) from send-to-remote. + Example: zfs set com.pursotin:backup=false zroot/some/heavy-dataset Examples: $(basename "$0") init-remote snapshot send-to-remote prune-remote prune-local notify |
