a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
270 lines
8.8 KiB
Bash
270 lines
8.8 KiB
Bash
#!/bin/bash
|
|
#############################################################
|
|
#### Docker Volume Backup Script ####
|
|
#### Backup and restore Docker named volumes using ####
|
|
#### tar archives with optional compression ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version: 1.0 ####
|
|
#### ####
|
|
#### Usage: ./docker-volume-backup.sh [OPTIONS] ####
|
|
#############################################################
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME=$(basename "$0")
|
|
readonly SCRIPT_NAME
|
|
readonly DEFAULT_BACKUP_DIR="/opt/docker-backups"
|
|
readonly DEFAULT_RETAIN=7
|
|
readonly ALPINE_IMAGE="alpine:latest"
|
|
|
|
BACKUP_DIR="$DEFAULT_BACKUP_DIR"
|
|
RETAIN="$DEFAULT_RETAIN"
|
|
MODE=""
|
|
TARGET_VOLUME=""
|
|
RESTORE_ARCHIVE=""
|
|
|
|
# Colors
|
|
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m'
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
|
|
log_step() { echo -e "${BLUE}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Usage: $SCRIPT_NAME [OPTIONS]
|
|
|
|
Backup and restore Docker named volumes using tar archives with compression.
|
|
|
|
OPTIONS:
|
|
--backup [VOLUME] Backup all named volumes, or a specific volume if given
|
|
--restore ARCHIVE Restore a volume from the specified tar.gz archive
|
|
--list List available backups
|
|
--backup-dir PATH Backup directory (default: $DEFAULT_BACKUP_DIR)
|
|
--retain N Number of backups to keep per volume (default: $DEFAULT_RETAIN)
|
|
--help, -h Show this help message
|
|
|
|
EXAMPLES:
|
|
$SCRIPT_NAME --backup
|
|
$SCRIPT_NAME --backup my_volume
|
|
$SCRIPT_NAME --restore $DEFAULT_BACKUP_DIR/my_volume_20260309_143022.tar.gz
|
|
$SCRIPT_NAME --list
|
|
$SCRIPT_NAME --backup --backup-dir /mnt/backups --retain 14
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--backup)
|
|
MODE="backup"; shift
|
|
[[ $# -gt 0 && ! "$1" =~ ^-- ]] && { TARGET_VOLUME="$1"; shift; }
|
|
;;
|
|
--restore)
|
|
MODE="restore"
|
|
[[ $# -lt 2 ]] && { log_error "--restore requires an archive path"; exit 1; }
|
|
RESTORE_ARCHIVE="$2"; shift 2
|
|
;;
|
|
--list) MODE="list"; shift ;;
|
|
--backup-dir)
|
|
[[ $# -lt 2 ]] && { log_error "--backup-dir requires a path"; exit 1; }
|
|
BACKUP_DIR="$2"; shift 2
|
|
;;
|
|
--retain)
|
|
[[ $# -lt 2 ]] && { log_error "--retain requires a number"; exit 1; }
|
|
RETAIN="$2"; shift 2
|
|
;;
|
|
--help|-h) show_help ;;
|
|
*) log_error "Unknown option: $1"; show_help ;;
|
|
esac
|
|
done
|
|
if [[ -z "$MODE" ]]; then
|
|
log_error "No action specified. Use --backup, --restore, or --list."
|
|
show_help
|
|
fi
|
|
}
|
|
|
|
check_dependencies() {
|
|
if ! command -v docker &>/dev/null; then
|
|
log_error "docker is required but not installed"; exit 1
|
|
fi
|
|
if ! docker info &>/dev/null; then
|
|
log_error "Cannot connect to Docker daemon. Is it running?"; exit 1
|
|
fi
|
|
}
|
|
|
|
backup_volume() {
|
|
local volume_name="$1"
|
|
local timestamp archive_name final_path tmp_file size
|
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
|
archive_name="${volume_name}_${timestamp}.tar.gz"
|
|
final_path="${BACKUP_DIR}/${archive_name}"
|
|
|
|
log_step "Backing up volume: ${volume_name}"
|
|
|
|
if ! docker volume inspect "$volume_name" &>/dev/null; then
|
|
log_error "Volume '$volume_name' does not exist"; return 1
|
|
fi
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
tmp_file=$(mktemp "${BACKUP_DIR}/.backup_XXXXXX.tar.gz")
|
|
|
|
if docker run --rm \
|
|
-v "${volume_name}:/source:ro" \
|
|
-v "${BACKUP_DIR}:/backup" \
|
|
"$ALPINE_IMAGE" \
|
|
tar czf "/backup/$(basename "$tmp_file")" -C /source . 2>/dev/null; then
|
|
mv "$tmp_file" "$final_path"
|
|
size=$(du -h "$final_path" | cut -f1)
|
|
log_info "Created backup: ${final_path} (${size})"
|
|
else
|
|
rm -f "$tmp_file"
|
|
log_error "Failed to backup volume: ${volume_name}"; return 1
|
|
fi
|
|
}
|
|
|
|
do_backup() {
|
|
log_step "Starting Docker volume backup"
|
|
log_info "Backup directory: ${BACKUP_DIR}"
|
|
|
|
local volumes=()
|
|
if [[ -n "$TARGET_VOLUME" ]]; then
|
|
volumes=("$TARGET_VOLUME")
|
|
else
|
|
while IFS= read -r vol; do
|
|
[[ -n "$vol" ]] && volumes+=("$vol")
|
|
done < <(docker volume ls --format '{{.Name}}' | sort)
|
|
fi
|
|
|
|
if [[ ${#volumes[@]} -eq 0 ]]; then
|
|
log_warn "No Docker named volumes found"; return 0
|
|
fi
|
|
log_info "Found ${#volumes[@]} volume(s) to backup"
|
|
|
|
local success=0 failed=0
|
|
for vol in "${volumes[@]}"; do
|
|
if backup_volume "$vol"; then
|
|
success=$((success + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
done
|
|
|
|
apply_retention
|
|
log_step "Backup complete: ${success} succeeded, ${failed} failed"
|
|
[[ $failed -gt 0 ]] && return 1
|
|
return 0
|
|
}
|
|
|
|
do_restore() {
|
|
if [[ ! -f "$RESTORE_ARCHIVE" ]]; then
|
|
log_error "Archive not found: ${RESTORE_ARCHIVE}"; exit 1
|
|
fi
|
|
|
|
local basename_archive volume_name
|
|
basename_archive=$(basename "$RESTORE_ARCHIVE")
|
|
volume_name=$(echo "$basename_archive" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
|
|
|
|
if [[ -z "$volume_name" || "$volume_name" == "$basename_archive" ]]; then
|
|
log_error "Cannot determine volume name from archive: ${basename_archive}"
|
|
log_error "Expected format: volumename_YYYYMMDD_HHMMSS.tar.gz"; exit 1
|
|
fi
|
|
|
|
log_step "Restoring volume: ${volume_name} from ${RESTORE_ARCHIVE}"
|
|
|
|
if ! docker volume inspect "$volume_name" &>/dev/null; then
|
|
log_info "Creating volume: ${volume_name}"
|
|
docker volume create "$volume_name" >/dev/null
|
|
else
|
|
log_warn "Volume '${volume_name}' already exists — contents will be overwritten"
|
|
fi
|
|
|
|
local archive_abs archive_dir archive_file
|
|
archive_abs=$(realpath "$RESTORE_ARCHIVE")
|
|
archive_dir=$(dirname "$archive_abs")
|
|
archive_file=$(basename "$archive_abs")
|
|
|
|
if docker run --rm \
|
|
-v "${volume_name}:/target" \
|
|
-v "${archive_dir}:/backup:ro" \
|
|
"$ALPINE_IMAGE" \
|
|
sh -c "rm -rf /target/* /target/..?* /target/.[!.]* 2>/dev/null; tar xzf /backup/${archive_file} -C /target" 2>/dev/null; then
|
|
log_info "Volume '${volume_name}' restored successfully"
|
|
else
|
|
log_error "Failed to restore volume: ${volume_name}"; exit 1
|
|
fi
|
|
}
|
|
|
|
do_list() {
|
|
if [[ ! -d "$BACKUP_DIR" ]]; then
|
|
log_info "No backups found (directory does not exist: ${BACKUP_DIR})"; return 0
|
|
fi
|
|
|
|
local count=0
|
|
log_step "Available backups in ${BACKUP_DIR}:"
|
|
printf "\n %-40s %-10s %s\n" "ARCHIVE" "SIZE" "VOLUME"
|
|
printf " %-40s %-10s %s\n" "-------" "----" "------"
|
|
|
|
for archive in "$BACKUP_DIR"/*.tar.gz; do
|
|
[[ -f "$archive" ]] || continue
|
|
count=$((count + 1))
|
|
local name size vol_name
|
|
name=$(basename "$archive")
|
|
size=$(du -h "$archive" | cut -f1)
|
|
vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
|
|
printf " %-40s %-10s %s\n" "$name" "$size" "$vol_name"
|
|
done
|
|
|
|
echo ""
|
|
if [[ $count -eq 0 ]]; then
|
|
log_info "No backup archives found in ${BACKUP_DIR}"
|
|
else
|
|
log_info "${count} backup(s) found"
|
|
fi
|
|
}
|
|
|
|
apply_retention() {
|
|
log_step "Applying retention policy (keep last ${RETAIN} per volume)"
|
|
|
|
local vol_names=()
|
|
for archive in "$BACKUP_DIR"/*.tar.gz; do
|
|
[[ -f "$archive" ]] || continue
|
|
local name vol_name
|
|
name=$(basename "$archive")
|
|
vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
|
|
vol_names+=("$vol_name")
|
|
done
|
|
|
|
local unique_vols
|
|
unique_vols=$(printf '%s\n' "${vol_names[@]}" 2>/dev/null | sort -u)
|
|
|
|
while IFS= read -r vol; do
|
|
[[ -z "$vol" ]] && continue
|
|
local old_archives=()
|
|
while IFS= read -r f; do
|
|
[[ -n "$f" ]] && old_archives+=("$f")
|
|
done < <(ls -1t "$BACKUP_DIR"/${vol}_[0-9]*_[0-9]*.tar.gz 2>/dev/null | tail -n +$((RETAIN + 1)))
|
|
for old_archive in "${old_archives[@]}"; do
|
|
log_info "Removing old backup: $(basename "$old_archive")"
|
|
rm -f "$old_archive"
|
|
done
|
|
done <<< "$unique_vols"
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
check_dependencies
|
|
case "$MODE" in
|
|
backup) do_backup ;;
|
|
restore) do_restore ;;
|
|
list) do_list ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|