summaryrefslogtreecommitdiff
path: root/hardlinked-backup.in
diff options
context:
space:
mode:
Diffstat (limited to 'hardlinked-backup.in')
-rwxr-xr-xhardlinked-backup.in313
1 files changed, 313 insertions, 0 deletions
diff --git a/hardlinked-backup.in b/hardlinked-backup.in
new file mode 100755
index 0000000..ab6fac0
--- /dev/null
+++ b/hardlinked-backup.in
@@ -0,0 +1,313 @@
+#!/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