diff options
Diffstat (limited to 'hardlinked-backup.in')
-rwxr-xr-x | hardlinked-backup.in | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/hardlinked-backup.in b/hardlinked-backup.in new file mode 100755 index 0000000..d5c76e2 --- /dev/null +++ b/hardlinked-backup.in @@ -0,0 +1,302 @@ +#!/bin/bash + +rsyncOptions='-vxcaHAXF' +rsync --help | \ + grep -q -- --may-modify-others && \ + rsyncOptions="${rsyncOptions} --may-modify-others" + +[ -r "#ETCDIR#/backup.conf" ] && \ + . "#ETCDIR#/backup.conf" + +usage() +{ + >&2 echo \ +'Usage: hardlinked-backup /tmp/pidFile /path/to/destination/ user@source:path [proxy_user@ssh_host] +Backup files from remote with rsync, possibly via SSH-tunnel. + +With no arguments, information is expected to be in array $backups in #ETCDIR#/backup.conf with name of executable (e.g. a symlink) as key. +With one argument, information is expected to be in array ${backups[$1]} in #ETCDIR#/backup.conf. + +Options: + /tmp/pidFile location of file to store PID in + /path/to/destination location to store backups in + user@source:path remote data to back up + proxy_user@ssh_host ssh login to proxy node (optional) +#HELPTEXT# #' + [ -z "$1" ] && exit 1 + exit $1 +} + +extract_ssh_host() { + { + printf '%s\n' "$1" + if [ -f ~/.ssh/config ]; then + sed ' + /^Host\s\+'"$1"'$/,/^Host\s/ { + s/^\s*Hostname\s\+// + t + } + d + ' ~/.ssh/config + fi + } \ + | tail -n1 +} + +extract_ssh_ip_protocols() { + getent ahosts "$(extract_ssh_host "$1")" \ + | sed ' + s/^\([0-9]\+\.\)\{3\}[0-9]\+\s\+STREAM\(\s.*\)\?$/4/ + t + s/^[0-9a-f][0-9a-f:]\+\s\+STREAM\(\s.*\)\?$/6/ + t + d + ' \ + | sort -u \ + | if [ -f ~/.ssh/config ]; then + grep -vxF "$( + sed ' + /^Host\s\+'"$1"'$/,/^Host\s/ { + s/^\s*AddressFamily\s\+inet\s*$/6/ + t + s/^\s*AddressFamily\s\+inet6\s*$/4/ + t + } + d + ' ~/.ssh/config + )" + else + cat + fi +} + +connected_ip_versions() { + ip -o addr show scope global \ + | awk '{print $4}' \ + | sed ' + s@^\([0-9]\+\.\)\{3\}[0-9]\+\(/[0-9]\+\)\?$@4@ + t + s@^[0-9a-f][0-9a-f:]\+\(/[0-9]\+\)\?$@6@ + t + d + ' \ + | sort -u +} + +if [ $# -eq 1 ]; then + if [ "$1" == "--help" ]; then + usage 0 + elif [ "$1" == "--version" ]; then + echo '#VERSION#' + exit + fi +fi + +seldom=false +if [ $# -eq 0 ] || [ $# -eq 1 ]; then + if [ $# -eq 0 ]; then + backupID="$(basename $0)" + else + backupID="$1" + fi + [ -z "${backups[${backupID}]}" ] && usage + set /tmp/${backupID}.pid ${backups[${backupID}]} + if printf '%s\n' "${seldomBackups[@]}" | \ + grep -qxF "${backupID}"; then + seldom=true + fi +fi + +Basis="$2" +pidFile="$1" +QuellIP=$(echo "$3" | sed "s|^[a-zA-Z]*://||; s|^[a-zA-Z]*@||; s|:\?/.*$||") + +if [ "$#" -eq 3 ]; then + Quelle="$3" + ipVer=$( + { + extract_ssh_ip_protocols "${QuellIP}" \ + | sort -n + extract_ssh_ip_protocols "${QuellIP}" \ + | grep -xF "$(connected_ip_versions)" \ + | sort -n + } \ + | tail -n1 + ) +elif [ "$#" -eq 4 ]; then + sshHopp="$4" + lokPort=$[$RANDOM/2+8000] + HoppIP="${sshHopp#*@}" + ipVer=$( + { + extract_ssh_ip_protocols "${QuellIP}" + extract_ssh_ip_protocols "${HoppIP}" + } \ + | sort -n \ + | uniq -d \ + | tail -n1 + ) + if [ -z ${ipVer} ]; then + >&2 echo 'not reachable' + exit 11 + fi + if [ ${ipVer} -eq 4 ]; then + localAddress='127.0.0.1' + elif [ ${ipVer} -eq 6 ]; then + localAddress='[::1]' + else + >&2 echo 'ip version of hopp and target must be the same' + exit 1 + fi + rsyncShell="-e ssh -${ipVer} -p${lokPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + tunnelBefehl="ssh -${ipVer} -t -t -L${localAddress}:${lokPort}:${QuellIP}:22 ${sshHopp} while sleep 60; do date; done" + Quelle="$(echo "$3" | sed "s|${QuellIP}|${localAddress}|")" +else + usage +fi + +rsyncOptions="${rsyncOptions}${ipVer}" + +if [ ! -d ${Basis} ]; then + for neededMount in "${neededMounts[@]}"; do + if ! mountpoint -q "${neededMount}"; then + >&2 printf 'Mountpoint %s is not available.\n' "${neededMount}" + exit 11 + fi + done + exit 2 +fi + +neues_Datum="${Basis}/$(date "+%Y_%m_%d")" +neues="${Basis}/aktuell" +linkdests="" +for s in $(ls -1 "${Basis}" | sort -r | grep -vxF -m 20 aktuell ); do + linkdests="${linkdests} --link-dest ${Basis}/${s}" +done + +if [ ! "$(whoami)" == "root" ]; then + echo "I need to be root." + exit 3 +fi + +[ -e "${neues_Datum}" ] && exit 4 +if ${seldom}; then + for date_diff in $(seq ${seldomness}); do + if [ -e "${Basis}/$(date '+%Y_%m_%d' -d@$(($(date '+%s')-24*60*60*date_diff)))" ]; then + exit 4 + fi + done +fi +[ -w "${Basis}" ] || exit 11 +[ -s "${pidFile}" ] && [ -d "/proc/$(cat "${pidFile}")" ] && exit 5 + +echo $$ > "${pidFile}" + +if [ -n "${tunnelBefehl}" ]; then + preConnectHook + ${tunnelBefehl} & + backgroundPid=$! + sleep 4 +fi + +for toExclude in "${excludes[@]}"; do + excludeArgs="${excludeArgs} --exclude ${toExclude}" +done + +mkdir -p "${neues}/wip" +chmod 750 "${neues}"{,/wip} +chown root:root "${neues}"{,/wip} +if [ -z "${rsyncShell}" ]; then + preConnectHook + if [ -z "${Quelle#*@*:/}" ]; then + ssh "-${ipVer}" "${Quelle%:/}" ' + pacman-conf Architecture + find /var/lib/pacman/local -name desc -exec cat {} + + ' \ + | { + read -r arch + sed -n ' + /^%\(NAME\|VERSION\|ARCH\)%/ { + N + s/\n/ / + p + } + ' \ + | sed ' + N + N + s@^%NAME% \(\S\+\)\n%VERSION% \(\S\+\)\n%ARCH% \(\S\+\)$@\1-\2-\3.pkg.tar.zst@ + t + w /dev/stderr + d + ' \ + | while read -r file; do + cache_file='/var/cache/pacman/pkg/'"${file}" + if [ ! -f "${cache_file}" ]; then + case "${arch}" in + 'x86_64') + for url in \ + 'https://mirror.f4st.host/archlinux/pool/packages/'"${file}" \ + 'https://archive.archlinux.org/packages/'"${file:0:1}"'/'"${file%-*}"'/'"${file}"; do + curl -Lo "${cache_file}" "${url}" || continue + tar --zstd -tf "${cache_file}" >/dev/null 2>&1 && break + done + ;; + 'i486'|'i686'|'pentium4') + for url in \ + 'https://mirror.archlinux32.org/pool/'"${file}" \ + 'https://archive.archlinux32.org/packages/'"${file:0:1}"'/'"${file%-*}"'/'"${file}"; do + curl -Lo "${cache_file}" "${url}" || continue + tar --zstd -tf "${cache_file}" >/dev/null 2>&1 && break + done + ;; + *) + for url in \ + 'https://mirror.archlinux.org/'"${arch}"'/'{extra,community,core,alarm,aur}'/'"${file}"; do + curl -Lo "${cache_file}" "${url}" || continue + tar --zstd -tf "${cache_file}" >/dev/null 2>&1 && break + done + ;; + esac + fi + if [ -f "${cache_file}" ]; then + tar -C "${neues:?}/wip/" --zstd -xkf "${cache_file}" + fi + done + } + fi + rsync ${rsyncOptions} \ + ${linkdests} \ + ${excludeArgs} \ + ${Quelle} "${neues}/wip/" + sleep 1 + preConnectHook + rsync ${Quelle} +else + preConnectHook + rsync "${rsyncShell}" \ + ${rsyncOptions} \ + ${linkdests} \ + ${excludeArgs} \ + ${Quelle} "${neues}/wip/" + sleep 1 + preConnectHook + rsync "${rsyncShell}" ${Quelle} +fi +erg=$? + +[ -n "${backgroundPid}" ] && kill "${backgroundPid}" + +if [ ${erg} -eq 0 ] || [ ${erg} -eq 24 ] && ! rmdir "${neues}/wip" 2>/dev/null; then + chmod o-rwx "${neues}/wip" + neueres_Datum="${Basis}/$(date "+%Y_%m_%d")" + if [ ! -e "${neueres_Datum}" ]; then + neues_Datum="${neueres_Datum}" + fi + mv "${neues}/wip" "${neues_Datum}" + rmdir "${neues}" + rm "${pidFile}" +else + rm "${pidFile}" + exit 11 +fi |