Skip to content

Instantly share code, notes, and snippets.

@tflori
Last active March 21, 2025 10:36
Show Gist options
  • Save tflori/86afd8016454a95869eb7ab5a002ad53 to your computer and use it in GitHub Desktop.
Save tflori/86afd8016454a95869eb7ab5a002ad53 to your computer and use it in GitHub Desktop.
Backup Sript
# Backup Duration: How old a backup can be before a new one is triggered (in days)
BACKUP_DURATION=5
# Base backup location (will create a folder named by hostname)
BACKUP_LOCATION="/mnt/backups/$(hostname)"
# Backup sources (identifier and path)
BACKUP_BACKUPS=(
"myhome $HOME"
# "identifier" "/path/to/other/folder"
)
# Folders to exclude from backup
BACKUP_EXCLUDES=(
"myhome /Downloads"
"myhome /.cache"
# "identifier" "exclude pattern"
)
# Backup options for each source (incremental, keep, or versioned)
BACKUP_OPTIONS=(
"myhome incremental"
# "identifier" "space separated options"
)
# Compress specific paths in the backup (not incremental or versioned)
BACKUP_COMPRESSION=(
"myhome .config/vivaldi"
# "identifier" "relative/path"
)
# RAID metadata to backup (list of md devices, e.g., "md0")
BACKUP_RAID_META=(
# "md0"
)
# Commands to mount necessary drives before backup (optional)
BACKUP_MOUNT_BEFORE=(
# "mount -t ntfs /dev/sdX /mnt"
)
#!/bin/bash
# Load the config file
source ~/.config/backup.conf.sh
# Default values
FORCE_BACKUP=false
DRY_RUN=false
VERBOSE=false
LIST_BACKUPS=false
SPECIFIC_BACKUP=""
# Function to display usage
usage() {
echo "Usage: rbackup [options] [<backup>]"
echo " <backup> Backup only this part (optional; will not update timestamp)"
echo "Options:"
echo " --force, -f Ignore backup timestamp and don't ask"
echo " --dry-run, -n Simulate backup without copying files"
echo " --verbose, -v Show detailed output"
echo " --list, -l List available backups"
echo " --help, -h Show this help message"
exit 0
}
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--force|-f)
FORCE_BACKUP=true
;;
--dry-run|-n)
DRY_RUN=true
;;
--verbose|-v)
VERBOSE=true
;;
--list|-l)
LIST_BACKUPS=true
;;
--help|-h)
usage
;;
*)
SPECIFIC_BACKUP="$1"
;;
esac
shift
done
# If --list is used, show available backups and exit
if [ "$LIST_BACKUPS" = true ]; then
echo "Available backups:"
for ITEM in "${BACKUP_BACKUPS[@]}"; do
IDENTIFIER=$(echo "$ITEM" | /usr/bin/cut -d ' ' -f 1)
echo " - $IDENTIFIER"
done
exit 0
fi
# Define the backup file to check the last backup date
BACKUP_FILE="$BACKUP_LOCATION/.backup"
# Check if the last backup exists and is not too old, unless forced
if [ "$FORCE_BACKUP" = false ] && [ -f "$BACKUP_FILE" ]; then
LAST_BACKUP=$(cat "$BACKUP_FILE")
LAST_BACKUP_DATE=$(/usr/bin/date -d "$LAST_BACKUP" +%s)
CURRENT_DATE=$(/usr/bin/date +%s)
DAYS_SINCE_LAST_BACKUP=$(( (CURRENT_DATE - LAST_BACKUP_DATE) / 86400 ))
if [ "$DAYS_SINCE_LAST_BACKUP" -lt "$BACKUP_DURATION" ]; then
echo "Last backup was $DAYS_SINCE_LAST_BACKUP days ago. No backup needed."
exit 0
fi
fi
# Ask the user if they want to start a backup now (unless forced)
if [ "$FORCE_BACKUP" = false ]; then
if ! zenity --question --title="Backup" --text="Do you want to start a backup now?" --default-cancel; then
echo "Backup cancelled by user."
exit 1
fi
fi
# Mount necessary drives (optional)
for MOUNT_CMD in "${BACKUP_MOUNT_BEFORE[@]}"; do
eval "$MOUNT_CMD"
done
# Backup RAID metadata if necessary
for RAID in "${BACKUP_RAID_META[@]}"; do
if [ -e "/dev/$RAID" ]; then
echo "Backing up RAID metadata: $RAID"
dd if="/dev/$RAID" of="$BACKUP_LOCATION/$(hostname)_$RAID.$(/usr/bin/date +%F).bin" bs=1M
fi
done
# Loop through each backup source
for ITEM in "${BACKUP_BACKUPS[@]}"; do
IDENTIFIER=$(echo "$ITEM" | /usr/bin/cut -d ' ' -f 1)
BACKUP_PATH=$(echo "$ITEM" | /usr/bin/cut -d ' ' -f 2)
# If a specific backup is requested, skip others
if [ -n "$SPECIFIC_BACKUP" ] && [ "$IDENTIFIER" != "$SPECIFIC_BACKUP" ]; then
continue
fi
# Check for exclusions
EXCLUDE_PATTERNS=""
for EXCLUDE in "${BACKUP_EXCLUDES[@]}"; do
EXCLUDE_ID=$(echo "$EXCLUDE" | /usr/bin/cut -d ' ' -f 1)
EXCLUDE_PATH=$(echo "$EXCLUDE" | /usr/bin/cut -d ' ' -f 2)
if [ "$EXCLUDE_ID" == "$IDENTIFIER" ]; then
EXCLUDE_PATTERNS="$EXCLUDE_PATTERNS --exclude=$EXCLUDE_PATH"
fi
done
# Check for compression config
for COMPRESSION_ITEM in "${BACKUP_COMPRESSION[@]}"; do
COMP_IDENTIFIER=$(echo "$COMPRESSION_ITEM" | /usr/bin/cut -d ' ' -f 1)
COMPRESSION_PATH=$(echo "$COMPRESSION_ITEM" | /usr/bin/cut -d ' ' -f 2)
if [ "$IDENTIFIER" == "$COMP_IDENTIFIER" ]; then
TAR_FILE="$BACKUP_LOCATION/$IDENTIFIER-$(basename "$COMPRESSION_PATH").tar.gz"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would compress $COMPRESSION_PATH to $TAR_FILE"
else
echo "Compressing $COMPRESSION_PATH for $IDENTIFIER"
# Check if pv is available for progress output
if command -v pv &>/dev/null; then
tar -cf - -C "$BACKUP_PATH" "$COMPRESSION_PATH" | pv -s $(du -sb "$BACKUP_PATH/$COMPRESSION_PATH" | awk '{print $1}') | gzip > $TAR_FILE
else
tar -czf $TAR_FILE -C "$BACKUP_PATH" "$COMPRESSION_PATH"
fi
fi
EXCLUDE_PATTERNS="$EXCLUDE_PATTERNS --exclude=$COMPRESSION_PATH"
fi
done
# Define rsync options
OPTIONS=""
for OPTION in "${BACKUP_OPTIONS[@]}"; do
if [[ "$OPTION" == "$IDENTIFIER" ]]; then
if [[ "$OPTION" == *"incremental"* ]]; then
OPTIONS="$OPTIONS --link-dest=$BACKUP_LOCATION/$IDENTIFIER/previous"
fi
if [[ "$OPTION" == *"versioned"* ]]; then
OPTIONS="$OPTIONS --backup --backup-dir=$BACKUP_LOCATION/$IDENTIFIER/versions"
fi
fi
done
# Perform the backup using rsync
echo "Backing up $IDENTIFIER from $BACKUP_PATH"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would execute: /usr/bin/rsync -avh --progress $EXCLUDE_PATTERNS $OPTIONS \"$BACKUP_PATH/\" \"$BACKUP_LOCATION/$IDENTIFIER/\""
else
/usr/bin/rsync -ah --info=progress2 $EXCLUDE_PATTERNS $OPTIONS "$BACKUP_PATH/" "$BACKUP_LOCATION/$IDENTIFIER/"
fi
done
# Update the last backup date unless a specific backup was run or it's a dry run
if [ "$DRY_RUN" = false ] && [ -z "$SPECIFIC_BACKUP" ]; then
/usr/bin/date "+%FT%T" > "$BACKUP_FILE"
fi
echo "Backup completed!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment