summaryrefslogtreecommitdiff
path: root/backomp
diff options
context:
space:
mode:
Diffstat (limited to 'backomp')
-rwxr-xr-xbackomp174
1 files changed, 142 insertions, 32 deletions
diff --git a/backomp b/backomp
index bf91371..e45eff8 100755
--- a/backomp
+++ b/backomp
@@ -92,7 +92,7 @@ END {
EOF
err() {
- echo "${0##*/}: error: $*" >&2
+ echo "${0##*/}: ${err_ctx}: error: $*" >&2
}
run() {
@@ -148,15 +148,14 @@ filter() {
mapfile -td $'\0' "${__dest_name}" < <(
printf '%s\0' "${__src[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \
"${@}" -- "${__script}")
- wait "$!"
- if [[ "$?" -ne 0 ]]; then
+ if ! wait "$!"; then
err "${__label} failed"
return 1
fi
- if [[ ${#__src[@]} -gt 0 ]]; then
+ if ((${#__src[@]})); then
exists "${__dest[0]}" && return 0
- [[ "$?" -eq 1 ]] && \
+ [[ "$?" -eq 1 ]] &&
err "${__label} returned empty or invalid list"
return 1
fi
@@ -186,13 +185,14 @@ populate() {
copy() {
local src="${1}" dest="${2}" link_dest="${3}"
- local path res r_dest r_link_dest
+ local rsync_flags="${4}" rclone_flags="${5}"
+ local path res flags r_dest r_link_dest
for path in "${dest}" "${dest}.tmp"; do
exists "${path}"
res="$?"
if [[ "${res}" -ne 1 ]]; then
- [[ "${res}" -eq 0 ]] && \
+ [[ "${res}" -eq 0 ]] &&
err "destination already exists: ${path}"
return 1
fi
@@ -206,14 +206,26 @@ copy() {
r_dest+="/${src##*/}"
r_link_dest+="${r_link_dest:+/${src##*/}}"
fi
- run rclone copy --sftp-copy-is-hardlink \
+ mapfile -td $'\0' flags < <(
+ printf '%s' "${rclone_flags}" | xargs -r printf '%s\0')
+ if ! wait "$!"; then
+ err 'parsing rclone flags failed'
+ return 1
+ fi
+ run rclone copy "${flags[@]}" \
${r_link_dest:+"--copy-dest=${r_link_dest#rclone:}"} \
-- "${src}" "${r_dest#rclone:}" || return 1
run rclone move -- "${dest#rclone:}.tmp" "${dest#rclone:}"
return
fi
- run rsync -ac --mkpath \
+ mapfile -td $'\0' flags < <(
+ printf '%s' "${rsync_flags}" | xargs -r printf '%s\0')
+ if ! wait "$!"; then
+ err 'parsing rsync flags failed'
+ return 1
+ fi
+ run rsync "${flags[@]}" --mkpath \
${link_dest:+"--link-dest=${link_dest}"} \
-- "${src}" "${dest}.tmp/" || return 1
run mv -T -- "${dest}.tmp" "${dest}" || return 1
@@ -239,6 +251,7 @@ prune() {
backup() {
local src_path="${1}" dest_dir="${2}"
local retain="${3}" interval="${4}" week_start="${5}"
+ local rsync_flags="${6}" rclone_flags="${7}"
local dest_path snaps keep
if [[ ! -e "${src_path}" ]]; then
@@ -253,7 +266,8 @@ backup() {
&& return
dest_path="${dest_dir}/$(date -u '+%Y-%m-%d-%H%M%S')"
- copy "${src_path}" "${dest_path}" "${snaps[0]}" || return 1
+ copy "${src_path}" "${dest_path}" "${snaps[0]}" \
+ "${rsync_flags}" "${rclone_flags}" || return 1
if [[ ! "${retain}" =~ ^( *([0-9]+|\*)\^?[hdwM] *)+$ ]]; then
err "invalid \$retain setting: ${retain}"
@@ -273,39 +287,135 @@ declare -A def_opts=(
['retain']='*h' # e.g. 24h7d4w12M or 24^h10d*M
['interval']='0'
['week_start']='mon'
+ ['rsync_flags']='-ac'
+ ['rclone_flags']='--checksum --sftp-copy-is-hardlink'
)
-declare -A opts
+declare -A opts tainted_opts
+reset_opts() {
+ opts=()
+ tainted_opts=()
+ for k in "${!def_opts[@]}"; do
+ opts["${k}"]="${def_opts["${k}"]}"
+ done
+}
+reset_opts
+
+expand_opts() {
+ local str="${1}" default="${2}"
+ local res opt tainted=0
+
+ while [[ "${str}" =~ \\([\\$=!])|\$([A-Za-z0-9_]+)|\$\{([^}]*)\} ]]; do
+ res+="${str%%"${BASH_REMATCH[0]}"*}"
+ opt="${BASH_REMATCH[2]:-${BASH_REMATCH[3]}}"
+ if [[ -n "${BASH_REMATCH[1]}" ]]; then
+ res+="${BASH_REMATCH[1]}"
+ elif [[ "${opt}" == 'default' ]]; then
+ res+="${default}"
+ elif [[ -n "${opt}" ]]; then
+ res+="${opts["${opt}"]}"
+ if [[ ! -v opts["${opt}"] ]]; then
+ err "reference to undefined opt: ${opt}"
+ tainted=1
+ elif [[ -v tainted_opts["${opt}"] ]]; then
+ err "reference to tainted opt: ${opt}"
+ tainted=1
+ fi
+ fi
+ str="${str#*"${BASH_REMATCH[0]}"}"
+ done
+ printf '%s\n' "${res}${str}"
+ return "${tainted}"
+}
declare -A seen
-while IFS='=' read -r f1 f2; do
- [[ -z "${f1}" ]] || [[ "${f1}" == '#'* ]] && continue
+while IFS= read -r line; do
+ ((++lnum))
+ [[ -z "${line}" || "${line}" == \#* ]] && continue
- if [[ "${f1}" =~ ^\[(.*)]$ ]]; then
+ if [[ "${line}" =~ ^\[(.*)]$ ]]; then
base_path="${BASH_REMATCH[1]}"
- declare -A opts="$(declare -p def_opts | sed '1s/[^=]*=//')"
- elif [[ "${f1}" == '$'* ]]; then
- opt="${f1:1}"
- [[ "${f2}" == '-' ]] && opts["${opt}"]= ||
- opts["${opt}"]="${f2:-"${def_opts["${opt}"]}"}"
- else
- dest="${base_path}/${f1}"
- printf '[%d] [%s] %s => %s%s\n' "$((++dcount))" \
- "${opts['retain']}" "${f2}" "${dest}"
+ reset_opts
+ continue
+ fi
+
+ err_ctx="line ${lnum}"
+ if [[ ! "${line}" =~ ^((\\[\\=]|[^=])+)=(.*)$ ]]; then
+ err "expected '='"
+ ((++errors))
+ continue
+ fi
+ f1="${BASH_REMATCH[1]}"
+ f2="${BASH_REMATCH[3]}"
+ [[ "${f1}" == \$?* ]] && opt="${f1:1}" || opt=
+ err_ctx="${opt:+opt }${opt:-task $((++bcount))} (${err_ctx})"
- dest_rp="$(realpath -m -- "${dest}")"
- if [[ -v "seen["${dest_rp}"]" ]]; then
- err "destination already seen: ${dest}"
+ tainted_src=0
+ tainted_dest=0
+ if [[ "${f2}" == \!?* ]]; then
+ cmd="${f2:1}"
+ f2="$(default="${opt:+${def_opts["${opt}"]}}" eval "${cmd}" \
+ 2> >(while read -r x || [[ -n "${x}" ]]; do
+ [[ "${x}" =~ ^"${0}: line "[^:]+": "(.*) ]] &&
+ x="bash: ${BASH_REMATCH[1]}"
+ echo "${0##*/}: ${err_ctx}: stderr: ${x}"
+ done >&2))"
+ ret="$?"
+ [[ "${ret}" -ne 0 ]] && err "command failed (${ret}): ${cmd}"
+ else
+ f2="$(expand_opts "${f2}" "${opt:+${def_opts["${opt}"]}}")"
+ ret="$?"
+ fi
+ if [[ "${ret}" -ne 0 ]]; then
+ if [[ -n "${opt}" ]]; then
+ tainted_opts["${opt}"]=1
((++errors))
continue
fi
- seen["${dest_rp}"]=
+ tainted_src=1
+ elif [[ -n "${opt}" ]]; then
+ opts["${opt}"]="${f2}"
+ unset tainted_opts["${opt}"]
+ continue
+ fi
- backup "${f2}" "${dest}" \
- "${opts['retain']}" \
- "${opts['interval']}" \
- "${opts['week_start']}" \
- || ((++errors))
+ src="${f2}"
+ dest="${base_path}${base_path:+/}$(expand_opts "${f1}")"
+ tainted_dest="$?"
+ printf '[%d] [%s] %s => %s\n' "${bcount}" "${opts['retain']}" \
+ "${src}" "${dest}"
+
+ skips=()
+ ((tainted_src)) && skips+=('tainted source')
+ ((tainted_dest)) && skips+=('tainted destination')
+ for k in "${!def_opts[@]}"; do
+ [[ -v tainted_opts["${k}"] ]] && skips+=("tainted opt: ${k}")
+ done
+ [[ -z "${src}" ]] && skips+=('source blank')
+ if ((!tainted_dest)); then
+ if [[ -z "${dest}" ]]; then
+ skips+=('destination blank')
+ else
+ dest_rp="$(realpath -m -- "${dest}")"
+ [[ -v seen["${dest_rp}"] ]] &&
+ skips+=("destination already seen: ${dest}")
+ seen["${dest_rp}"]=
+ fi
+ fi
+ if ((${#skips[@]})); then
+ for x in "${skips[@]}"; do
+ err "${x}"
+ done
+ ((++errors))
+ continue
fi
+
+ backup "${src}" "${dest}" \
+ "${opts['retain']}" \
+ "${opts['interval']}" \
+ "${opts['week_start']}" \
+ "${opts['rsync_flags']}" \
+ "${opts['rclone_flags']}" \
+ || ((++errors))
done < "${1}"
((!errors))