diff options
| -rwxr-xr-x | backomp | 134 |
1 files changed, 107 insertions, 27 deletions
@@ -124,69 +124,149 @@ current() { (($(date -u +%s) / s == $(date -ud "${d}" +%s) / s)) } +exists() { + local path="${1}" + local res + + if [[ "${path}" == rclone:* ]]; then + rclone lsf -- "${path#rclone:}" &>/dev/null + res="$?" + [[ "${res}" -eq 0 ]] && return 0 + [[ "${res}" -eq 3 ]] && return 1 + err "rclone lsf failed: ${path#rclone:}" + return 2 + fi + + [[ -e "${path}" ]] +} + filter() { - local -n _f_src="${1}" _f_dest="${2}" - local _f_dest_name="${2}" _f_label="${3}" _f_script="${4}" + local -n __src="${1}" __dest="${2}" + local __dest_name="${2}" __label="${3}" __script="${4}" shift 4 - mapfile -td $'\0' "${_f_dest_name}" < <( - printf '%s\0' "${_f_src[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ - "${@}" -- "${_f_script}") + mapfile -td $'\0' "${__dest_name}" < <( + printf '%s\0' "${__src[@]}" | awk -v 'RS=\0' -v 'ORS=\0' \ + "${@}" -- "${__script}") wait "$!" if [[ "$?" -ne 0 ]]; then - err "${_f_label} failed" + err "${__label} failed" return 1 fi - if [[ ${#_f_src[@]} -gt 0 && ! -d "${_f_dest[0]}" ]]; then - err "${_f_label} returned empty or invalid list" + + if [[ ${#__src[@]} -gt 0 ]]; then + exists "${__dest[0]}" && return 0 + [[ "$?" -eq 1 ]] && \ + err "${__label} returned empty or invalid list" return 1 fi } +populate() { + local -n __arr="${1}" + local __arr_name="${1}" __dir="${2}" + + if [[ "${__dir}" == rclone:* ]]; then + mapfile -t "${__arr_name}" < \ + <(rclone lsf --dir-slash=false -- "${__dir#rclone:}" \ + 2>/dev/null) + wait "$!" + if [[ "$?" -ne 0 && "$?" -ne 3 ]]; then + err "rclone lsf failed: ${__dir#rclone:}" + return 1 + fi + + __arr=( "${__arr[@]/#/${__dir}/}" ) + return + fi + + mapfile -td $'\0' "${__arr_name}" < \ + <(shopt -s nullglob; printf '%s\0' "${__dir}/"*) +} + +copy() { + local src="${1}" dest="${2}" link_dest="${3}" + local path res r_dest r_link_dest + + for path in "${dest}" "${dest}.tmp"; do + exists "${path}" + res="$?" + if [[ "${res}" -ne 1 ]]; then + [[ "${res}" -eq 0 ]] && \ + err "destination already exists: ${path}" + return 1 + fi + done + + if [[ "${dest}" == rclone:* ]]; then + r_dest="${dest}.tmp" + r_link_dest="${link_dest}" + if [[ -d "${src}" ]]; then + # emulates rsync src trailing slash behaviour + r_dest+="/${src##*/}" + r_link_dest+="${r_link_dest:+/${src##*/}}" + fi + run rclone copy --sftp-copy-is-hardlink \ + ${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 \ + ${link_dest:+"--link-dest=${link_dest}"} \ + -- "${src}" "${dest}.tmp/" || return 1 + run mv -T -- "${dest}.tmp" "${dest}" || return 1 +} + +prune() { + local -n __snaps="${1}" __keep="${2}" + local __i __k + + for ((__i = ${#__snaps[@]} - 1, __k = 0; __i >= 0; --__i)); do + if [[ "${__snaps[__i]}" == "${__keep[__k]}" ]]; then + ((++__k)) + else + if [[ "${__snaps[__i]}" == rclone:* ]]; then + run rclone purge -- "${__snaps[__i]#rclone:}" + else + run rm -rf -- "${__snaps[__i]}" + fi + fi + done +} + backup() { local src_path="${1}" dest_dir="${2}" local retain="${3}" interval="${4}" week_start="${5}" - local dest_path snaps keep skip i ki + local dest_path snaps keep 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}/"*) + populate snaps "${dest_dir}" || return 1 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}.tmp/" || return 1 - run mv -T "${dest_path}.tmp" "${dest_path}" || return 1 + copy "${src_path}" "${dest_path}" "${snaps[0]}" || 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 -rf "${snaps[i]}" - fi - done + prune snaps keep } declare -A def_opts=( @@ -212,7 +292,7 @@ while IFS='=' read -r f1 f2; do printf '[%d] [%s] %s => %s%s\n' "$((++dcount))" \ "${opts['retain']}" "${f2}" "${dest}" - dest_rp="$(realpath -m "${dest}")" + dest_rp="$(realpath -m -- "${dest}")" if [[ -v "seen["${dest_rp}"]" ]]; then err "destination already seen: ${dest}" ((++errors)) |
