#!/bin/bash # Colors for better output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Emojis for visual feedback INFO="ℹ️" SUCCESS="✅" WARNING="⚠️" ERROR="❌" BACKUP="📦" RESTORE="🔄" INSTALL="🔧" MODE="$1" TARGET_DIR="$2" shift 2 VOLUMES=("$@") BUSYBOX_IMAGE="busybox" INSTALL_DIR="/usr/local/bin" print_usage() { echo -e "${BLUE}Docker Volume Backup and Restore Utility by @vksh (https://vksh.fyi)${NC}" echo -e "${BLUE}=======================================${NC}\n" echo -e "${INFO} Usage:" echo -e " $0 backup [volume2 ...]" echo -e " $0 restore [more...]" echo -e " $0 install" echo echo -e "${INFO} Installation Options:" echo -e " install - Interactive installation that will:" echo -e " - Ask for installation type (system-wide or user-only)" echo -e " - System-wide: Install to /usr/local/bin (requires sudo)" echo -e " - User-only: Install to ~/.local/bin and update PATH" echo echo -e "${INFO} Examples:" echo -e " $0 backup ./backups solr_data postgres_data" echo -e " $0 restore ./backups solr_data postgres_data" echo -e " $0 restore ./backups my_pg=postgres_data my_solr=solr_data" echo -e " $0 install" echo exit 1 } # Function to install the script install_script() { echo -e "${INSTALL} Installation Options:" echo -e "1. System-wide installation (requires sudo)" echo -e "2. User-only installation" read -p "Choose installation type (1/2): " install_type case $install_type in 1) INSTALL_DIR="/usr/local/bin" echo -e "${INSTALL} Installing dvbackup to ${BLUE}${INSTALL_DIR}${NC} (requires sudo)" # Check if script is already installed if [[ -f "${INSTALL_DIR}/dvbackup" ]]; then echo -e "${WARNING} dvbackup is already installed at ${INSTALL_DIR}/dvbackup" read -p "Do you want to overwrite it? (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo -e "${INFO} Installation cancelled" exit 0 fi fi # Get the absolute path of the current script SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" # Copy the script to the install directory if sudo cp "$SCRIPT_PATH" "${INSTALL_DIR}/dvbackup" && sudo chmod +x "${INSTALL_DIR}/dvbackup"; then echo -e "${SUCCESS} dvbackup has been installed successfully!" echo -e "${INFO} You can now use it from anywhere by typing: ${GREEN}dvbackup${NC}" else echo -e "${ERROR} Failed to install dvbackup" exit 1 fi ;; 2) INSTALL_DIR="$HOME/.local/bin" echo -e "${INSTALL} Installing dvbackup to ${BLUE}${INSTALL_DIR}${NC}" # Create directory if it doesn't exist mkdir -p "$INSTALL_DIR" # Check if script is already installed if [[ -f "${INSTALL_DIR}/dvbackup" ]]; then echo -e "${WARNING} dvbackup is already installed at ${INSTALL_DIR}/dvbackup" read -p "Do you want to overwrite it? (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo -e "${INFO} Installation cancelled" exit 0 fi fi # Get the absolute path of the current script SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" # Copy the script to the install directory if cp "$SCRIPT_PATH" "${INSTALL_DIR}/dvbackup" && chmod +x "${INSTALL_DIR}/dvbackup"; then # Add to PATH if not already there if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then echo -e "${INFO} Adding ${INSTALL_DIR} to your PATH" if [[ -f "$HOME/.bashrc" ]]; then echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$HOME/.bashrc" echo -e "${INFO} Added to ~/.bashrc. Run 'source ~/.bashrc' to update your current shell" elif [[ -f "$HOME/.zshrc" ]]; then echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$HOME/.zshrc" echo -e "${INFO} Added to ~/.zshrc. Run 'source ~/.zshrc' to update your current shell" fi fi echo -e "${SUCCESS} dvbackup has been installed successfully!" echo -e "${INFO} You can now use it from anywhere by typing: ${GREEN}dvbackup${NC}" else echo -e "${ERROR} Failed to install dvbackup" exit 1 fi ;; *) echo -e "${ERROR} Invalid option. Please choose 1 or 2." exit 1 ;; esac } if [[ "$MODE" == "install" ]]; then install_script exit 0 fi if [[ -z "$MODE" || -z "$TARGET_DIR" || "${#VOLUMES[@]}" -eq 0 ]]; then print_usage fi echo -e "${BLUE}Docker Volume Backup and Restore Utility${NC}" echo -e "${BLUE}=======================================${NC}\n" set -e # Function to check if a volume exists volume_exists() { docker volume inspect "$1" >/dev/null 2>&1 } # Function to validate backup directory validate_backup_dir() { if [[ ! -d "$1" ]]; then echo -e "${ERROR} Backup directory does not exist: $1" exit 1 fi } # Function to validate volume names validate_volume_name() { if [[ ! "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ ]]; then echo -e "${ERROR} Invalid volume name: $1" echo -e "${INFO} Volume names must start with a letter or number and can only contain letters, numbers, underscores, dots, and hyphens." exit 1 fi } # Function to check if a container is using the volume is_volume_in_use() { local volume_name="$1" local containers=() while IFS= read -r container; do if docker inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}' "$container" | grep -q "$volume_name"; then containers+=("$container") fi done < <(docker ps --format '{{.Names}}') if [[ ${#containers[@]} -gt 0 ]]; then echo "${containers[@]}" return 0 fi return 1 } # Resolve absolute path safely for macOS, Linux, and Git Bash if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then # Git Bash on Windows ABS_BACKUP_DIR="$(cd "$TARGET_DIR" && pwd -W)" else # macOS and Linux ABS_BACKUP_DIR="$(cd "$TARGET_DIR" && pwd)" fi mkdir -p "$ABS_BACKUP_DIR" if [[ "$MODE" == "backup" ]]; then for VOLUME in "${VOLUMES[@]}"; do validate_volume_name "$VOLUME" if ! volume_exists "$VOLUME"; then echo -e "${ERROR} Volume does not exist: $VOLUME" exit 1 fi ARCHIVE="${ABS_BACKUP_DIR}/${VOLUME}.tar.gz" echo -e "${BACKUP} Backing up volume: ${BLUE}$VOLUME${NC} → ${GREEN}$ARCHIVE${NC}" # Check if volume is in use containers=$(is_volume_in_use "$VOLUME") if [[ $? -eq 0 ]]; then echo -e "${WARNING} Volume is in use by containers:" for container in $containers; do echo -e " - ${YELLOW}$container${NC}" done if [[ "$FORCE" != "true" ]]; then echo -e "${INFO} Using read-only mount for safety" MOUNT_OPTION=":ro" else echo -e "${WARNING} Force mode enabled - using regular mount" MOUNT_OPTION="" fi else MOUNT_OPTION="" fi docker run --rm \ -v "${VOLUME}:/source${MOUNT_OPTION}" \ -v "$ABS_BACKUP_DIR:/backup" \ "$BUSYBOX_IMAGE" \ tar czf "/backup/${VOLUME}.tar.gz" -C /source . echo -e "${SUCCESS} Backup completed for volume: $VOLUME" done elif [[ "$MODE" == "restore" ]]; then for VOLUME_ARG in "${VOLUMES[@]}"; do if [[ "$VOLUME_ARG" == *"="* ]]; then NEW_VOLUME="${VOLUME_ARG%%=*}" ARCHIVE_NAME="${VOLUME_ARG##*=}" else NEW_VOLUME="$VOLUME_ARG" ARCHIVE_NAME="$VOLUME_ARG" fi validate_volume_name "$NEW_VOLUME" ARCHIVE="${ABS_BACKUP_DIR}/${ARCHIVE_NAME}.tar.gz" if [[ ! -f "$ARCHIVE" ]]; then echo -e "${ERROR} Archive not found: $ARCHIVE" exit 1 fi if volume_exists "$NEW_VOLUME"; then echo -e "${WARNING} Volume already exists: $NEW_VOLUME" echo -e "${INFO} Please choose a different name or remove the existing volume first." exit 1 fi echo -e "${RESTORE} Restoring: ${BLUE}${ARCHIVE_NAME}.tar.gz${NC} → volume '${GREEN}${NEW_VOLUME}${NC}'" docker volume create "$NEW_VOLUME" >/dev/null docker run --rm \ -v "${NEW_VOLUME}:/restore" \ -v "$ABS_BACKUP_DIR:/backup" \ "$BUSYBOX_IMAGE" \ tar xzf "/backup/${ARCHIVE_NAME}.tar.gz" -C /restore echo -e "${SUCCESS} Restore completed for volume: $NEW_VOLUME" done else echo -e "${ERROR} Invalid mode: $MODE. Use 'backup' or 'restore'." print_usage fi echo -e "\n${SUCCESS} Operation completed successfully!"