diff --git a/vps-snapshot.sh b/vps-snapshot.sh index 0a4448b..7f37ee7 100755 --- a/vps-snapshot.sh +++ b/vps-snapshot.sh @@ -1,10 +1,9 @@ #!/bin/bash #=============================================================================== -# VPS 快照备份脚本 v1.1 +# VPS 快照备份脚本 v2.0 # 支持: Ubuntu, Debian, CentOS, Alpine -# 功能: 系统快照 + rsync 远程同步 + Telegram 通知 + 自动清理 -# 认证: 支持密码和 SSH 密钥两种方式 +# 功能: 创建/恢复快照 + rsync 远程同步 + Telegram 通知 + 自动清理 #=============================================================================== set -e @@ -13,6 +12,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' NC='\033[0m' CONFIG_FILE="/etc/vps-snapshot.conf" @@ -22,16 +22,16 @@ SSH_KEY_PATH="/root/.ssh/vps_snapshot_key" print_banner() { echo -e "${BLUE}" echo "╔═══════════════════════════════════════════════════════════╗" - echo "║ VPS 快照备份脚本 v1.1 ║" + echo "║ VPS 快照备份脚本 v2.0 ║" echo "║ 支持 Ubuntu/Debian/CentOS/Alpine ║" echo "║ 支持密码/SSH密钥认证 ║" echo "╚═══════════════════════════════════════════════════════════╝" echo -e "${NC}" } -log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"; echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; } -error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}"; echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE"; } -warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARN: $1${NC}"; } +log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}" >&2; echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; } +error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" >&2; echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE"; } +warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARN: $1${NC}" >&2; } detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release; echo "$ID" @@ -52,6 +52,10 @@ install_dependencies() { log "依赖安装完成" } +#------------------------------------------------------------------------------- +# SSH 密钥管理 +#------------------------------------------------------------------------------- + generate_ssh_key() { log "生成 SSH 密钥对..." if [ -f "$SSH_KEY_PATH" ]; then @@ -59,9 +63,9 @@ generate_ssh_key() { read -p "是否覆盖? [y/N]: " overwrite [[ ! "$overwrite" =~ ^[Yy]$ ]] && return 0 fi - ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "vps-snapshot-$(hostname)" + ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "vps-snapshot-${VPS_NAME:-$(hostname)}" chmod 600 "$SSH_KEY_PATH" - log "密钥生成完成: $SSH_KEY_PATH" + log "密钥生成完成" } copy_ssh_key_to_remote() { @@ -81,23 +85,46 @@ test_ssh_connection() { fi } +ssh_exec() { + if [ "$AUTH_METHOD" = "key" ]; then + ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -p "$REMOTE_PORT" "${REMOTE_USER}@${REMOTE_IP}" "$1" + else + sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no -p "$REMOTE_PORT" "${REMOTE_USER}@${REMOTE_IP}" "$1" + fi +} + +send_telegram() { + [ -n "$TG_BOT_TOKEN" ] && [ -n "$TG_CHAT_ID" ] && \ + curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \ + -d chat_id="$TG_CHAT_ID" -d text="$1" -d parse_mode="HTML" >/dev/null 2>&1 +} + +#------------------------------------------------------------------------------- +# 交互式配置 +#------------------------------------------------------------------------------- + interactive_setup() { print_banner echo -e "${YELLOW}开始交互式配置...${NC}\n" + # VPS 名称 + read -p "请输入 VPS 名称 (用于区分备份): " VPS_NAME + VPS_NAME=${VPS_NAME:-$(hostname)} + + # 远程服务器信息 read -p "请输入远程服务器 IP: " REMOTE_IP read -p "请输入 SSH 端口 [22]: " REMOTE_PORT REMOTE_PORT=${REMOTE_PORT:-22} read -p "请输入 SSH 用户名 [root]: " REMOTE_USER REMOTE_USER=${REMOTE_USER:-root} + # 认证方式 echo -e "\n${YELLOW}选择认证方式:${NC}" - echo "1) SSH 密钥 (推荐,更安全)" + echo "1) SSH 密钥 (推荐)" echo "2) 密码" read -p "请选择 [1]: " AUTH_TYPE - AUTH_TYPE=${AUTH_TYPE:-1} - if [ "$AUTH_TYPE" = "1" ]; then + if [ "${AUTH_TYPE:-1}" = "1" ]; then setup_ssh_key_auth else setup_password_auth @@ -115,8 +142,7 @@ setup_ssh_key_auth() { read -p "使用现有密钥? [Y/n]: " use_existing [[ "$use_existing" =~ ^[Nn]$ ]] && generate_ssh_key else - read -p "是否生成新的 SSH 密钥? [Y/n]: " gen_key - [[ ! "$gen_key" =~ ^[Nn]$ ]] && generate_ssh_key + generate_ssh_key fi echo -e "\n需要将公钥复制到远程服务器" @@ -142,46 +168,59 @@ setup_password_auth() { } continue_setup() { - read -p "请输入远程备份目录 [/backup/snapshots]: " REMOTE_DIR - REMOTE_DIR=${REMOTE_DIR:-/backup/snapshots} + # 远程目录 - 自动创建以 VPS 名称命名的文件夹 + read -p "请输入远程备份根目录 [/backup]: " REMOTE_BASE + REMOTE_BASE=${REMOTE_BASE:-/backup} + REMOTE_DIR="${REMOTE_BASE}/${VPS_NAME}" + read -p "请输入本地快照目录 [/var/snapshots]: " LOCAL_DIR LOCAL_DIR=${LOCAL_DIR:-/var/snapshots} - read -p "本地保留快照数量 [1]: " LOCAL_KEEP - LOCAL_KEEP=${LOCAL_KEEP:-1} + read -p "本地保留快照数量 [3]: " LOCAL_KEEP + LOCAL_KEEP=${LOCAL_KEEP:-3} read -p "远程保留天数 [30]: " REMOTE_KEEP_DAYS REMOTE_KEEP_DAYS=${REMOTE_KEEP_DAYS:-30} + # Telegram read -p "是否启用 Telegram 通知? [y/N]: " ENABLE_TG if [[ "$ENABLE_TG" =~ ^[Yy]$ ]]; then read -p "请输入 Telegram Bot Token: " TG_BOT_TOKEN read -p "请输入 Telegram Chat ID: " TG_CHAT_ID fi + setup_backup_dirs +} + +setup_backup_dirs() { echo -e "\n${YELLOW}选择要备份的内容:${NC}" echo "1) 完整系统 (排除临时文件)" echo "2) 仅 /etc /home /root /var/www" echo "3) 自定义目录" read -p "请选择 [1]: " BACKUP_TYPE - BACKUP_TYPE=${BACKUP_TYPE:-1} - case $BACKUP_TYPE in + case ${BACKUP_TYPE:-1} in 2) BACKUP_DIRS="/etc /home /root /var/www" ;; - 3) read -p "请输入要备份的目录 (空格分隔): " BACKUP_DIRS ;; + 3) read -p "请输入目录 (空格分隔): " BACKUP_DIRS ;; *) BACKUP_DIRS="/" ;; esac save_config + + # 创建远程目录 + log "创建远程目录: $REMOTE_DIR" + ssh_exec "mkdir -p $REMOTE_DIR" } save_config() { log "保存配置到 $CONFIG_FILE" cat > "$CONFIG_FILE" << CONF +VPS_NAME="$VPS_NAME" AUTH_METHOD="$AUTH_METHOD" SSH_KEY_PATH="$SSH_KEY_PATH" REMOTE_IP="$REMOTE_IP" REMOTE_PORT="$REMOTE_PORT" REMOTE_USER="$REMOTE_USER" REMOTE_PASS="$REMOTE_PASS" +REMOTE_BASE="$REMOTE_BASE" REMOTE_DIR="$REMOTE_DIR" LOCAL_DIR="$LOCAL_DIR" LOCAL_KEEP="$LOCAL_KEEP" @@ -195,32 +234,21 @@ CONF } load_config() { - [ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE" || { error "配置文件不存在,请先运行: $0 setup"; return 1; } + [ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE" || { error "未配置,请先运行: $0 setup"; return 1; } } -ssh_exec() { - if [ "$AUTH_METHOD" = "key" ]; then - ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -p "$REMOTE_PORT" "${REMOTE_USER}@${REMOTE_IP}" "$1" - else - sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no -p "$REMOTE_PORT" "${REMOTE_USER}@${REMOTE_IP}" "$1" - fi -} - -send_telegram() { - [ -n "$TG_BOT_TOKEN" ] && [ -n "$TG_CHAT_ID" ] && \ - curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \ - -d chat_id="$TG_CHAT_ID" -d text="$1" -d parse_mode="HTML" >/dev/null 2>&1 -} +#------------------------------------------------------------------------------- +# 快照操作 +#------------------------------------------------------------------------------- create_snapshot() { - local hostname=$(hostname) local timestamp=$(date '+%Y%m%d_%H%M%S') - local snapshot_name="${hostname}_${timestamp}.tar.gz" + local snapshot_name="${VPS_NAME}_${timestamp}.tar.gz" local snapshot_path="${LOCAL_DIR}/${snapshot_name}" mkdir -p "$LOCAL_DIR" log "开始创建快照: $snapshot_name" - send_telegram "🔄 开始备份%0A主机: ${hostname}" + send_telegram "🔄 开始备份%0AVPS: ${VPS_NAME}" local excludes="--exclude=/proc --exclude=/sys --exclude=/dev" excludes+=" --exclude=/run --exclude=/tmp --exclude=/mnt" @@ -233,117 +261,292 @@ create_snapshot() { tar -czf "$snapshot_path" $BACKUP_DIRS 2>/dev/null || true fi - log "快照创建完成: $snapshot_path ($(du -h "$snapshot_path" | cut -f1))" + log "快照完成: $snapshot_path ($(du -h "$snapshot_path" | cut -f1))" echo "$snapshot_path" } sync_to_remote() { local snapshot_path="$1" - log "创建远程目录: $REMOTE_DIR" - ssh_exec "mkdir -p $REMOTE_DIR" - - log "开始同步到远程..." + log "同步到远程: $REMOTE_DIR" + if [ "$AUTH_METHOD" = "key" ]; then - rsync -avz -e "ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -p $REMOTE_PORT" \ + rsync -avz --progress -e "ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -p $REMOTE_PORT" \ "$snapshot_path" "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/" else - sshpass -p "$REMOTE_PASS" rsync -avz \ + sshpass -p "$REMOTE_PASS" rsync -avz --progress \ -e "ssh -o StrictHostKeyChecking=no -p $REMOTE_PORT" \ "$snapshot_path" "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/" fi - log "远程同步完成" + log "同步完成" } cleanup_local() { - log "清理本地快照,保留最新 $LOCAL_KEEP 个" + log "清理本地快照,保留 $LOCAL_KEEP 个" cd "$LOCAL_DIR" ls -1t *.tar.gz 2>/dev/null | tail -n +$((LOCAL_KEEP + 1)) | xargs -r rm -f } cleanup_remote() { - log "清理远程超过 $REMOTE_KEEP_DAYS 天的快照" + log "清理远程 $REMOTE_KEEP_DAYS 天前的快照" ssh_exec "find $REMOTE_DIR -name '*.tar.gz' -mtime +$REMOTE_KEEP_DAYS -delete" 2>/dev/null - log "远程清理完成" } run_backup() { load_config || exit 1 - local start_time=$(date +%s) - local hostname=$(hostname) + local start=$(date +%s) - log "========== 开始备份任务 ==========" - local snapshot_path=$(create_snapshot) - sync_to_remote "$snapshot_path" + log "===== 开始备份 [$VPS_NAME] =====" + local snapshot=$(create_snapshot) + sync_to_remote "$snapshot" cleanup_local cleanup_remote - local duration=$(($(date +%s) - start_time)) - local size=$(du -h "$snapshot_path" | cut -f1) - log "========== 备份完成 ==========" - send_telegram "✅ 备份完成%0A主机: ${hostname}%0A大小: ${size}%0A耗时: ${duration}秒" + local dur=$(($(date +%s) - start)) + local size=$(du -h "$snapshot" | cut -f1) + log "===== 备份完成 =====" + send_telegram "✅ 备份完成%0AVPS: ${VPS_NAME}%0A大小: ${size}%0A耗时: ${dur}秒" +} + +#------------------------------------------------------------------------------- +# 快照列表 +#------------------------------------------------------------------------------- + +list_local_snapshots() { + echo -e "\n${CYAN}=== 本地快照 ===${NC}" + if [ -d "$LOCAL_DIR" ]; then + ls -lh "$LOCAL_DIR"/*.tar.gz 2>/dev/null | awk '{print NR") "$9" ("$5")"}' + else + echo " (无)" + fi +} + +list_remote_snapshots() { + echo -e "\n${CYAN}=== 远程快照 [$VPS_NAME] ===${NC}" + ssh_exec "ls -lh $REMOTE_DIR/*.tar.gz 2>/dev/null" | awk '{print NR") "$9" ("$5")"}' +} + +#------------------------------------------------------------------------------- +# 恢复快照 +#------------------------------------------------------------------------------- + +restore_local() { + load_config || exit 1 + list_local_snapshots + + echo "" + read -p "选择要恢复的快照编号: " num + local file=$(ls -1t "$LOCAL_DIR"/*.tar.gz 2>/dev/null | sed -n "${num}p") + + [ -z "$file" ] && { error "无效选择"; return 1; } + + echo -e "${RED}警告: 即将恢复快照到系统!${NC}" + read -p "确认恢复 $file? [y/N]: " confirm + [[ ! "$confirm" =~ ^[Yy]$ ]] && return + + log "恢复快照: $file" + tar -xzf "$file" -C / 2>/dev/null + log "恢复完成,建议重启系统" +} + +restore_from_remote() { + load_config || exit 1 + list_remote_snapshots + + echo "" + read -p "选择要恢复的快照编号: " num + local file=$(ssh_exec "ls -1t $REMOTE_DIR/*.tar.gz" | sed -n "${num}p") + + [ -z "$file" ] && { error "无效选择"; return 1; } + + log "下载远程快照: $file" + local local_file="$LOCAL_DIR/$(basename $file)" + + if [ "$AUTH_METHOD" = "key" ]; then + rsync -avz -e "ssh -i $SSH_KEY_PATH -p $REMOTE_PORT" \ + "${REMOTE_USER}@${REMOTE_IP}:${file}" "$local_file" + else + sshpass -p "$REMOTE_PASS" rsync -avz \ + -e "ssh -p $REMOTE_PORT" \ + "${REMOTE_USER}@${REMOTE_IP}:${file}" "$local_file" + fi + + echo -e "${RED}警告: 即将恢复快照!${NC}" + read -p "确认恢复? [y/N]: " confirm + [[ ! "$confirm" =~ ^[Yy]$ ]] && return + + log "恢复快照..." + tar -xzf "$local_file" -C / 2>/dev/null + log "恢复完成,建议重启" +} + +restore_custom_remote() { + load_config || exit 1 + + read -p "输入远程快照完整路径: " remote_file + [ -z "$remote_file" ] && { error "路径不能为空"; return 1; } + + local local_file="$LOCAL_DIR/$(basename $remote_file)" + mkdir -p "$LOCAL_DIR" + + log "下载: $remote_file" + if [ "$AUTH_METHOD" = "key" ]; then + rsync -avz -e "ssh -i $SSH_KEY_PATH -p $REMOTE_PORT" \ + "${REMOTE_USER}@${REMOTE_IP}:${remote_file}" "$local_file" + else + sshpass -p "$REMOTE_PASS" rsync -avz \ + -e "ssh -p $REMOTE_PORT" \ + "${REMOTE_USER}@${REMOTE_IP}:${remote_file}" "$local_file" + fi + + echo -e "${RED}警告: 即将恢复!${NC}" + read -p "确认? [y/N]: " confirm + [[ ! "$confirm" =~ ^[Yy]$ ]] && return + + tar -xzf "$local_file" -C / 2>/dev/null + log "恢复完成" +} + +#------------------------------------------------------------------------------- +# 配置管理 +#------------------------------------------------------------------------------- + +edit_config() { + load_config || exit 1 + + echo -e "\n${CYAN}=== 当前配置 ===${NC}" + echo "1) VPS名称: $VPS_NAME" + echo "2) 远程IP: $REMOTE_IP" + echo "3) 远程端口: $REMOTE_PORT" + echo "4) 远程用户: $REMOTE_USER" + echo "5) 远程目录: $REMOTE_DIR" + echo "6) 本地目录: $LOCAL_DIR" + echo "7) 本地保留: $LOCAL_KEEP 个" + echo "8) 远程保留: $REMOTE_KEEP_DAYS 天" + echo "9) Telegram: $([ -n "$TG_BOT_TOKEN" ] && echo '已配置' || echo '未配置')" + echo "0) 返回" + + read -p "选择要修改的项: " choice + case $choice in + 1) read -p "新VPS名称: " VPS_NAME; REMOTE_DIR="${REMOTE_BASE}/${VPS_NAME}" ;; + 2) read -p "新远程IP: " REMOTE_IP ;; + 3) read -p "新端口: " REMOTE_PORT ;; + 4) read -p "新用户: " REMOTE_USER ;; + 5) read -p "新远程目录: " REMOTE_DIR ;; + 6) read -p "新本地目录: " LOCAL_DIR ;; + 7) read -p "本地保留数: " LOCAL_KEEP ;; + 8) read -p "远程保留天数: " REMOTE_KEEP_DAYS ;; + 9) read -p "Bot Token: " TG_BOT_TOKEN; read -p "Chat ID: " TG_CHAT_ID ;; + 0) return ;; + esac + save_config + echo -e "${GREEN}配置已更新${NC}" } setup_cron() { - echo -e "${YELLOW}设置定时备份任务${NC}" + echo -e "${YELLOW}设置定时备份${NC}" echo "1) 每天凌晨 3 点" - echo "2) 每周日凌晨 3 点" - echo "3) 每月 1 号凌晨 3 点" - read -p "请选择 [1]: " choice + echo "2) 每 3 天凌晨 3 点" + echo "3) 每周日凌晨 3 点" + echo "4) 每月 1 号" + read -p "选择 [1]: " c - case ${choice:-1} in - 2) cron_expr="0 3 * * 0" ;; - 3) cron_expr="0 3 1 * *" ;; - *) cron_expr="0 3 * * *" ;; + case ${c:-1} in + 2) expr="0 3 */3 * *" ;; + 3) expr="0 3 * * 0" ;; + 4) expr="0 3 1 * *" ;; + *) expr="0 3 * * *" ;; esac - local script_path=$(readlink -f "$0") - (crontab -l 2>/dev/null | grep -v "vps-snapshot"; echo "$cron_expr $script_path run") | crontab - - log "定时任务已设置: $cron_expr" + local path=$(readlink -f "$0") + (crontab -l 2>/dev/null | grep -v "vps-snapshot"; echo "$expr $path run") | crontab - + log "定时任务: $expr" } show_status() { print_banner if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" - echo -e "${GREEN}配置状态:${NC}" - echo " 认证方式: ${AUTH_METHOD}" - echo " 远程服务器: ${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}" - echo " 远程目录: ${REMOTE_DIR}" - echo " 本地目录: ${LOCAL_DIR}" - echo " 本地保留: ${LOCAL_KEEP} 个" - echo " 远程保留: ${REMOTE_KEEP_DAYS} 天" - echo " Telegram: $([ -n "$TG_BOT_TOKEN" ] && echo '已配置' || echo '未配置')" - echo -e "\n${GREEN}本地快照:${NC}" - ls -lh "$LOCAL_DIR"/*.tar.gz 2>/dev/null || echo " (无)" + echo -e "${GREEN}VPS: $VPS_NAME${NC}" + echo "认证: $AUTH_METHOD" + echo "远程: ${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}" + echo "远程目录: $REMOTE_DIR" + echo "本地目录: $LOCAL_DIR" + echo "保留: 本地${LOCAL_KEEP}个 / 远程${REMOTE_KEEP_DAYS}天" + echo "TG: $([ -n "$TG_BOT_TOKEN" ] && echo '✓' || echo '✗')" + list_local_snapshots else - echo -e "${RED}未配置,请运行: $0 setup${NC}" + echo -e "${RED}未配置${NC}" fi } +#------------------------------------------------------------------------------- +# 主菜单 +#------------------------------------------------------------------------------- + +show_menu() { + load_config 2>/dev/null + print_banner + echo -e "${CYAN}VPS: ${VPS_NAME:-未配置}${NC}\n" + echo "1) 创建快照并同步" + echo "2) 仅创建本地快照" + echo "3) 查看本地快照" + echo "4) 查看远程快照" + echo "5) 恢复本地快照" + echo "6) 从远程恢复快照" + echo "7) 自定义远程恢复" + echo "8) 修改配置" + echo "9) 设置定时任务" + echo "10) 查看状态" + echo "0) 退出" + echo "" + read -p "请选择: " choice + + case $choice in + 1) run_backup ;; + 2) load_config && create_snapshot ;; + 3) load_config && list_local_snapshots ;; + 4) load_config && list_remote_snapshots ;; + 5) restore_local ;; + 6) restore_from_remote ;; + 7) restore_custom_remote ;; + 8) edit_config ;; + 9) setup_cron ;; + 10) show_status ;; + 0) exit 0 ;; + *) echo "无效选择" ;; + esac + + echo "" + read -p "按回车继续..." + show_menu +} + show_help() { print_banner echo "用法: $0 <命令>" echo "" echo "命令:" - echo " setup 交互式配置" - echo " run 执行备份" - echo " install 安装依赖" - echo " cron 设置定时任务" - echo " status 查看配置状态" - echo " help 显示帮助" + echo " setup 交互式配置" + echo " run 创建快照并同步" + echo " menu 交互式菜单" + echo " list 查看快照" + echo " restore 恢复快照" + echo " config 修改配置" + echo " cron 设置定时" + echo " status 查看状态" } main() { - [ "$EUID" -ne 0 ] && { error "请使用 root 权限运行"; exit 1; } + [ "$EUID" -ne 0 ] && { error "请用 root 运行"; exit 1; } touch "$LOG_FILE" - case "${1:-help}" in - setup) install_dependencies; interactive_setup - echo -e "\n${GREEN}配置完成!${NC}" - echo "运行 '$0 run' 执行备份" - echo "运行 '$0 cron' 设置定时任务" ;; + case "${1:-menu}" in + setup) install_dependencies; interactive_setup ;; run) run_backup ;; - install) install_dependencies ;; + menu) show_menu ;; + list) load_config && list_local_snapshots && list_remote_snapshots ;; + restore) restore_local ;; + config) edit_config ;; cron) setup_cron ;; status) show_status ;; *) show_help ;;