diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-13 21:54:49 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-13 21:54:49 -0800 |
| commit | 26d67debb8eeb9f8ef06ce8f28ea48e9f5703d77 (patch) | |
| tree | fc78ea149d789a75d605937a37e6989e04c88293 /backomp | |
| parent | f6ed8e53eed9524ae9407974f0bc8274864d9135 (diff) | |
| download | backomp-master.tar.gz backomp-master.tar.xz | |
Diffstat (limited to 'backomp')
| -rwxr-xr-x | backomp | 109 |
1 files changed, 77 insertions, 32 deletions
@@ -1,8 +1,6 @@ #!/usr/bin/env bash -set -e - -! read -rd '' AWK_PREFILTER <<'EOF' +read -rd '' AWK_PREFILTER <<'EOF' /(\/|^)[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}$/ { a[$0] } @@ -14,7 +12,7 @@ END { } EOF -! read -rd '' AWK_FILTER <<'EOF' +read -rd '' AWK_FILTER <<'EOF' function flr(n, s) { return int(n / s) * s } @@ -93,9 +91,16 @@ END { } EOF +err() { + echo "${0##*/}: error: $*" >&2 +} + run() { - echo "* ${@}" - "${@}" + echo "* $*" + if ! "$@"; then + err "command failed: $*" + return 1 + fi } current() { @@ -119,43 +124,67 @@ current() { (($(date -u +%s) / s == $(date -ud "${d}" +%s) / s)) } +filter() { + local -n _f_src="${1}" _f_dest="${2}" + local _f_dest_name="${2}" _f_label="${3}" _f_script="${4}" + shift 4 + + mapfile -td $'\0' "${_f_dest_name}" < <( + printf '%s\0' "${_f_src[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ + "${@}" -- "${_f_script}") + wait "$!" + if [[ "$?" -ne 0 ]]; then + err "${_f_label} failed" + return 1 + fi + if [[ ${#_f_src[@]} -gt 0 && ! -d "${_f_dest[0]}" ]]; then + err "${_f_label} returned empty or invalid list" + return 1 + fi +} + backup() { - local dest_dir="${1}" src_path="${2}" + local src_path="${1}" dest_dir="${2}" local retain="${3}" interval="${4}" week_start="${5}" local dest_path snaps keep skip i ki - mapfile -td $'\0' snaps < <(shopt -s nullglob; - printf '%s\0' "${dest_dir}/"* | awk -v 'RS=\0' -v 'ORS=\0' \ - -- "${AWK_PREFILTER}") - [[ -n "${snaps}" ]] && current "${snaps[0]##*/}" "${interval}" && \ - skip=1 - printf '[%d] [%s] %s => %s%s\n' "$((++dcount))" "${retain}" \ - "${src_path}" "${dest_dir}" "${skip:+ [SKIPPED]}" - [[ -n "${skip}" ]] && return + if [[ ! -e "${src_path}" ]]; then + err "source does not exist: ${src_path}" + return 1 + fi + + mapfile -td $'\0' snaps < \ + <(shopt -s nullglob; printf '%s\0' "${dest_dir}/"*) + filter snaps snaps 'pre-snapshot awk prefilter' "${AWK_PREFILTER}" \ + || return 1 + [[ -n "${snaps}" ]] && current "${snaps[0]##*/}" "${interval}" \ + && return dest_path="${dest_dir}/$(date -u '+%Y-%m-%d-%H%M%S')" + if [[ -e "${dest_path}" || -e "${dest_path}.tmp" ]]; then + err "destination already exists: ${dest_path}" + return 1 + fi run rsync -ac --mkpath ${snaps[0]:+"--link-dest=${snaps[0]}"} \ - "${src_path}" "${dest_path}" - snaps+=("${dest_path}") - mapfile -td $'\0' snaps < <( - printf '%s\0' "${snaps[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ - -- "${AWK_PREFILTER}") - - mapfile -td $'\0' keep < <( - printf '%s\0' "${snaps[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ - -v "opt_retain=${retain}" \ - -v "opt_week_start=${week_start}" \ - -- "${AWK_FILTER}") - if [[ -z "${keep}" ]]; then - echo "WARNING: dirs to keep shouldn't be empty; skipping" >&2 - return + "${src_path}" "${dest_path}.tmp/" || return 1 + run mv -T "${dest_path}.tmp" "${dest_path}" || return 1 + + if [[ ! "${retain}" =~ ^( *([0-9]+|\*)\^?[hdwM] *)+$ ]]; then + err "invalid \$retain setting: ${retain}" + return 1 fi + snaps+=("${dest_path}") + filter snaps snaps 'post-snapshot awk prefilter' "${AWK_PREFILTER}" \ + || return 1 + filter snaps keep 'awk filter' "${AWK_FILTER}" \ + -v "opt_retain=${retain}" -v "opt_week_start=${week_start}" \ + || return 1 for ((i = ${#snaps[@]} - 1, ki = 0; i >= 0; --i)); do if [[ "${snaps[i]}" == "${keep[ki]}" ]]; then ((++ki)) else - run rm -r "${snaps[i]}" + run rm -rf "${snaps[i]}" fi done } @@ -167,6 +196,7 @@ declare -A def_opts=( ) declare -A opts +declare -A seen while IFS='=' read -r f1 f2; do [[ -z "${f1}" ]] || [[ "${f1}" == '#'* ]] && continue @@ -178,9 +208,24 @@ while IFS='=' read -r f1 f2; do [[ "${f2}" == '-' ]] && opts["${opt}"]= || opts["${opt}"]="${f2:-"${def_opts["${opt}"]}"}" else - backup "${base_path}/${f1}" "${f2}" \ + dest="${base_path}/${f1}" + printf '[%d] [%s] %s => %s%s\n' "$((++dcount))" \ + "${opts['retain']}" "${f2}" "${dest}" + + dest_rp="$(realpath -m "${dest}")" + if [[ -v "seen["${dest_rp}"]" ]]; then + err "destination already seen: ${dest}" + ((++errors)) + continue + fi + seen["${dest_rp}"]= + + backup "${f2}" "${dest}" \ "${opts['retain']}" \ "${opts['interval']}" \ - "${opts['week_start']}" + "${opts['week_start']}" \ + || ((++errors)) fi done < "${1}" + +((!errors)) |
