Files
cloud-backup/templates/opt/backup/inst/cloud-server-backup.j2
T
lhahnandClaude Opus 5.5 2a19c705be Backup server: skip self-pruning repos in remote-prune
backup_server_remote_skip lists apps whose client prunes the repository itself; remote-prune without arguments leaves them alone (an explicit app list still works).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:26:36 +02:00

285 lines
9.0 KiB
Django/Jinja

{% set server_clients = backup_server_clients | default(backup_clients) %}
#!/bin/bash
BORGUSER="{{ backup_owner }}";
TARGETFOLDER="{{ backup_remotes_folder }}";
LOCALREPOLOCATION="{{ backup_location }}/{{ backup_app }}";
APPLIST="{{ server_clients | json_query('*.app') | join(' ') }}";
# repos that are only pruned remotely, not mirrored locally
LOCALAPPLIST="{{ server_clients | json_query('*.app') | difference(backup_server_local_skip) | join(' ') }}";
# repos pruned by their own client (e.g. a NAS borg plugin), skipped by remote-prune without arguments
REMOTEPRUNEAPPLIST="{{ server_clients | json_query('*.app') | difference(backup_server_remote_skip) | join(' ') }}";
MIRRORFOLDER="{{ backup_server_mirror | default(backup_remotes_folder) }}";
declare -A BORG_CLIENT_MAP
{% for client in server_clients %}
BORG_CLIENT_MAP[{{ server_clients[client].app }}]="{{ client }}";
{% endfor %}
declare -A BORG_USER_MAP
{% for client in server_clients %}
BORG_USER_MAP[{{ server_clients[client].app }}]="{{ server_clients[client].owner }}";
{% endfor %}
help (){
echo "cloud-server-backup - backup and restore script on cloud backup with borg target v1.0 by L.Hahn.
Usage: $0 COMMAND [APPS]
COMMAND:
- remote-apps-list List available apps in remote borg repositories.
- remote-list [APPS] List available archives for apps in remote borg repositories.
- remote-prune [APPS] Prune remote app (or all) borg repositories.
- list List available archives of remote repositories on local repository.
- backup Perform backup of remote borg repositories and create a new archive in local borg repository.
- prune Prune local borg backup of remote repositories.
- restore ARCHIVENAME [APPS] Upload backup to borg repository to restore older file states.
May turn off your application if still running.
If latest is provided as ARCHIVENAME, the latest one based on timestamp is taken.
IF ARCHIVENAME is provided, will try to download it; throws error if not found.
If APP is provided, only the mentioned app repositories are restored.
APPS: A single app or comma-separated (sub-)set of below listed apps to be considered.
{% for client in server_clients %}
- {{ server_clients[client].app }}
{% endfor %}
";
}
###############################################################################
#=== HELP FUNCTIONS ==========================================================#
###############################################################################
remote_delete_single_backup () {
REPOLOCATION=$1;
ARCHIVENAME=$2;
BORG_PASSPHRASE=$3;
KEYFILE=$4;
echo "Remove archive $ARCHIVENAME";
sudo -H -u $BORGUSER bash -c '
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg delete $REPOLOCATION::$ARCHIVENAME --rsh "/usr/bin/ssh -i $KEYFILE"';
}
remote_free_space () {
REPOLOCATION=$1;
KEYFILE=$2;
APP=$3;
echo "Free space on repository for app '$APP'.";
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg compact $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"';
}
has_argument () {
if [[ -z "$1" ]];
then
echo "Missing required paramter!";
exit 1;
fi
}
validate_apps_or_default () {
VALID=1;
if [[ -z "$1" ]];
then
echo $APPLIST;
else
CHECKLIST=$(echo $1 | tr ',' ' ');
for APP in $CHECKLIST;
do
if [[ ! ${APPLIST[@]} =~ $APP ]]
then
VALID=0;
echo "$APP is not a valid app.";
fi
done
if [[ ! $VALID == 1 ]]
then
echo "Aborting.";
exit 1;
else
echo $CHECKLIST;
fi
fi
}
###############################################################################
#=== USECASE FUNCTIONS =======================================================#
###############################################################################
remote_list_app () {
for APP in $APPLIST;
do
echo $APP;
done
}
remote_list () {
REMOTELIST=$1;
for APP in $REMOTELIST;
do
REPOSITORYCLIENT=${BORG_CLIENT_MAP[$APP]}
BORG_REPO_USER=${BORG_USER_MAP[$APP]}
REPOLOCATION="ssh://$BORG_REPO_USER@{{ backup_host }}:{{ backup_ssh_port }}/./$APP"
BORG_PASSPHRASE=$(cat {{ backup_key_folder }}/$APP)
KEYFILE="{{ backup_home }}/.ssh/$REPOSITORYCLIENT"
ARCHIVEIDS=$(
sudo -H -u $BORGUSER bash -c '
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg list $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"' | sort -r
);
echo "$ARCHIVEIDS";
done
}
remote_prune () {
REMOTEPRUNELIST=$1;
for APP in $REMOTEPRUNELIST;
do
BORG_REPO_USER=${BORG_USER_MAP[$APP]}
BORG_PASSPHRASE=$(cat {{ backup_key_folder }}/$APP)
REPOLOCATION="ssh://$BORG_REPO_USER@{{ backup_host }}:{{ backup_ssh_port }}/./$APP"
KEYFILE="{{ backup_home }}/.ssh/${BORG_CLIENT_MAP[$APP]}"
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
borg prune -H 24 -d 7 -w 4 -m -1 -y 100 $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"';
remote_free_space $REPOLOCATION $KEYFILE $APP;
done
}
local_list () {
ARCHIVEIDS=$(
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$LOCALREPOLOCATION';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg list $REPOLOCATION' | grep {{ backup_app }} | sort -r);
echo "$ARCHIVEIDS";
}
local_prune () {
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$LOCALREPOLOCATION';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg prune -H 24 -d 7 -w 4 -m -1 -y 100 $REPOLOCATION';
echo "Free space on local repository.";
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$LOCALREPOLOCATION';
borg compact $REPOLOCATION';
}
local_backup () {
ARCHIVENAME="{{ backup_app }}-$(date '+%s')";
mkdir -p $MIRRORFOLDER;
chown $BORGUSER: $MIRRORFOLDER;
# keep a persistent mirror so rsync only transfers what changed
for APP in $LOCALAPPLIST;
do
sudo -H -u $BORGUSER bash -c '
MIRRORFOLDER='$MIRRORFOLDER';
REPOLOCATION='${BORG_CLIENT_MAP[$APP]}':'$APP';
rsync -ar --delete $REPOLOCATION $MIRRORFOLDER/;';
done
# borg repositories are encrypted already, compression rarely helps
sudo -H -u $BORGUSER bash -c '
MIRRORFOLDER='$MIRRORFOLDER';
REPOLOCATION='$LOCALREPOLOCATION';
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg create -C {{ backup_server_compression }} $REPOLOCATION::$ARCHIVENAME $MIRRORFOLDER;';
}
local_delete () {
ARCHIVENAME=$1;
ARCHIVEIDS=$(local_list | cut -f 1 -d ' ');
if [[ "$ARCHIVEIDS" != *$ARCHIVENAME* ]];
then
echo "ERROR! Provided archivename '$ARCHIVENAME' is not part of the available archives! Aborting.";
exit 1;
fi
echo "Remove archive $ARCHIVENAME";
sudo -H -u $BORGUSER bash -c '
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE='$(cat {{ backup_home }}/.borg.key)';
REPOLOCATION='$LOCALREPOLOCATION';
borg delete $REPOLOCATION::$ARCHIVENAME';
}
###############################################################################
#=== MAIN FUNCTIONS ==========================================================#
###############################################################################
(
flock -n 9 || {
echo "BACKUP ALREADY RUNNING! ABORTING.";
exit 1;
}
if [ $# = 0 ]; then
help;
exit 0;
fi
action=$1
case $action in
"remote-apps-list")
remote_list_app;
;;
"remote-list")
USERLIST=$(validate_apps_or_default "$2");
remote_list "$USERLIST";
;;
"remote-prune")
if [[ -z "$2" ]]; then
USERLIST=$REMOTEPRUNEAPPLIST;
else
USERLIST=$(validate_apps_or_default "$2");
fi
remote_prune "$USERLIST";
;;
"list")
local_list;
;;
"prune")
local_prune;
;;
"backup")
local_backup;
;;
"restore")
has_argument $2;
USERLIST=$(validate_apps_or_default $3);
#local_restore $2;
;;
"delete")
has_argument $2;
local_delete $2;
;;
*)
help;
exit 0;
;;
esac
) 9>/var/run/lock/cloud-server-backup.lock;