348 lines
10 KiB
Bash
Executable File
348 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Generic Restic Backup Script
|
|
# Usage: ./backup.sh [OPTIONS] <source_path> <repository>
|
|
|
|
set -euo pipefail
|
|
|
|
# Default values
|
|
RESTIC_PASSWORD_FILE="$HOME/.restic-password"
|
|
LOG_FILE=""
|
|
HEALTHCHECK_URL=""
|
|
KEEP_DAILY=7
|
|
KEEP_WEEKLY=4
|
|
KEEP_MONTHLY=6
|
|
KEEP_YEARLY=2
|
|
TAG_PREFIX=""
|
|
DRY_RUN=false
|
|
VERBOSE=false
|
|
EXCLUDE_CACHES=true
|
|
ONE_FILE_SYSTEM=true
|
|
COMPRESSION="auto"
|
|
|
|
# Function to display usage
|
|
usage() {
|
|
cat << EOF
|
|
Usage: $0 [OPTIONS] <source_path> <repository>
|
|
|
|
Generic backup script using Restic for any folder and repository.
|
|
|
|
REQUIRED ARGUMENTS:
|
|
source_path Path to backup (e.g., /home/user/documents)
|
|
repository Restic repository URL (e.g., sftp:user@host:/path)
|
|
|
|
OPTIONS:
|
|
-p, --password-file FILE Password file path (default: ~/.restic-password)
|
|
-l, --log-file FILE Log file path (default: no logging)
|
|
-h, --healthcheck URL Healthchecks.io ping URL
|
|
-t, --tag TAG Additional tag for backup (can be used multiple times)
|
|
--tag-prefix PREFIX Prefix for auto-generated tags (default: none)
|
|
|
|
RETENTION POLICY:
|
|
--keep-daily N Keep N daily snapshots (default: 7)
|
|
--keep-weekly N Keep N weekly snapshots (default: 4)
|
|
--keep-monthly N Keep N monthly snapshots (default: 6)
|
|
--keep-yearly N Keep N yearly snapshots (default: 2)
|
|
|
|
BACKUP OPTIONS:
|
|
--no-exclude-caches Don't exclude cache directories
|
|
--no-one-file-system Allow crossing filesystem boundaries
|
|
--compression LEVEL Compression level: auto|max|off|fastest|better (default: auto)
|
|
|
|
OTHER OPTIONS:
|
|
--dry-run Show what would be backed up without doing it
|
|
-v, --verbose Enable verbose output
|
|
--help Show this help message
|
|
|
|
EXAMPLES:
|
|
# Basic backup
|
|
$0 /home/user/music sftp:user@host:/backups/music
|
|
|
|
# With custom retention and healthcheck
|
|
$0 --keep-daily 14 --healthcheck https://hc-ping.com/uuid \\
|
|
/var/www sftp:user@host:/backups/www
|
|
|
|
# Multiple tags and custom log file
|
|
$0 -t production -t database --tag-prefix server1 \\
|
|
--log-file /var/log/backup.log \\
|
|
/var/lib/mysql sftp:user@host:/backups/mysql
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
RESTIC_PASSWORD_FILE Alternative to --password-file
|
|
RESTIC_PASSWORD Direct password (not recommended)
|
|
RESTIC_REPOSITORY Alternative to repository argument
|
|
|
|
EOF
|
|
}
|
|
|
|
# Function to log messages
|
|
log_message() {
|
|
local message="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
|
|
|
if [ "$VERBOSE" = true ]; then
|
|
echo "$message"
|
|
fi
|
|
|
|
if [ -n "$LOG_FILE" ]; then
|
|
echo "$message" >> "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
# Function to send healthcheck ping
|
|
send_healthcheck() {
|
|
local status="$1"
|
|
local message="$2"
|
|
|
|
if [ -z "$HEALTHCHECK_URL" ]; then
|
|
[ "$VERBOSE" = true ] && echo "Healthcheck: $status - $message (no URL configured)"
|
|
return
|
|
fi
|
|
|
|
case "$status" in
|
|
"START")
|
|
curl -fsS -m 10 --retry 3 "$HEALTHCHECK_URL/start" >/dev/null 2>&1 || true
|
|
;;
|
|
"SUCCESS")
|
|
curl -fsS -m 10 --retry 3 --data-raw "$message" "$HEALTHCHECK_URL" >/dev/null 2>&1 || true
|
|
;;
|
|
"FAILED"|"WARNING")
|
|
curl -fsS -m 10 --retry 3 --data-raw "$message" "$HEALTHCHECK_URL/fail" >/dev/null 2>&1 || true
|
|
;;
|
|
esac
|
|
|
|
[ "$VERBOSE" = true ] && echo "Healthcheck sent: $status - $message"
|
|
}
|
|
|
|
# Parse command line arguments
|
|
CUSTOM_TAGS=()
|
|
POSITIONAL_ARGS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-p|--password-file)
|
|
RESTIC_PASSWORD_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-l|--log-file)
|
|
LOG_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-h|--healthcheck)
|
|
HEALTHCHECK_URL="$2"
|
|
shift 2
|
|
;;
|
|
-t|--tag)
|
|
CUSTOM_TAGS+=("$2")
|
|
shift 2
|
|
;;
|
|
--tag-prefix)
|
|
TAG_PREFIX="$2"
|
|
shift 2
|
|
;;
|
|
--keep-daily)
|
|
KEEP_DAILY="$2"
|
|
shift 2
|
|
;;
|
|
--keep-weekly)
|
|
KEEP_WEEKLY="$2"
|
|
shift 2
|
|
;;
|
|
--keep-monthly)
|
|
KEEP_MONTHLY="$2"
|
|
shift 2
|
|
;;
|
|
--keep-yearly)
|
|
KEEP_YEARLY="$2"
|
|
shift 2
|
|
;;
|
|
--no-exclude-caches)
|
|
EXCLUDE_CACHES=false
|
|
shift
|
|
;;
|
|
--no-one-file-system)
|
|
ONE_FILE_SYSTEM=false
|
|
shift
|
|
;;
|
|
--compression)
|
|
case "$2" in
|
|
auto|max|off|fastest|better)
|
|
COMPRESSION="$2"
|
|
;;
|
|
*)
|
|
echo "Error: Invalid compression value '$2'. Valid values: auto, max, off, fastest, better"
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
echo "Unknown option $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
POSITIONAL_ARGS+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Restore positional parameters
|
|
set -- "${POSITIONAL_ARGS[@]}"
|
|
|
|
# Check required arguments
|
|
if [ $# -lt 2 ]; then
|
|
echo "Error: Missing required arguments"
|
|
echo
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
SOURCE_PATH="$1"
|
|
REPOSITORY="$2"
|
|
|
|
# Use environment variable as fallback for repository
|
|
if [ -n "${RESTIC_REPOSITORY:-}" ] && [ "$REPOSITORY" = "${RESTIC_REPOSITORY}" ]; then
|
|
REPOSITORY="$RESTIC_REPOSITORY"
|
|
fi
|
|
|
|
# Use environment variable as fallback for password file
|
|
if [ -n "${RESTIC_PASSWORD_FILE:-}" ]; then
|
|
RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE}"
|
|
fi
|
|
|
|
# Validation
|
|
if [ ! -d "$SOURCE_PATH" ]; then
|
|
echo "ERROR: Source path does not exist: $SOURCE_PATH"
|
|
send_healthcheck "FAILED" "Source path not found: $SOURCE_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$RESTIC_PASSWORD_FILE" ] && [ -z "${RESTIC_PASSWORD:-}" ]; then
|
|
echo "ERROR: Restic password file not found: $RESTIC_PASSWORD_FILE"
|
|
echo " Set RESTIC_PASSWORD environment variable or provide valid password file"
|
|
send_healthcheck "FAILED" "Restic password file not found: $RESTIC_PASSWORD_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Build restic command base
|
|
RESTIC_CMD="restic -r $REPOSITORY"
|
|
if [ -f "$RESTIC_PASSWORD_FILE" ]; then
|
|
RESTIC_CMD="$RESTIC_CMD --password-file $RESTIC_PASSWORD_FILE"
|
|
fi
|
|
# Add compression as a global flag
|
|
RESTIC_CMD="$RESTIC_CMD --compression $COMPRESSION"
|
|
|
|
# Generate backup name from source path for tagging
|
|
BACKUP_NAME=$(basename "$SOURCE_PATH")
|
|
if [ -n "$TAG_PREFIX" ]; then
|
|
BACKUP_NAME="${TAG_PREFIX}-${BACKUP_NAME}"
|
|
fi
|
|
|
|
# Build tag arguments
|
|
TAG_ARGS=()
|
|
TAG_ARGS+=("--tag" "$BACKUP_NAME")
|
|
TAG_ARGS+=("--tag" "$(date +%Y-%m)")
|
|
|
|
# Add custom tags
|
|
for tag in "${CUSTOM_TAGS[@]}"; do
|
|
if [ -n "$TAG_PREFIX" ]; then
|
|
TAG_ARGS+=("--tag" "${TAG_PREFIX}-${tag}")
|
|
else
|
|
TAG_ARGS+=("--tag" "$tag")
|
|
fi
|
|
done
|
|
|
|
# Build backup command arguments
|
|
BACKUP_ARGS=()
|
|
if [ "$EXCLUDE_CACHES" = true ]; then
|
|
BACKUP_ARGS+=("--exclude-caches")
|
|
fi
|
|
if [ "$ONE_FILE_SYSTEM" = true ]; then
|
|
BACKUP_ARGS+=("--one-file-system")
|
|
fi
|
|
|
|
log_message "Starting backup of $SOURCE_PATH to $REPOSITORY"
|
|
send_healthcheck "START" "Starting backup of $BACKUP_NAME"
|
|
|
|
# Pre-backup: Check repository connectivity
|
|
log_message "Checking repository connectivity..."
|
|
CONNECTIVITY_OUTPUT=""
|
|
if ! CONNECTIVITY_OUTPUT=$(eval "$RESTIC_CMD snapshots --last" 2>&1); then
|
|
log_message "WARNING: Could not connect to repository or no snapshots exist yet"
|
|
if [ "$VERBOSE" = true ]; then
|
|
log_message "Connectivity check output: $CONNECTIVITY_OUTPUT"
|
|
fi
|
|
fi
|
|
|
|
# Perform backup
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_message "DRY RUN: Would backup $SOURCE_PATH"
|
|
echo "Would run: $RESTIC_CMD backup \"$SOURCE_PATH\" ${TAG_ARGS[*]} ${BACKUP_ARGS[*]}"
|
|
exit 0
|
|
fi
|
|
|
|
log_message "Creating backup snapshot..."
|
|
log_message "Running command: $RESTIC_CMD backup \"$SOURCE_PATH\" ${TAG_ARGS[*]} ${BACKUP_ARGS[*]}"
|
|
|
|
BACKUP_OUTPUT=""
|
|
BACKUP_SUCCESS=false
|
|
|
|
if [ "$VERBOSE" = true ]; then
|
|
# Show progress in verbose mode
|
|
if eval "$RESTIC_CMD backup \"$SOURCE_PATH\" ${TAG_ARGS[*]} ${BACKUP_ARGS[*]}"; then
|
|
BACKUP_SUCCESS=true
|
|
fi
|
|
else
|
|
# Capture output for logging
|
|
if BACKUP_OUTPUT=$(eval "$RESTIC_CMD backup \"$SOURCE_PATH\" ${TAG_ARGS[*]} ${BACKUP_ARGS[*]}" 2>&1); then
|
|
BACKUP_SUCCESS=true
|
|
fi
|
|
fi
|
|
|
|
if [ "$BACKUP_SUCCESS" = true ]; then
|
|
log_message "Backup completed successfully"
|
|
if [ "$VERBOSE" = true ] && [ -n "${BACKUP_OUTPUT:-}" ]; then
|
|
echo "$BACKUP_OUTPUT"
|
|
fi
|
|
|
|
# Clean up old snapshots according to retention policy
|
|
log_message "Applying retention policy..."
|
|
FORGET_OUTPUT=""
|
|
if FORGET_OUTPUT=$(eval "$RESTIC_CMD forget --tag $BACKUP_NAME --keep-daily $KEEP_DAILY --keep-weekly $KEEP_WEEKLY --keep-monthly $KEEP_MONTHLY --keep-yearly $KEEP_YEARLY --prune" 2>&1); then
|
|
log_message "Retention policy applied successfully"
|
|
|
|
# Get backup stats for healthcheck message
|
|
BACKUP_STATS=""
|
|
BACKUP_STATS=$(eval "$RESTIC_CMD stats --mode raw-data" 2>/dev/null | tail -n 3 | head -n 1 2>/dev/null || echo "Stats unavailable")
|
|
send_healthcheck "SUCCESS" "Backup of $BACKUP_NAME completed successfully. Repository: $BACKUP_STATS"
|
|
else
|
|
log_message "WARNING: Backup succeeded but retention cleanup failed"
|
|
if [ "$VERBOSE" = true ] && [ -n "$FORGET_OUTPUT" ]; then
|
|
log_message "Retention error output: $FORGET_OUTPUT"
|
|
fi
|
|
send_healthcheck "WARNING" "Backup of $BACKUP_NAME succeeded but retention cleanup failed"
|
|
fi
|
|
else
|
|
log_message "ERROR: Backup failed"
|
|
if [ -n "${BACKUP_OUTPUT:-}" ]; then
|
|
log_message "Backup error output: $BACKUP_OUTPUT"
|
|
fi
|
|
send_healthcheck "FAILED" "Backup of $BACKUP_NAME failed - check logs for details"
|
|
exit 1
|
|
fi
|
|
|
|
log_message "Backup process completed"
|