summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbackomp109
1 files changed, 77 insertions, 32 deletions
diff --git a/backomp b/backomp
index f653621..2fdd219 100755
--- a/backomp
+++ b/backomp
@@ -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))