diff --git a/ansible/playbooks/deployer_apt.yml b/ansible/playbooks/deployer_apt.yml index 7f41de5..7f99ecd 100644 --- a/ansible/playbooks/deployer_apt.yml +++ b/ansible/playbooks/deployer_apt.yml @@ -72,3 +72,10 @@ name: sysstat state: started enabled: yes + + - name: Install lftp + apt: + name: lftp + state: present + update_cache: yes + cache_valid_time: 3600 diff --git a/ansible/playbooks/deployer_setup.yml b/ansible/playbooks/deployer_setup.yml index e844de5..d0047df 100644 --- a/ansible/playbooks/deployer_setup.yml +++ b/ansible/playbooks/deployer_setup.yml @@ -45,6 +45,30 @@ dest: /usr/local/bin/ mode: 0755 + - name: Install the beamup-sync scripts [1/3] + copy: + src: ../../scripts/beamup-sync/beamup-sync.sh + dest: /usr/local/bin/beamup-sync + mode: 0755 + + - name: Install the beamup-sync scripts [2/4] + copy: + src: ../../scripts/beamup-sync/beamup-common.sh + dest: /usr/local/lib/beamup/beamup-common + mode: 0755 + + - name: Install the beamup-sync scripts [3/4] + copy: + src: ../../scripts/beamup-sync/beamup-backup.sh + dest: /usr/local/lib/beamup/beamup-backup + mode: 0755 + + - name: Install the beamup-sync scripts [4/4] + copy: + src: ../../scripts/beamup-sync/beamup-restore.sh + dest: /usr/local/lib/beamup/beamup-restore + mode: 0755 + - name: template | Generate bash script beamup-delete-addon template: src: ../templates/beamup-delete-addon.j2 diff --git a/scripts/beamup-sync/beamup-backup.sh b/scripts/beamup-sync/beamup-backup.sh new file mode 100644 index 0000000..869ad05 --- /dev/null +++ b/scripts/beamup-sync/beamup-backup.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +set -euo pipefail + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common functions +if [ -f "${SCRIPT_DIR}/beamup-common.sh" ]; then + source "${SCRIPT_DIR}/beamup-common.sh" +elif [ -f "/usr/local/lib/beamup/beamup-common" ]; then + source "/usr/local/lib/beamup/beamup-common" +else + echo "Error: beamup-common.sh not found" >&2 + exit 1 +fi + +# Configuration +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="${BEAMUP_BASE}/backup-${TIMESTAMP}" +FINAL_ARCHIVE="${BACKUP_DIR}/beamup-backup-${TIMESTAMP}.tar.xz" +CHECKSUM_FILE="checksums.sha256" + +# Parse arguments +show_usage() { + cat << EOF +Usage: beamup-backup [OPTIONS] + +Create a backup of Dokku applications and system files. + +OPTIONS: + -v, --verbose Enable verbose output + -h, --help Show this help message + +EXAMPLES: + beamup-backup + beamup-backup --verbose +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_usage + exit 1 + ;; + esac +done + +# Check root +check_root + +# Initialize logging +LOG_FILE="${LOG_DIR}/backup-${TIMESTAMP}.log" +init_logging + +log_info "Starting Beamup Backup Process" +if [ "$VERBOSE" = true ]; then + log_info "Verbose mode enabled" +fi +log_verbose "Timestamp: ${TIMESTAMP}" +log_verbose "Log file: ${LOG_FILE}" + +# Cleanup on error +cleanup_on_error() { + if [ -d "$BACKUP_DIR" ]; then + log_error "Backup failed, cleaning up temporary directory..." + rm -rf "$BACKUP_DIR" + fi +} + +trap cleanup_on_error ERR + +# Create backup directories +log_info "Creating backup directory..." +mkdir -p "$BEAMUP_BASE" || { + log_error "Failed to create beamup base directory: ${BEAMUP_BASE}" + exit 1 +} + +mkdir -p "$BACKUP_DIR" || { + log_error "Failed to create backup directory: ${BACKUP_DIR}" + exit 1 +} + +log_verbose "Backup directory created: ${BACKUP_DIR}" + +# Create temporary working directory for files before archiving +TEMP_WORK_DIR="${BACKUP_DIR}/work" +mkdir -p "$TEMP_WORK_DIR" + +# Backup Dokku authorized_keys +log_info "Backing up Dokku authorized_keys..." +if [ -f "/home/dokku/.ssh/authorized_keys" ]; then + mkdir -p "${TEMP_WORK_DIR}/dokku-ssh" + if [ "$VERBOSE" = true ]; then + cp -v "/home/dokku/.ssh/authorized_keys" "${TEMP_WORK_DIR}/dokku-ssh/" 2>&1 | tee -a "$LOG_FILE" || { + log_error "Failed to backup authorized_keys" + exit 1 + } + else + cp "/home/dokku/.ssh/authorized_keys" "${TEMP_WORK_DIR}/dokku-ssh/" >> "$LOG_FILE" 2>&1 || { + log_error "Failed to backup authorized_keys" + exit 1 + } + fi + log_info "✓ Dokku authorized_keys backed up" +else + log_warn "Dokku authorized_keys not found" +fi + +# Backup Dokku app directories +log_info "Backing up Dokku app directories..." +if [ -d "/home/dokku" ]; then + cd /home + if [ "$VERBOSE" = true ]; then + tar -cJv --exclude='**/cache/*' -f "${TEMP_WORK_DIR}/dokku-apps.tar.xz" dokku 2>&1 | tee -a "$LOG_FILE" || { + log_error "Failed to backup Dokku app directories" + exit 1 + } + else + tar -cJ --exclude='**/cache/*' -f "${TEMP_WORK_DIR}/dokku-apps.tar.xz" dokku >> "$LOG_FILE" 2>&1 || { + log_error "Failed to backup Dokku app directories" + exit 1 + } + fi + log_info "✓ Dokku app directories backed up" +else + log_warn "Dokku directory not found" +fi + +# Backup SSH host keys +log_info "Backing up SSH host keys..." +if [ -d "/etc/ssh" ]; then + cd /etc/ssh + if [ "$VERBOSE" = true ]; then + tar -cJv -f "${TEMP_WORK_DIR}/ssh-host-keys.tar.xz" ssh_host_* 2>&1 | tee -a "$LOG_FILE" || { + log_error "Failed to backup SSH host keys" + exit 1 + } + else + tar -cJ -f "${TEMP_WORK_DIR}/ssh-host-keys.tar.xz" ssh_host_* >> "$LOG_FILE" 2>&1 || { + log_error "Failed to backup SSH host keys" + exit 1 + } + fi + log_info "✓ SSH host keys backed up" +else + log_warn "SSH directory not found" +fi + +# Backup cron jobs +log_info "Backing up cron jobs..." +CRON_FILES=() +[ -f "/etc/cron.daily/disk-usage-by-containers" ] && CRON_FILES+=("/etc/cron.daily/disk-usage-by-containers") +[ -f "/etc/cron.daily/df" ] && CRON_FILES+=("/etc/cron.daily/df") + +if [ ${#CRON_FILES[@]} -gt 0 ]; then + if [ "$VERBOSE" = true ]; then + tar -cJv -f "${TEMP_WORK_DIR}/cron-jobs.tar.xz" "${CRON_FILES[@]}" 2>&1 | tee -a "$LOG_FILE" || { + log_error "Failed to backup cron jobs" + exit 1 + } + else + tar -cJ -f "${TEMP_WORK_DIR}/cron-jobs.tar.xz" "${CRON_FILES[@]}" >> "$LOG_FILE" 2>&1 || { + log_error "Failed to backup cron jobs" + exit 1 + } + fi + log_info "✓ Cron jobs backed up" +else + log_warn "No cron job files found" +fi + +# Generate checksums for all backed up files (excluding the checksum file itself) +log_info "Generating checksums..." +cd "$TEMP_WORK_DIR" +find . -type f ! -name "$CHECKSUM_FILE" -exec sha256sum {} \; > "$CHECKSUM_FILE" || { + log_error "Failed to generate checksums" + exit 1 +} +log_info "✓ Checksums generated" +if [ "$VERBOSE" = true ]; then + log_verbose "Checksum contents:" + cat "$CHECKSUM_FILE" | tee -a "$LOG_FILE" +fi + +# Create final archive +log_info "Creating final archive..." +cd "$TEMP_WORK_DIR" +if [ "$VERBOSE" = true ]; then + tar -cJv -f "$FINAL_ARCHIVE" . 2>&1 | tee -a "$LOG_FILE" || { + log_error "Failed to create final archive" + exit 1 + } +else + tar -cJ -f "$FINAL_ARCHIVE" . >> "$LOG_FILE" 2>&1 || { + log_error "Failed to create final archive" + exit 1 + } +fi +log_info "✓ Final archive created" + +# Generate checksum for final archive +log_info "Generating archive checksum..." +FINAL_CHECKSUM="${FINAL_ARCHIVE}.sha256" +cd "$BACKUP_DIR" +sha256sum "$(basename "$FINAL_ARCHIVE")" > "$FINAL_CHECKSUM" || { + log_error "Failed to generate final archive checksum" + exit 1 +} +log_info "✓ Archive checksum generated" +if [ "$VERBOSE" = true ]; then + log_verbose "Final archive checksum:" + cat "$FINAL_CHECKSUM" | tee -a "$LOG_FILE" +fi + +# Clean up temporary work directory +log_verbose "Cleaning up temporary work directory..." +rm -rf "$TEMP_WORK_DIR" +log_verbose "Temporary files cleaned up" + +# Summary +log_info "==========================================" +log_info "Backup completed successfully!" +log_info "==========================================" +log_info "Archive: ${FINAL_ARCHIVE}" +log_info "Size: $(du -h "$FINAL_ARCHIVE" | cut -f1)" +log_verbose "Checksum: ${FINAL_CHECKSUM}" +log_verbose "Log: ${LOG_FILE}" +log_info "==========================================" \ No newline at end of file diff --git a/scripts/beamup-sync/beamup-common.sh b/scripts/beamup-sync/beamup-common.sh new file mode 100644 index 0000000..baacfab --- /dev/null +++ b/scripts/beamup-sync/beamup-common.sh @@ -0,0 +1,228 @@ +#!/bin/bash + +# beamup-common.sh - Shared functions and variables for beamup scripts + +# Configuration +BEAMUP_BASE="/tmp/beamup-backup" +LOG_DIR="/var/log/beamup-backup" +CONFIG_DIR="/etc/beamup" +CONFIG_FILE="${CONFIG_DIR}/sync.conf" +LOCK_FILE="/var/run/beamup-sync.lock" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Global verbose flag +VERBOSE=false + +# Logging functions +log_info() { + local msg="[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" + echo -e "${GREEN}${msg}${NC}" + if [ -n "${LOG_FILE:-}" ]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +log_verbose() { + local msg="[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') - $1" + if [ "$VERBOSE" = true ]; then + echo -e "${NC}${msg}${NC}" + fi + if [ -n "${LOG_FILE:-}" ]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +log_error() { + local msg="[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" + echo -e "${RED}${msg}${NC}" >&2 + if [ -n "${LOG_FILE:-}" ]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +log_warn() { + local msg="[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $1" + echo -e "${YELLOW}${msg}${NC}" + if [ -n "${LOG_FILE:-}" ]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +log_success() { + local msg="[SUCCESS] $(date '+%Y-%m-%d %H:%M:%S') - $1" + echo -e "${GREEN}${msg}${NC}" + if [ -n "${LOG_FILE:-}" ]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +# Check if running as root +check_root() { + if [ "$EUID" -ne 0 ]; then + log_error "This script must be run as root" + exit 1 + fi +} + +# Create lock file +acquire_lock() { + if [ -f "$LOCK_FILE" ]; then + local pid=$(cat "$LOCK_FILE") + if ps -p "$pid" > /dev/null 2>&1; then + log_error "Another beamup process is already running (PID: $pid)" + exit 1 + else + log_warn "Stale lock file found, removing..." + rm -f "$LOCK_FILE" + fi + fi + echo $$ > "$LOCK_FILE" +} + +# Release lock file +release_lock() { + rm -f "$LOCK_FILE" +} + +# Parse INI config file +parse_config() { + local config_file="$1" + local section="" + + if [ ! -f "$config_file" ]; then + log_error "Configuration file not found: $config_file" + return 1 + fi + + while IFS='=' read -r key value; do + # Skip comments and empty lines + [[ "$key" =~ ^[[:space:]]*# ]] && continue + [[ -z "$key" ]] && continue + + # Detect section + if [[ "$key" =~ ^\[(.*)\] ]]; then + section="${BASH_REMATCH[1]}" + continue + fi + + # Parse key-value pairs + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + + # Create variable name: SECTION_KEY + if [ -n "$section" ]; then + declare -g "${section}_${key}=${value}" + fi + done < "$config_file" +} + +# Initialize logging +init_logging() { + mkdir -p "$LOG_DIR" || { + echo "Failed to create log directory: ${LOG_DIR}" >&2 + exit 1 + } + + # Set up log rotation config if it doesn't exist + local logrotate_conf="/etc/logrotate.d/beamup-backup" + if [ ! -f "$logrotate_conf" ]; then + cat > "$logrotate_conf" << 'EOF' +/var/log/beamup-backup/*.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty + create 0640 root root + sharedscripts +} +EOF + log_verbose "Created logrotate configuration at ${logrotate_conf}" + fi +} + +# List all backups in beamup directory +list_local_backups() { + if [ ! -d "$BEAMUP_BASE" ]; then + return 0 + fi + + find "$BEAMUP_BASE" -maxdepth 2 -name "*.tar.xz" -type f 2>/dev/null | sort -r +} + +# Get latest local backup +get_latest_backup() { + list_local_backups | head -n 1 +} + +# Extract timestamp from backup filename +extract_timestamp() { + local filename="$1" + echo "$filename" | grep -oP '\d{8}_\d{6}' || echo "" +} + +# Verify backup integrity +verify_backup() { + local backup_file="$1" + local checksum_file="${backup_file}.sha256" + + if [ ! -f "$checksum_file" ]; then + log_error "Checksum file not found: $checksum_file" + return 1 + fi + + log_info "Verifying backup integrity..." + cd "$(dirname "$backup_file")" + if sha256sum -c "$(basename "$checksum_file")" > /dev/null 2>&1; then + log_success "Backup integrity verified" + return 0 + else + log_error "Backup integrity check failed" + return 1 + fi +} + +# Confirm action with user +confirm_action() { + local prompt="$1" + local default="${2:-n}" + + if [ "$default" = "y" ]; then + prompt="$prompt [Y/n]: " + else + prompt="$prompt [y/N]: " + fi + + read -r -p "$prompt" response + response=${response:-$default} + + case "$response" in + [yY][eE][sS]|[yY]) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# Format bytes to human readable +format_bytes() { + local bytes=$1 + if [ "$bytes" -lt 1024 ]; then + echo "${bytes}B" + elif [ "$bytes" -lt 1048576 ]; then + echo "$(( bytes / 1024 ))KB" + elif [ "$bytes" -lt 1073741824 ]; then + echo "$(( bytes / 1048576 ))MB" + else + echo "$(( bytes / 1073741824 ))GB" + fi +} \ No newline at end of file diff --git a/scripts/beamup-sync/beamup-restore.sh b/scripts/beamup-sync/beamup-restore.sh new file mode 100644 index 0000000..fb9e85d --- /dev/null +++ b/scripts/beamup-sync/beamup-restore.sh @@ -0,0 +1,264 @@ +#!/bin/bash + +set -euo pipefail + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common functions +if [ -f "${SCRIPT_DIR}/beamup-common.sh" ]; then + source "${SCRIPT_DIR}/beamup-common.sh" +elif [ -f "/usr/local/lib/beamup/beamup-common" ]; then + source "/usr/local/lib/beamup/beamup-common" +else + echo "Error: beamup-common.sh not found" >&2 + exit 1 +fi + +# Parse arguments +BACKUP_FILE="" +FORCE=false + +show_usage() { + cat << EOF +Usage: beamup-restore [OPTIONS] [BACKUP_FILE] + +Restore from a Dokku backup archive. + +OPTIONS: + -f, --force Skip confirmation prompts + -v, --verbose Enable verbose output + -h, --help Show this help message + +ARGUMENTS: + BACKUP_FILE Path to backup archive (optional, will use latest if not specified) + +EXAMPLES: + beamup-restore + beamup-restore /tmp/beamup-backup/backup-20231028_143022/beamup-backup-20231028_143022.tar.xz + beamup-restore --force --verbose +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + -f|--force) + FORCE=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + BACKUP_FILE="$1" + shift + ;; + esac +done + +# Check root +check_root + +# Initialize logging +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOG_FILE="${LOG_DIR}/restore-${TIMESTAMP}.log" +init_logging + +log_info "Starting Beamup Restore Process" + +# Determine backup file to restore +if [ -z "$BACKUP_FILE" ]; then + log_info "No backup file specified, searching for latest backup..." + BACKUP_FILE=$(get_latest_backup) + + if [ -z "$BACKUP_FILE" ]; then + log_error "No backups found in ${BEAMUP_BASE}" + exit 1 + fi + + log_info "Found latest backup: $BACKUP_FILE" +fi + +# Verify backup file exists +if [ ! -f "$BACKUP_FILE" ]; then + log_error "Backup file not found: $BACKUP_FILE" + exit 1 +fi + +# Verify backup integrity +verify_backup "$BACKUP_FILE" || { + log_error "Backup integrity verification failed" + exit 1 +} + +# Show warning and get confirmation +if [ "$FORCE" != true ]; then + echo "" + echo -e "${RED}╔════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${RED}║ ⚠️ WARNING ⚠️ ║${NC}" + echo -e "${RED}╠════════════════════════════════════════════════════════════════╣${NC}" + echo -e "${RED}║ This operation will OVERWRITE existing system files: ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ • Dokku authorized_keys ║${NC}" + echo -e "${RED}║ • Dokku app directories ║${NC}" + echo -e "${RED}║ • SSH host keys ║${NC}" + echo -e "${RED}║ • Cron jobs ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ This action is IRREVERSIBLE and may cause service downtime. ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ Backup file: $(basename "$BACKUP_FILE")${NC}" + echo -e "${RED}╚════════════════════════════════════════════════════════════════╝${NC}" + echo "" + + if ! confirm_action "Are you absolutely sure you want to proceed?"; then + log_info "Restore cancelled by user" + exit 0 + fi + + echo "" + if ! confirm_action "Type 'yes' to confirm (not just 'y')"; then + log_info "Restore cancelled by user" + exit 0 + fi +fi + +# Create temporary extraction directory +TEMP_EXTRACT_DIR=$(mktemp -d -p /tmp beamup-restore.XXXXXX) +cleanup_temp() { + if [ -d "$TEMP_EXTRACT_DIR" ]; then + log_verbose "Cleaning up temporary extraction directory..." + rm -rf "$TEMP_EXTRACT_DIR" + fi +} +trap cleanup_temp EXIT + +# Extract backup archive +log_info "Extracting backup archive..." +if [ "$VERBOSE" = true ]; then + tar -xJvf "$BACKUP_FILE" -C "$TEMP_EXTRACT_DIR" 2>&1 | tee -a "$LOG_FILE" +else + tar -xJf "$BACKUP_FILE" -C "$TEMP_EXTRACT_DIR" >> "$LOG_FILE" 2>&1 +fi +log_info "✓ Backup extracted" + +# Verify checksums of extracted files +if [ -f "${TEMP_EXTRACT_DIR}/checksums.sha256" ]; then + log_info "Verifying extracted files..." + cd "$TEMP_EXTRACT_DIR" + if sha256sum -c checksums.sha256 >> "$LOG_FILE" 2>&1; then + log_info "✓ All files verified" + else + log_error "File verification failed" + exit 1 + fi +fi + +# Restore Dokku authorized_keys +if [ -f "${TEMP_EXTRACT_DIR}/dokku-ssh/authorized_keys" ]; then + log_info "Restoring Dokku authorized_keys..." + mkdir -p /home/dokku/.ssh + cp "${TEMP_EXTRACT_DIR}/dokku-ssh/authorized_keys" /home/dokku/.ssh/authorized_keys + chown dokku:dokku /home/dokku/.ssh/authorized_keys + chmod 600 /home/dokku/.ssh/authorized_keys + log_info "✓ Dokku authorized_keys restored" +else + log_warn "Dokku authorized_keys not found in backup" +fi + +# Restore Dokku apps +if [ -f "${TEMP_EXTRACT_DIR}/dokku-apps.tar.xz" ]; then + log_info "Restoring Dokku app directories..." + # Extract once to a temporary location + DOKKU_RESTORE_TEMP=$(mktemp -d -p ${TEMP_EXTRACT_DIR} dokku.XXXXXX) + cd "$DOKKU_RESTORE_TEMP" + if [ "$VERBOSE" = true ]; then + tar -xJvf "${TEMP_EXTRACT_DIR}/dokku-apps.tar.xz" 2>&1 | tee -a "$LOG_FILE" + else + tar -xJf "${TEMP_EXTRACT_DIR}/dokku-apps.tar.xz" >> "$LOG_FILE" 2>&1 + fi + + # Create each Dokku app before restoring data + log_info "Creating Dokku apps..." + if [ -d "${DOKKU_RESTORE_TEMP}/dokku" ]; then + for app_dir in "${DOKKU_RESTORE_TEMP}/dokku"/*; do + if [ -d "$app_dir" ]; then + app_name=$(basename "$app_dir") + # Skip special directories + if [[ "$app_name" != "VHOST" && "$app_name" != "ENV" && "$app_name" != "HOSTNAME" && "$app_name" != ".ssh" ]]; then + if dokku apps:list | grep -q "^${app_name}$"; then + log_verbose "App already exists: $app_name" + else + log_info "Creating app: $app_name" + dokku apps:create "$app_name" >> "$LOG_FILE" 2>&1 || { + log_warn "Failed to create app: $app_name" + } + fi + fi + fi + done + log_info "✓ Dokku apps created" + fi + + # Now copy the app directories (excluding .ssh which is handled separately) + log_info "Copying app data..." + if [ -d "${DOKKU_RESTORE_TEMP}/dokku" ]; then + rsync -a --exclude='.ssh' "${DOKKU_RESTORE_TEMP}/dokku/" /home/dokku/ + fi + + # Set correct ownership for all Dokku files + log_info "Setting Dokku file permissions..." + chown -R dokku:dokku /home/dokku + log_info "✓ Dokku app directories restored" + + # Rebuild all Dokku apps in parallel (2 at a time) + log_info "Rebuilding all Dokku apps in parallel..." + dokku ps:rebuild --all --parallel 5 >> "$LOG_FILE" 2>&1 + log_info "✓ Dokku apps rebuild done" + + # Cleanup temporary directory + rm -rf "$DOKKU_RESTORE_TEMP" +else + log_warn "Dokku apps archive not found in backup" +fi + +# Restore SSH host keys +if [ -f "${TEMP_EXTRACT_DIR}/ssh-host-keys.tar.xz" ]; then + log_info "Restoring SSH host keys..." + cd /etc/ssh + if [ "$VERBOSE" = true ]; then + tar -xJvf "${TEMP_EXTRACT_DIR}/ssh-host-keys.tar.xz" 2>&1 | tee -a "$LOG_FILE" + else + tar -xJf "${TEMP_EXTRACT_DIR}/ssh-host-keys.tar.xz" >> "$LOG_FILE" 2>&1 + fi + log_info "✓ SSH host keys restored" + log_warn "SSH service restart may be required" +else + log_warn "SSH host keys archive not found in backup" +fi + +# Restore cron jobs +if [ -f "${TEMP_EXTRACT_DIR}/cron-jobs.tar.xz" ]; then + log_info "Restoring cron jobs..." + cd / + if [ "$VERBOSE" = true ]; then + tar -xJvf "${TEMP_EXTRACT_DIR}/cron-jobs.tar.xz" 2>&1 | tee -a "$LOG_FILE" + else + tar -xJf "${TEMP_EXTRACT_DIR}/cron-jobs.tar.xz" >> "$LOG_FILE" 2>&1 + fi + log_info "✓ Cron jobs restored" +else + log_warn "Cron jobs archive not found in backup" +fi + +# Summary +log_success "==========================================" +log_success "Restore completed successfully!" +log_success "==========================================" +log_info "Restored from: $BACKUP_FILE" +log_info "Log file: ${LOG_FILE}" +log_success "==========================================" \ No newline at end of file diff --git a/scripts/beamup-sync/beamup-sync.sh b/scripts/beamup-sync/beamup-sync.sh new file mode 100644 index 0000000..40a2c76 --- /dev/null +++ b/scripts/beamup-sync/beamup-sync.sh @@ -0,0 +1,1089 @@ +#!/bin/bash + +set -euo pipefail + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common functions and set helper script paths +if [ -f "${SCRIPT_DIR}/beamup-common.sh" ]; then + source "${SCRIPT_DIR}/beamup-common.sh" + BACKUP_SCRIPT="${SCRIPT_DIR}/beamup-backup.sh" + RESTORE_SCRIPT="${SCRIPT_DIR}/beamup-restore.sh" +elif [ -f "/usr/local/lib/beamup/beamup-common" ]; then + source "/usr/local/lib/beamup/beamup-common" + BACKUP_SCRIPT="/usr/local/lib/beamup/beamup-backup" + RESTORE_SCRIPT="/usr/local/lib/beamup/beamup-restore" +else + echo "Error: Helper scripts not found" >&2 + exit 1 +fi + +# Parse command and arguments +COMMAND="${1:-}" +shift || true + +USE_FTP=false +USE_S3=false +USE_RSYNC=false +AUTO_RESTORE=false +FORCE_BACKUP=false +BACKUP_NAME="" + +show_usage() { + cat << EOF +Usage: beamup-sync [COMMAND] [OPTIONS] + +Sync backups to/from remote storage. + +COMMANDS: + backup Create a new local backup only (no remote push) + push Push local backups to remote storage + pull BACKUP_NAME Pull backup from remote storage + restore Restore latest local backup + list List backups on remote storage + config Configure remote storage settings + verify Verify configuration and connectivity + +OPTIONS: + --ftp Use FTP storage + --s3 Use S3 storage + --rsync Use rsync storage + -f, --force Force creation of new backup before push + -r, --restore Automatically restore after pull + -v, --verbose Enable verbose output + -h, --help Show this help message + +EXAMPLES: + beamup-sync backup # Create local backup only + beamup-sync backup --verbose # Create backup with verbose output + beamup-sync push # Push to all enabled remotes + beamup-sync push --force # Create new backup and push + beamup-sync push --ftp --s3 # Push only to FTP and S3 + beamup-sync push -f --verbose # Force backup with verbose output + beamup-sync pull backup-20231028 # Pull specific backup + beamup-sync pull --latest --s3 # Pull latest backup from S3 + beamup-sync pull --latest -r # Pull latest and restore + beamup-sync restore # Restore latest local backup + beamup-sync list --ftp # List backups on FTP + beamup-sync config # Interactive configuration +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + --ftp) + USE_FTP=true + shift + ;; + --s3) + USE_S3=true + shift + ;; + --rsync) + USE_RSYNC=true + shift + ;; + -f|--force) + FORCE_BACKUP=true + shift + ;; + -r|--restore) + AUTO_RESTORE=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + --latest) + BACKUP_NAME="latest" + shift + ;; + *) + if [ -z "$BACKUP_NAME" ]; then + BACKUP_NAME="$1" + fi + shift + ;; + esac +done + +# Check root +check_root + +# Initialize logging +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOG_FILE="${LOG_DIR}/sync-${TIMESTAMP}.log" +init_logging + +# Load configuration +if [ -f "$CONFIG_FILE" ]; then + parse_config "$CONFIG_FILE" +else + if [ "$COMMAND" != "config" ]; then + log_error "Configuration file not found: $CONFIG_FILE" + log_info "Run 'beamup-sync config' to create configuration" + exit 1 + fi +fi + +# Determine which remotes to use +determine_remotes() { + local remotes=() + + # If flags specified, use only those + if [ "$USE_FTP" = true ] || [ "$USE_S3" = true ] || [ "$USE_RSYNC" = true ]; then + [ "$USE_FTP" = true ] && remotes+=("ftp") + [ "$USE_S3" = true ] && remotes+=("s3") + [ "$USE_RSYNC" = true ] && remotes+=("rsync") + else + # Use enabled remotes from config + if [ -n "${general_enabled_remotes:-}" ]; then + IFS=',' read -ra remotes <<< "$general_enabled_remotes" + fi + fi + + echo "${remotes[@]}" +} + +# FTP functions +ftp_push() { + local backup_file="$1" + local backup_name=$(basename "$backup_file") + local checksum_file="${backup_file}.sha256" + + log_info "[FTP] Uploading $backup_name..." + + if [ "${ftp_enabled:-false}" != "true" ]; then + log_warn "[FTP] FTP is not enabled in configuration" + return 1 + fi + + # Upload using lftp for better reliability + if command -v lftp &> /dev/null; then + lftp -e "set ftp:ssl-allow no; put -O ${ftp_remote_path} ${backup_file}; put -O ${ftp_remote_path} ${checksum_file}; bye" \ + -u "${ftp_username},${ftp_password}" "${ftp_host}" >> "$LOG_FILE" 2>&1 && { + log_success "[FTP] Upload completed" + return 0 + } || { + log_error "[FTP] Upload failed" + return 1 + } + else + log_error "[FTP] lftp not installed" + return 1 + fi +} + +ftp_list() { + log_info "[FTP] Listing backups..." + + if command -v lftp &> /dev/null; then + lftp -e "set ftp:ssl-allow no; cd ${ftp_remote_path}; cls -1 *.tar.xz; bye" \ + -u "${ftp_username},${ftp_password}" "${ftp_host}" 2>> "$LOG_FILE" + fi +} + +ftp_pull() { + local backup_name="$1" + local dest_dir="$2" + + # If backup_name is 'latest', determine the latest backup file + if [ "$backup_name" = "latest" ]; then + log_info "[FTP] Determining latest backup..." + if command -v lftp &> /dev/null; then + latest_file=$(lftp -e "set ftp:ssl-allow no; cd ${ftp_remote_path}; cls -1 *.tar.xz; bye" \ + -u "${ftp_username},${ftp_password}" "${ftp_host}" 2>> "$LOG_FILE" | sort | tail -n 1) + if [ -z "$latest_file" ]; then + log_error "[FTP] No backups found on remote." + return 1 + fi + backup_name="$latest_file" + log_info "[FTP] Latest backup is: $backup_name" + else + log_error "[FTP] lftp not installed" + return 1 + fi + fi + + log_info "[FTP] Downloading $backup_name..." + + if command -v lftp &> /dev/null; then + mkdir -p "$dest_dir" + lftp -e "set ftp:ssl-allow no; get -O ${dest_dir} ${ftp_remote_path}/${backup_name}; get -O ${dest_dir} ${ftp_remote_path}/${backup_name}.sha256; bye" \ + -u "${ftp_username},${ftp_password}" "${ftp_host}" >> "$LOG_FILE" 2>&1 && { + log_success "[FTP] Download completed" + ACTUAL_BACKUP_NAME="$backup_name" + return 0 + } || { + log_error "[FTP] Download failed" + return 1 + } + fi +} + +# S3 functions +s3_push() { + local backup_file="$1" + local backup_name=$(basename "$backup_file") + local checksum_file="${backup_file}.sha256" + + log_info "[S3] Uploading $backup_name..." + + if [ "${s3_enabled:-false}" != "true" ]; then + log_warn "[S3] S3 is not enabled in configuration" + return 1 + fi + + if command -v aws &> /dev/null; then + local aws_cmd="AWS_ACCESS_KEY_ID=\"${s3_access_key}\" AWS_SECRET_ACCESS_KEY=\"${s3_secret_key}\" aws s3 cp" + local aws_opts="--region \"${s3_region}\"" + + # Add endpoint-url if configured + if [ -n "${s3_endpoint_url:-}" ]; then + aws_opts="$aws_opts --endpoint-url \"${s3_endpoint_url}\" --no-verify-ssl" + fi + + eval "$aws_cmd \"$backup_file\" \"s3://${s3_bucket}/${s3_prefix}${backup_name}\" $aws_opts >> \"$LOG_FILE\" 2>&1" && \ + eval "$aws_cmd \"$checksum_file\" \"s3://${s3_bucket}/${s3_prefix}${backup_name}.sha256\" $aws_opts >> \"$LOG_FILE\" 2>&1" && { + log_success "[S3] Upload completed" + return 0 + } || { + log_error "[S3] Upload failed" + return 1 + } + else + log_error "[S3] aws-cli not installed" + return 1 + fi +} + +s3_list() { + log_info "[S3] Listing backups..." + + if command -v aws &> /dev/null; then + local aws_cmd="AWS_ACCESS_KEY_ID=\"${s3_access_key}\" AWS_SECRET_ACCESS_KEY=\"${s3_secret_key}\" aws s3 ls" + local aws_opts="--region \"${s3_region}\"" + + # Add endpoint-url if configured + if [ -n "${s3_endpoint_url:-}" ]; then + aws_opts="$aws_opts --endpoint-url \"${s3_endpoint_url}\" --no-verify-ssl" + fi + + eval "$aws_cmd \"s3://${s3_bucket}/${s3_prefix}\" $aws_opts 2>> \"$LOG_FILE\" | grep \".tar.xz$\"" + fi +} + +s3_pull() { + local backup_name="$1" + local dest_dir="$2" + + log_info "[S3] Downloading $backup_name..." + + if command -v aws &> /dev/null; then + mkdir -p "$dest_dir" + + local aws_cmd="AWS_ACCESS_KEY_ID=\"${s3_access_key}\" AWS_SECRET_ACCESS_KEY=\"${s3_secret_key}\" aws s3 cp" + local aws_opts="--region \"${s3_region}\"" + + # Add endpoint-url if configured + if [ -n "${s3_endpoint_url:-}" ]; then + aws_opts="$aws_opts --endpoint-url \"${s3_endpoint_url}\" --no-verify-ssl" + fi + + eval "$aws_cmd \"s3://${s3_bucket}/${s3_prefix}${backup_name}\" \"${dest_dir}/${backup_name}\" $aws_opts >> \"$LOG_FILE\" 2>&1" && \ + eval "$aws_cmd \"s3://${s3_bucket}/${s3_prefix}${backup_name}.sha256\" \"${dest_dir}/${backup_name}.sha256\" $aws_opts >> \"$LOG_FILE\" 2>&1" && { + log_success "[S3] Download completed" + return 0 + } || { + log_error "[S3] Download failed" + return 1 + } + fi +} + +# Rsync functions +rsync_push() { + local backup_file="$1" + local backup_name=$(basename "$backup_file") + local checksum_file="${backup_file}.sha256" + + log_info "[RSYNC] Uploading $backup_name..." + + if [ "${rsync_enabled:-false}" != "true" ]; then + log_warn "[RSYNC] Rsync is not enabled in configuration" + return 1 + fi + + local rsync_opts="-avz" + [ "$VERBOSE" = true ] && rsync_opts="-avzP" + + if [ -n "${rsync_ssh_key:-}" ]; then + rsync $rsync_opts -e "ssh -i ${rsync_ssh_key}" \ + "$backup_file" "$checksum_file" \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/" >> "$LOG_FILE" 2>&1 && { + log_success "[RSYNC] Upload completed" + return 0 + } || { + log_error "[RSYNC] Upload failed" + return 1 + } + else + rsync $rsync_opts "$backup_file" "$checksum_file" \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/" >> "$LOG_FILE" 2>&1 && { + log_success "[RSYNC] Upload completed" + return 0 + } || { + log_error "[RSYNC] Upload failed" + return 1 + } + fi +} + +rsync_list() { + log_info "[RSYNC] Listing backups..." + + if [ -n "${rsync_ssh_key:-}" ]; then + ssh -i "${rsync_ssh_key}" "${rsync_user}@${rsync_host}" \ + "ls -lh ${rsync_remote_path}/*.tar.xz" 2>> "$LOG_FILE" + else + ssh "${rsync_user}@${rsync_host}" \ + "ls -lh ${rsync_remote_path}/*.tar.xz" 2>> "$LOG_FILE" + fi +} + +rsync_pull() { + local backup_name="$1" + local dest_dir="$2" + + log_info "[RSYNC] Downloading $backup_name..." + + mkdir -p "$dest_dir" + + local rsync_opts="-avz" + [ "$VERBOSE" = true ] && rsync_opts="-avzP" + + if [ -n "${rsync_ssh_key:-}" ]; then + rsync $rsync_opts -e "ssh -i ${rsync_ssh_key}" \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/${backup_name}" \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/${backup_name}.sha256" \ + "$dest_dir/" >> "$LOG_FILE" 2>&1 && { + log_success "[RSYNC] Download completed" + return 0 + } || { + log_error "[RSYNC] Download failed" + return 1 + } + else + rsync $rsync_opts \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/${backup_name}" \ + "${rsync_user}@${rsync_host}:${rsync_remote_path}/${backup_name}.sha256" \ + "$dest_dir/" >> "$LOG_FILE" 2>&1 && { + log_success "[RSYNC] Download completed" + return 0 + } || { + log_error "[RSYNC] Download failed" + return 1 + } + fi +} + +# Command: backup +cmd_backup() { + log_info "Starting local backup operation (no remote push)" + + acquire_lock + trap release_lock EXIT + + # Check if backup script exists + if [ ! -f "$BACKUP_SCRIPT" ]; then + log_error "beamup-backup.sh not found at $BACKUP_SCRIPT" + exit 1 + fi + + log_info "Running beamup-backup.sh..." + if [ "$VERBOSE" = true ]; then + "$BACKUP_SCRIPT" --verbose + else + "$BACKUP_SCRIPT" + fi + + # Verify backup was created + local latest_backup=$(get_latest_backup) + if [ -z "$latest_backup" ]; then + log_error "Backup creation failed" + exit 1 + fi + + log_success "Local backup created successfully" + log_info "Backup location: $latest_backup" + log_info "To push this backup to remote storage, run: beamup-sync push" +} + +# Command: push +cmd_push() { + log_info "Starting backup push operation" + + acquire_lock + trap release_lock EXIT + + # Determine remotes to use + local remotes=($(determine_remotes)) + + # If no remotes configured, just run backup + if [ ${#remotes[@]} -eq 0 ]; then + log_warn "No remotes configured, running backup only..." + + # Check if backup script exists + if [ -f "$BACKUP_SCRIPT" ]; then + log_info "Running beamup-backup.sh..." + if [ "$VERBOSE" = true ]; then + "$BACKUP_SCRIPT" --verbose + else + "$BACKUP_SCRIPT" + fi + log_success "Backup completed (no remotes to push to)" + exit 0 + else + log_error "beamup-backup.sh not found at $BACKUP_SCRIPT" + exit 1 + fi + fi + + # Find backups to push + local backups="" + + # If --force flag is set, create a new backup first + if [ "$FORCE_BACKUP" = true ]; then + log_info "Force flag detected, creating new backup..." + + if [ -f "$BACKUP_SCRIPT" ]; then + log_info "Running beamup-backup.sh..." + if [ "$VERBOSE" = true ]; then + "$BACKUP_SCRIPT" --verbose + else + "$BACKUP_SCRIPT" + fi + + # Get the newly created backup (should be the latest) + backups=$(get_latest_backup) + if [ -z "$backups" ]; then + log_error "Backup creation failed" + exit 1 + fi + log_success "New backup created successfully" + else + log_error "beamup-backup.sh not found at $BACKUP_SCRIPT" + exit 1 + fi + else + # Normal operation: find existing backups to push + backups=$(list_local_backups) + if [ -z "$backups" ]; then + log_warn "No backups found to push" + + # Offer to create backup + log_info "Would you like to create a backup now? This will happen automatically." + if [ -f "$BACKUP_SCRIPT" ]; then + log_info "Running beamup-backup.sh..." + if [ "$VERBOSE" = true ]; then + "$BACKUP_SCRIPT" --verbose + else + "$BACKUP_SCRIPT" + fi + + # Get the newly created backup + backups=$(list_local_backups) + if [ -z "$backups" ]; then + log_error "Backup creation failed" + exit 1 + fi + else + log_error "beamup-backup.sh not found" + exit 1 + fi + fi + fi + + log_info "Pushing to remotes: ${remotes[*]}" + + local success_count=0 + local fail_count=0 + + while IFS= read -r backup_file; do + log_info "Processing: $(basename "$backup_file")" + + local remote_success=0 + local remote_fail=0 + + for remote in "${remotes[@]}"; do + case "$remote" in + ftp) + ftp_push "$backup_file" && ((remote_success++)) || ((remote_fail++)) + ;; + s3) + s3_push "$backup_file" && ((remote_success++)) || ((remote_fail++)) + ;; + rsync) + rsync_push "$backup_file" && ((remote_success++)) || ((remote_fail++)) + ;; + esac + done + + if [ $remote_success -gt 0 ]; then + ((success_count++)) + if [ $remote_fail -gt 0 ]; then + log_warn "Backup pushed to $remote_success remote(s), but failed on $remote_fail" + fi + else + ((fail_count++)) + log_error "Failed to push backup to any remote" + fi + done <<< "$backups" + + log_info "==========================================" + log_success "Push operation completed" + log_info "Successful: $success_count, Failed: $fail_count" + log_info "==========================================" + + [ $success_count -gt 0 ] && exit 0 || exit 1 +} + +# Command: pull +cmd_pull() { + if [ -z "$BACKUP_NAME" ]; then + log_error "Backup name required. Use: beamup-sync pull BACKUP_NAME" + exit 1 + fi + + log_info "Starting backup pull operation" + + acquire_lock + trap release_lock EXIT + + local remotes=($(determine_remotes)) + if [ ${#remotes[@]} -eq 0 ]; then + log_error "No remotes configured or specified" + exit 1 + fi + + # Create download directory + local download_ts=$(date +%Y%m%d_%H%M%S) + local download_dir="${BEAMUP_BASE}/downloaded-${download_ts}" + mkdir -p "$download_dir" + + log_info "Pulling from remotes: ${remotes[*]}" + + local downloaded=false + ACTUAL_BACKUP_NAME="" + for remote in "${remotes[@]}"; do + case "$remote" in + ftp) + ftp_pull "$BACKUP_NAME" "$download_dir" && downloaded=true && break + ;; + s3) + s3_pull "$BACKUP_NAME" "$download_dir" && downloaded=true && break + ;; + rsync) + rsync_pull "$BACKUP_NAME" "$download_dir" && downloaded=true && break + ;; + esac + done + if [ -n "$ACTUAL_BACKUP_NAME" ]; then + BACKUP_NAME="$ACTUAL_BACKUP_NAME" + fi + + if [ "$downloaded" = false ]; then + log_error "Failed to download backup from any remote" + rm -rf "$download_dir" + exit 1 + fi + + local downloaded_file="${download_dir}/${BACKUP_NAME}" + + # Verify downloaded backup + verify_backup "$downloaded_file" || { + log_error "Downloaded backup failed integrity check" + exit 1 + } + + log_success "Backup downloaded successfully to: $downloaded_file" + + # Auto-restore if requested + if [ "$AUTO_RESTORE" = true ]; then + log_info "Auto-restore enabled, starting restore..." + "$RESTORE_SCRIPT" --force "$downloaded_file" + fi +} + +# Command: restore +cmd_restore() { + log_info "Starting restore from latest local backup" + "$RESTORE_SCRIPT" +} + +# Command: list +cmd_list() { + local remotes=($(determine_remotes)) + if [ ${#remotes[@]} -eq 0 ]; then + log_error "No remotes configured or specified" + exit 1 + fi + + for remote in "${remotes[@]}"; do + echo "" + echo "=== $remote backups ===" + case "$remote" in + ftp) ftp_list ;; + s3) s3_list ;; + rsync) rsync_list ;; + esac + done +} + +# Command: config +cmd_config() { + echo "" + echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Beamup Sync - Interactive Configuration ║${NC}" + echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo "" + + mkdir -p "$CONFIG_DIR" + + # Check if updating existing config or creating new + local updating_config=false + if [ -f "$CONFIG_FILE" ]; then + updating_config=true + fi + + # Backup existing config if it exists + if [ -f "$CONFIG_FILE" ]; then + local backup_config="${CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)" + cp "$CONFIG_FILE" "$backup_config" + log_info "Existing configuration backed up to: $backup_config" + echo "" + fi + + # Start building config + local config_content="# Beamup Sync Configuration +# Generated on $(date +%Y-%m-%d\ %H:%M:%S) + +[general] +" + + # General settings + echo -e "${GREEN}=== General Settings ===${NC}" + echo "" + + read -p "Retention days for backups [30]: " retention_days + retention_days=${retention_days:-30} + config_content+="retention_days=${retention_days} +" + + read -p "Enable parallel uploads? (y/n) [n]: " parallel_uploads + parallel_uploads=${parallel_uploads:-n} + if [[ "$parallel_uploads" =~ ^[yY]$ ]]; then + config_content+="parallel_uploads=true +" + else + config_content+="parallel_uploads=false +" + fi + + read -p "Require all remotes to succeed? (y/n) [n]: " require_all + require_all=${require_all:-n} + if [[ "$require_all" =~ ^[yY]$ ]]; then + config_content+="require_all_success=true +" + else + config_content+="require_all_success=false +" + fi + + local enabled_remotes=() + + # FTP Configuration + echo "" + echo -e "${GREEN}=== FTP Configuration ===${NC}" + echo "" + read -p "Enable FTP backup? (y/n) [n]: " enable_ftp + enable_ftp=${enable_ftp:-n} + + if [[ "$enable_ftp" =~ ^[yY]$ ]]; then + enabled_remotes+=("ftp") + config_content+=" +[ftp] +enabled=true +" + + read -p "FTP Host: " ftp_host + config_content+="host=${ftp_host} +" + + read -p "FTP Port [21]: " ftp_port + ftp_port=${ftp_port:-21} + config_content+="port=${ftp_port} +" + + read -p "FTP Username: " ftp_user + config_content+="username=${ftp_user} +" + + read -sp "FTP Password: " ftp_pass + echo "" + config_content+="password=${ftp_pass} +" + + read -p "Remote path [/backups]: " ftp_path + ftp_path=${ftp_path:-/backups} + config_content+="remote_path=${ftp_path} +" + + read -p "Verify SSL? (y/n) [y]: " ftp_ssl + ftp_ssl=${ftp_ssl:-y} + if [[ "$ftp_ssl" =~ ^[yY]$ ]]; then + config_content+="verify_ssl=true +" + else + config_content+="verify_ssl=false +" + fi + else + config_content+=" +[ftp] +enabled=false +host=ftp.example.com +port=21 +username=backup_user +password= +remote_path=/backups +verify_ssl=true +" + fi + + # S3 Configuration + echo "" + echo -e "${GREEN}=== S3 Configuration ===${NC}" + echo "" + read -p "Enable S3 backup? (y/n) [n]: " enable_s3 + enable_s3=${enable_s3:-n} + + if [[ "$enable_s3" =~ ^[yY]$ ]]; then + enabled_remotes+=("s3") + config_content+=" +[s3] +enabled=true +" + + read -p "S3 Bucket name: " s3_bucket + config_content+="bucket=${s3_bucket} +" + + read -p "S3 Region [us-east-1]: " s3_region + s3_region=${s3_region:-us-east-1} + config_content+="region=${s3_region} +" + + read -p "S3 Access Key: " s3_access + config_content+="access_key=${s3_access} +" + + read -sp "S3 Secret Key: " s3_secret + echo "" + config_content+="secret_key=${s3_secret} +" + + read -p "S3 Prefix (path) []: " s3_prefix + config_content+="prefix=${s3_prefix} +" + + # New: Endpoint URL for testing/mockup servers + echo "" + echo -e "${YELLOW}Testing/Development Options:${NC}" + read -p "S3 Endpoint URL (for testing with MinIO/LocalStack, leave empty for AWS) []: " s3_endpoint + if [ -n "$s3_endpoint" ]; then + config_content+="endpoint_url=${s3_endpoint} +" + log_info "Custom endpoint configured - SSL verification will be disabled" + else + config_content+="endpoint_url= +" + fi + else + config_content+=" +[s3] +enabled=false +bucket=my-backups +region=us-east-1 +access_key= +secret_key= +prefix= +endpoint_url= +" + fi + + # Rsync Configuration + echo "" + echo -e "${GREEN}=== Rsync Configuration ===${NC}" + echo "" + read -p "Enable Rsync backup? (y/n) [n]: " enable_rsync + enable_rsync=${enable_rsync:-n} + + if [[ "$enable_rsync" =~ ^[yY]$ ]]; then + enabled_remotes+=("rsync") + config_content+=" +[rsync] +enabled=true +" + + read -p "Rsync Host: " rsync_host + config_content+="host=${rsync_host} +" + + read -p "Rsync User: " rsync_user + config_content+="user=${rsync_user} +" + + read -p "Remote path: " rsync_path + config_content+="remote_path=${rsync_path} +" + + read -p "SSH Key path [/root/.ssh/id_rsa]: " rsync_key + rsync_key=${rsync_key:-/root/.ssh/id_rsa} + config_content+="ssh_key=${rsync_key} +" + else + config_content+=" +[rsync] +enabled=false +host=backup.example.com +user=backup +remote_path=/backups +ssh_key=/root/.ssh/id_rsa +" + fi + + # Add enabled_remotes to general section + if [ ${#enabled_remotes[@]} -gt 0 ]; then + local remotes_str=$(IFS=,; echo "${enabled_remotes[*]}") + # Insert after [general] section + config_content=$(echo "$config_content" | sed "s/\[general\]/[general]\nenabled_remotes=${remotes_str}/") + else + config_content=$(echo "$config_content" | sed "s/\[general\]/[general]\nenabled_remotes=/") + fi + + # Write configuration file + echo "$config_content" > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + + echo "" + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Configuration saved successfully! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo "" + log_success "Configuration file created: $CONFIG_FILE" + log_info "File permissions set to 600 (readable only by owner)" + + if [ ${#enabled_remotes[@]} -gt 0 ]; then + log_info "Enabled remotes: ${enabled_remotes[*]}" + echo "" + log_info "Testing configuration..." + cmd_verify + else + log_warn "No remotes enabled. You can edit $CONFIG_FILE to enable remotes later." + fi + + # Cronjob configuration + echo "" + echo -e "${GREEN}=== Automatic Backup Schedule ===${NC}" + echo "" + read -p "Would you like to set up automatic backups? (y/n) [y]: " setup_cron + setup_cron=${setup_cron:-y} + + if [[ "$setup_cron" =~ ^[yY]$ ]]; then + configure_cronjob "$updating_config" + else + log_info "Skipping automatic backup setup" + echo "" + log_info "You can manually set up a cronjob later with:" + log_info " crontab -e" + log_info " # Add: 0 2 * * * /usr/local/bin/beamup-sync push >/dev/null 2>&1" + fi +} + +# Configure cronjob for automatic backups +configure_cronjob() { + local updating="$1" + + echo "" + echo "Select backup frequency:" + echo " 1) Daily (recommended)" + echo " 2) Weekly (every Sunday)" + echo " 3) Custom schedule" + echo "" + read -p "Choose option [1]: " cron_option + cron_option=${cron_option:-1} + + local cron_schedule="" + local cron_description="" + + case $cron_option in + 1) + read -p "What time (24-hour format, e.g., 02:00)? [02:00]: " backup_time + backup_time=${backup_time:-02:00} + + # Parse hour and minute + local hour=$(echo "$backup_time" | cut -d: -f1) + local minute=$(echo "$backup_time" | cut -d: -f2) + + # Remove leading zeros for cron + hour=$((10#$hour)) + minute=$((10#$minute)) + + cron_schedule="$minute $hour * * *" + cron_description="Daily at $backup_time" + ;; + 2) + read -p "What time on Sunday (24-hour format, e.g., 02:00)? [02:00]: " backup_time + backup_time=${backup_time:-02:00} + + local hour=$(echo "$backup_time" | cut -d: -f1) + local minute=$(echo "$backup_time" | cut -d: -f2) + + hour=$((10#$hour)) + minute=$((10#$minute)) + + cron_schedule="$minute $hour * * 0" + cron_description="Weekly on Sunday at $backup_time" + ;; + 3) + echo "" + echo "Enter cron schedule (e.g., '0 2 * * *' for daily at 2 AM):" + read -p "Schedule: " custom_schedule + cron_schedule="$custom_schedule" + cron_description="Custom: $custom_schedule" + ;; + *) + log_error "Invalid option" + return 1 + ;; + esac + + # Verbose option + read -p "Enable verbose logging in cronjob? (y/n) [n]: " cron_verbose + cron_verbose=${cron_verbose:-n} + + local beamup_command="/usr/local/bin/beamup-sync push" + if [[ "$cron_verbose" =~ ^[yY]$ ]]; then + beamup_command="$beamup_command --verbose" + fi + + # Build the cron entry + local cron_entry="# Beamup automatic backup - $cron_description" + cron_entry="$cron_entry +$cron_schedule $beamup_command" + + cron_entry="$cron_entry >/dev/null 2>&1" + + # Check for existing beamup cronjob + if crontab -l 2>/dev/null | grep -q "beamup-sync push"; then + echo "" + log_warn "Existing beamup cronjob found" + + if [ "$updating" = true ]; then + read -p "Replace existing cronjob? (y/n) [y]: " replace_cron + replace_cron=${replace_cron:-y} + else + replace_cron="y" + fi + + if [[ "$replace_cron" =~ ^[yY]$ ]]; then + # Remove old beamup entries + crontab -l 2>/dev/null | grep -v "beamup-sync push" | grep -v "Beamup automatic backup" | crontab - + log_info "Removed old beamup cronjob" + else + log_info "Keeping existing cronjob" + return 0 + fi + fi + + # Add new cronjob + (crontab -l 2>/dev/null; echo ""; echo "$cron_entry") | crontab - + + echo "" + log_success "Cronjob installed successfully!" + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Backup Schedule ║${NC}" + echo -e "${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}" + echo -e "${GREEN}║ Schedule: $cron_description" + echo -e "${GREEN}║ Command: $beamup_command" + echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" + echo "" + log_info "View your crontab with: crontab -l" + log_info "Edit your crontab with: crontab -e" +} + +# Command: verify +cmd_verify() { + log_info "Verifying configuration..." + + if [ ! -f "$CONFIG_FILE" ]; then + log_error "Configuration file not found" + exit 1 + fi + + log_success "Configuration file found" + + # Test each enabled remote + local remotes=($(determine_remotes)) + for remote in "${remotes[@]}"; do + log_info "Testing $remote connection..." + case "$remote" in + ftp) + command -v lftp &> /dev/null && log_success "[FTP] lftp installed" || log_error "[FTP] lftp not installed" + ;; + s3) + command -v aws &> /dev/null && log_success "[S3] aws-cli installed" || log_error "[S3] aws-cli not installed" + ;; + rsync) + command -v rsync &> /dev/null && log_success "[RSYNC] rsync installed" || log_error "[RSYNC] rsync not installed" + ;; + esac + done +} + +# Main command router +case "$COMMAND" in + push) + cmd_push + ;; + pull) + cmd_pull + ;; + backup) + cmd_backup + ;; + restore) + cmd_restore + ;; + list) + cmd_list + ;; + config) + cmd_config + ;; + verify) + cmd_verify + ;; + *) + show_usage + exit 1 + ;; +esac \ No newline at end of file