diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-07-22 11:39:07 -0700 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-07-22 11:42:28 -0700 |
| commit | f0fc318169577f5d84d0de4e26cc11a3713f1784 (patch) | |
| tree | bed7fe74401d3b4a89cc9d2f3471ac8421ecc571 | |
| parent | ce9441e87dab47bd0741b2be59d335643dc8a31b (diff) | |
| download | backomp-f0fc318169577f5d84d0de4e26cc11a3713f1784.tar.gz backomp-f0fc318169577f5d84d0de4e26cc11a3713f1784.tar.xz | |
add custom flags, opt expansion, command execution
| -rwxr-xr-x | backomp | 174 |
1 files changed, 142 insertions, 32 deletions
@@ -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)) |
