diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-07-28 23:25:51 -0700 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-07-28 23:25:51 -0700 |
| commit | 043f719486b940d403bcdb72defb5ed401cb5252 (patch) | |
| tree | 2121e2ec8445e058efaac9146f7fbbeedd1ae763 /backomp | |
| parent | 82ea4a052c2bb0b4c1a76789b24b0f8ccbaaee16 (diff) | |
| download | backomp-043f719486b940d403bcdb72defb5ed401cb5252.tar.gz backomp-043f719486b940d403bcdb72defb5ed401cb5252.tar.xz | |
add pre-copy hook for rclone hardlink over ssh
Diffstat (limited to 'backomp')
| -rwxr-xr-x | backomp | 117 |
1 files changed, 77 insertions, 40 deletions
@@ -1,4 +1,6 @@ #!/usr/bin/env bash +# +# Copyright 2023 David Vazgenovich Shakaryan read -rd '' AWK_PREFILTER <<'EOF' /(\/|^)[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}$/ { @@ -98,22 +100,36 @@ err() { parse_flags() { local -n __arr="${1}" local __str="${2}" __label="${3}" + local res - mapfile -td $'\0' __arr < <( + mapfile -td '' __arr < <( printf '%s' "${__str}" | xargs -r printf '%s\0') - if ! wait "$!"; then - err "parsing ${__label} failed" + wait "$!" + res="$?" + if [[ "${res}" -ne 0 ]]; then + err "parsing ${__label} failed (${res})" return 1 fi } +qargs() { + local arg str= + + for arg; do + [[ "${arg:-*}" =~ [^A-Za-z0-9%+./:=@_-] ]] && arg="${arg@Q}" + str+=" ${arg}" + done + printf '%s\n' "${str:1}" +} + run() { - local res + local str res - echo "* $*" + str="$(qargs "$@")" + echo "* ${str}" "$@" res="$?" - [[ "${res}" -ne 0 ]] && err "command failed: $*" + [[ "${res}" -ne 0 ]] && err "command failed (${res}): ${str}" return "${res}" } @@ -128,7 +144,8 @@ ssh_run() { else "${cmd[@]}" res="$?" - [[ "${res}" -eq 255 ]] && err "command failed: ${cmd[*]}" + [[ "${res}" -eq 255 ]] && + err "command failed (${res}): $(qargs "${cmd[@]}")" return "${res}" fi } @@ -146,7 +163,7 @@ current() { *) ((s += val)) ;; esac - interval="${interval#*${BASH_REMATCH[0]}}" + interval="${interval#*"${BASH_REMATCH[0]}"}" done ((s < 1)) && return 1 @@ -164,7 +181,7 @@ exists() { res="$?" [[ "${res}" -eq 0 ]] && return 0 [[ "${res}" -eq 3 ]] && return 1 - err "command failed: ${cmd[*]}" + err "command failed (${res}): $(qargs "${cmd[@]}")" return 2 fi @@ -181,12 +198,15 @@ filter() { local -n __src="${1}" __dest="${2}" local __label="${3}" __script="${4}" shift 4 + local res - mapfile -td $'\0' __dest < <( - printf '%s\0' "${__src[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ - "${@}" -- "${__script}") - if ! wait "$!"; then - err "${__label} failed" + mapfile -td '' __dest < <( + printf '%s\0' "${__src[@]}" | \ + awk -v 'RS=\0' -v 'ORS=\0' "$@" -- "${__script}") + wait "$!" + res="$?" + if [[ "${res}" -ne 0 ]]; then + err "${__label} failed (${res})" return 1 fi @@ -200,15 +220,16 @@ filter() { populate() { local -n __arr="${1}" - local __dir="${2}" - local cmd + local __dir="${2}" + local cmd res if [[ "${__dir}" == rclone://* ]]; then cmd=(rclone lsf --dir-slash=false -- "${__dir#*//}") mapfile -t __arr < <("${cmd[@]}" 2>/dev/null) wait "$!" - if [[ "$?" -ne 0 && "$?" -ne 3 ]]; then - err "command failed: ${cmd[*]}" + res="$?" + if [[ "${res}" -ne 0 && "${res}" -ne 3 ]]; then + err "command failed (${res}): $(qargs "${cmd[@]}")" return 1 fi @@ -217,9 +238,9 @@ populate() { fi if [[ "${__dir}" =~ ^([^:/]*):(.*) ]]; then - mapfile -td $'\0' __arr < \ - <(ssh_run "${BASH_REMATCH[1]}" \ - "shopt -s nullglob; printf '%s\0' $(\ + mapfile -td '' __arr < <( + ssh_run "${BASH_REMATCH[1]}" \ + "shopt -s nullglob; printf '%s\0' $( \ printf '%q' "${BASH_REMATCH[2]}")/*") wait "$!" || return 1 @@ -227,8 +248,8 @@ populate() { return fi - mapfile -td $'\0' __arr < \ - <(shopt -s nullglob; printf '%s\0' "${__dir}/"*) + mapfile -td '' __arr < <( + shopt -s nullglob; printf '%s\0' "${__dir}/"*) } copy() { @@ -245,15 +266,28 @@ copy() { fi done + if [[ -n "${opts['pre_copy']}" ]]; then + bash -c "$(declare -p src dest link_dest opts \ + ); temp_dest=$(printf '%q' "${dest}.tmp" \ + ); ${opts['pre_copy']}" < /dev/null + res="$?" + if [[ "${res}" -ne 0 ]]; then + err "pre-copy command failed (${res}):" \ + "${opts['pre_copy']}" + return 1 + fi + fi + if [[ "${dest}" == rclone://* ]]; then r_dest="${dest}.tmp" + ((${opts['rclone_use_copy_dest']:-1})) || link_dest= if [[ -d "${src}" ]]; then # emulates rsync src trailing slash behaviour r_dest+="/${src##*/}" link_dest+="${link_dest:+/${src##*/}}" fi parse_flags flags "${rclone_flags}" 'rclone_flags' || return 1 - run rclone copy "${flags[@]}" \ + run rclone "${opts['rclone_copy_command']}" "${flags[@]}" \ ${link_dest:+"--copy-dest=${link_dest#*//}"} \ -- "${src}" "${r_dest#*//}" || return 1 run rclone move -- "${dest#*//}.tmp" "${dest#*//}" @@ -267,8 +301,8 @@ copy() { if [[ "${dest}" =~ ^([^:/]*):(.*) ]]; then USE_RUN=1 ssh_run "${BASH_REMATCH[1]}" \ - "mv -T -- $(\ - printf '%q' "${BASH_REMATCH[2]}.tmp") $(\ + "mv -T -- $( \ + printf '%q' "${BASH_REMATCH[2]}.tmp") $( \ printf '%q' "${BASH_REMATCH[2]}")" return fi @@ -338,8 +372,10 @@ 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' + ['rsync_flags']='-a' + ['rclone_flags']='--sftp-copy-is-hardlink' + ['rclone_copy_command']='copy' + ['rclone_use_copy_dest']=1 ) declare -A opts tainted_opts reset_opts() { @@ -355,7 +391,8 @@ expand_opts() { local str="${1}" default="${2}" local res opt tainted=0 - while [[ "${str}" =~ \\([\\$=!])|\$([A-Za-z0-9_]+)|\$\{([^}]*)\} ]]; do + 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 @@ -400,23 +437,23 @@ while IFS= read -r line; do [[ "${f1}" == \$?* ]] && opt="${f1:1}" || opt= err_ctx="${opt:+opt }${opt:-task $((++bcount))} (${err_ctx})" + res=0 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}" + default="${opt:+${def_opts["${opt}"]}}" + f2="$(bash -c "$(declare -p default opts); ${cmd}" \ + < /dev/null)" + res="$?" + [[ "${res}" -ne 0 ]] && err "command failed (${res}): ${cmd}" + elif [[ "${f2}" == %?* ]]; then + f2="${f2:1}" else f2="$(expand_opts "${f2}" "${opt:+${def_opts["${opt}"]}}")" - ret="$?" + res="$?" fi - if [[ "${ret}" -ne 0 ]]; then + if [[ "${res}" -ne 0 ]]; then if [[ -n "${opt}" ]]; then tainted_opts["${opt}"]=1 ((++errors)) @@ -425,7 +462,7 @@ while IFS= read -r line; do tainted_src=1 elif [[ -n "${opt}" ]]; then opts["${opt}"]="${f2}" - unset tainted_opts["${opt}"] + unset 'tainted_opts[${opt}]' continue fi |
