#!/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}" \ 'https://arch.eckner.net/os/'"${arch}"'/'"${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}" \ 'https://arch.eckner.net/os/'"${arch}"'/'"${file}"; do curl -Lo "${cache_file}" "${url}" || continue tar --zstd -tf "${cache_file}" >/dev/null 2>&1 && break done ;; 'armv6h') for url in \ 'http://software.is.never.null/arch/'"${arch}"'/'{extra,community,core,alarm,aur}'/'"${file}" \ 'https://arch.eckner.net/os/'"${arch}"'/'"${file}"; do curl -Lo "${cache_file}" "${url}" || continue tar --zstd -tf "${cache_file}" >/dev/null 2>&1 && break done ;; 'armv7h'|'aarch64') for url in \ 'https://mirror.archlinuxarm.org/'"${arch}"'/'{extra,community,core,alarm,aur}'/'"${file}" \ 'https://arch.eckner.net/os/'"${arch}"'/'"${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