收起左侧

对飞牛NAS容器部署的应用进行备份(为知笔记)

0
回复
264
查看
[ 复制链接 ]

3

主题

20

回帖

0

牛值

江湖小虾

2025-10-13 11:48:20 显示全部楼层 阅读模式

首先声明,由于代码能力有限,生成的代码由AI辅助完成,我主要在流程上和代码验证上做了相应的验证和修改。

由于在飞牛NAS上部署了“为知笔记”的容器,担心迁移笔记后万一出现系统或应用问题造成数据丢失,因此规划了这个脚本。此脚本理论上也支持检测和备份已经安装的其他容器应用(我只对迅雷容器验证测试)。

这是一个fnNAS下的容器备份脚本,提供交互菜单选项,手工备份需要手动输入备份目录;

可以自动创建定时备份任务(目前定义的是晚上10点自动备份,备份保留3份循环保存,存储路径是/vol3/1000/dockerbackup;);

可以自动恢复容器,支持异地恢复。恢复步骤注意事项:安装容器(如果已安装好可以忽略),手工停止容器,然后再执行脚本选择恢复,恢复菜单会要求输入存放备份的路径,和选择用哪个版本的备份进行恢复,提示脚本提示恢复完成后通常容器会自动拉起,如果发现恢复不成功,可以再此尝试(不成功的情况非常罕见,在多次多环境验证的情况下只发生1,2次应该是容器停止时出现某些异常造成的)。

对于自动备份的部分,包括定义的备份时间、备份副本数、存储路径、提示信息,都可以到脚本中做与你的环境和需求相适应的变更。

#!/bin/bash

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

# 配置
MAX_BACKUPS=3
AUTO_BACKUP_ROOT="/vol3/1000/dockerbackup"  # 自动备份固定目录

# 显示标题
function show_header() {
    clear
    echo -e "${CYAN}${BOLD}===============================================================${NC}"
    echo -e "${CYAN}${BOLD}               Docker 容器备份恢复工具${NC}"
    echo -e "${CYAN}${BOLD}                    支持异地恢复${NC}"
    echo -e "${CYAN}${BOLD}===============================================================${NC}"
    echo ""
}

# 检查 Docker 状态
function check_docker() {
    if ! docker info >/dev/null 2>&1; then
        echo -e "${RED}错误: Docker 未运行或当前用户无权限${NC}"
        return 1
    fi
    return 0
}

# 主菜单
function main_menu() {
    while true; do
        show_header
        echo -e "${CYAN}${BOLD}主菜单${NC}"
        echo -e "==============================================================="
        echo -e "1. 备份容器 (手动)"
        echo -e "2. 恢复容器"
        echo -e "3. 查看现有备份"
        echo -e "4. 安装自动备份定时任务"
        echo -e "0. 退出"
        echo -e "==============================================================="
    
        read -p "请选择操作 (0-4): " choice
    
        case $choice in
            1) backup_menu ;;
            2) restore_menu ;;
            3) view_backups ;;
            4) install_cron_** ;;
            0) 
                echo -e "${GREEN}再见!${NC}"
                exit 0
                ;;
            *) 
                echo -e "${RED}无效选择${NC}"
                sleep 1
                ;;
        esac
    done
}

# 备份菜单
function backup_menu() {
    show_header
    echo -e "${CYAN}${BOLD}备份容器${NC}"
    echo -e "==============================================================="
  
    if ! check_docker; then
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 显示运行中的容器
    echo -e "${YELLOW}运行中的容器:${NC}"
    docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
    echo ""
  
    read -p "请输入要备份的容器名称: " container_name
  
    if [ -z "$container_name" ]; then
        echo -e "${RED}容器名称不能为空${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    if ! docker inspect "$container_name" &>/dev/null; then
        echo -e "${RED}容器不存在: $container_name${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 获取备份路径
    read -p "请输入备份存储路径: " backup_root
  
    if [ -z "$backup_root" ]; then
        echo -e "${RED}备份路径不能为空${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 创建备份目录
    mkdir -p "$backup_root" || {
        echo -e "${RED}无法创建备份目录: $backup_root${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    }
  
    # 执行备份
    if backup_container "$container_name" "$backup_root"; then
        echo -e "${GREEN}备份完成!${NC}"
    else
        echo -e "${RED}备份失败!${NC}"
    fi
  
    read -p "按任意键返回..." -n 1 -s
    echo
}

# 备份容器函数(修复符号链接问题)
function backup_container() {
    local container_name=$1
    local backup_root=$2
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_dir="${backup_root}/${container_name}"
    local backup_file="${backup_dir}/${container_name}_${timestamp}.tar.gz"
  
    mkdir -p "$backup_dir"
  
    echo -e "${YELLOW}开始备份容器: $container_name${NC}"
    echo -e "${GREEN}备份存储路径: $backup_dir${NC}"
  
    # 特殊处理 wizserver 的符号链接问题
    if [ "$container_name" = "wizserver" ]; then
        echo -e "${CYAN}检测到 wizserver,处理符号链接...${NC}"
    
        # 符号链接路径
        local symlink_path="/var/apps/docker-wizserver/shares/wizdata"
        # 真实路径(从符号链接解析)
        local real_path=$(readlink -f "$symlink_path")
    
        if [ -z "$real_path" ]; then
            echo -e "${RED}错误: 无法解析符号链接${NC}"
            return 1
        fi
    
        echo -e "${YELLOW}符号链接: $symlink_path${NC}"
        echo -e "${YELLOW}真实路径: $real_path${NC}"
    
        # 验证真实路径
        if [ ! -d "$real_path" ]; then
            echo -e "${RED}错误: 真实路径不存在: $real_path${NC}"
            return 1
        fi
    
        echo -e "${GREEN}真实路径大小: $(du -sh "$real_path")${NC}"
    
        # 使用真实路径进行备份
        local backup_path="$real_path"
    else
        # 其他容器的标准处理
        local volumes=$(docker inspect --format='{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}} {{end}}{{end}}' "$container_name")
        if [ -z "$volumes" ]; then
            echo -e "${RED}未找到数据卷路径${NC}"
            return 1
        fi
        local backup_path="$volumes"
    fi
  
    echo -e "${GREEN}备份路径: $backup_path${NC}"
  
    # 停止容器
    echo -e "${YELLOW}停止容器...${NC}"
    docker stop "$container_name"
  
    # 创建备份
    echo -e "${YELLOW}创建备份文件...${NC}"
    tar -czf "$backup_file" -C "$backup_path" . 2>/dev/null
  
    # 启动容器
    docker start "$container_name"
  
    # 检查备份结果
    local backup_size=$(du -h "$backup_file" | cut -f1)
    local size_bytes=$(du -b "$backup_file" | cut -f1)
    local file_count=$(tar -tzf "$backup_file" 2>/dev/null | wc -l)
  
    echo -e "${GREEN}备份结果:${NC}"
    echo -e "文件: $backup_file"
    echo -e "大小: $backup_size"
    echo -e "文件数: $file_count"
  
    # 验证备份有效性
    if [ $size_bytes -lt 1048576 ]; then  # 小于1MB
        echo -e "${RED}警告: 备份文件过小,可能未包含实际数据${NC}"
        return 1
    fi
  
    # 清理旧备份
    local backups=($(ls -t "${backup_dir}/${container_name}"_*.tar.gz 2>/dev/null))
    if [ ${#backups[@]} -gt $MAX_BACKUPS ]; then
        for ((i = MAX_BACKUPS; i < ${#backups[@]}; i++)); do
            rm -f "${backups[$i]}"
            echo -e "${YELLOW}删除旧备份: ${backups[$i]}${NC}"
        done
    fi
  
    return 0
}

# 恢复菜单
function restore_menu() {
    show_header
    echo -e "${CYAN}${BOLD}恢复容器${NC}"
    echo -e "==============================================================="
  
    # 获取备份根目录
    read -p "请输入备份存储路径: " backup_root
  
    if [ -z "$backup_root" ]; then
        echo -e "${RED}备份路径不能为空${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 检查备份目录
    if [ ! -d "$backup_root" ]; then
        echo -e "${RED}备份目录不存在: $backup_root${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 获取有备份的容器列表
    local containers=()
    for dir in "$backup_root"/*/; do
        if [ -d "$dir" ]; then
            local container_name=$(basename "$dir")
            if [ "$(ls -A "$dir" 2>/dev/null)" ]; then
                containers+=("$container_name")
            fi
        fi
    done
  
    if [ ${#containers[@]} -eq 0 ]; then
        echo -e "${YELLOW}未找到任何备份${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 显示容器列表
    echo -e "${YELLOW}有备份的容器:${NC}"
    for i in "${!containers[@]}"; do
        local count=$(ls "${backup_root}/${containers[$i]}"/*.tar.gz 2>/dev/null | wc -l)
        echo "$((i+1)). ${containers[$i]} ($count 个备份)"
    done
  
    read -p "请选择容器编号 (1-${#containers[@]}): " choice
  
    if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#containers[@]} ]; then
        echo -e "${RED}无效选择${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    local container_name="${containers[$((choice-1))]}"
    restore_container "$container_name" "$backup_root"
}

# 恢复容器(增强版,支持异地恢复)
function restore_container() {
    local container_name=$1
    local backup_root=$2
    local backup_dir="${backup_root}/${container_name}"
    local backups=($(ls -t "$backup_dir"/*.tar.gz 2>/dev/null))
  
    if [ ${#backups[@]} -eq 0 ]; then
        echo -e "${RED}未找到备份文件${NC}"
        return 1
    fi
  
    # 显示备份列表
    echo -e "${YELLOW}可用的备份:${NC}"
    for i in "${!backups[@]}"; do
        local file="${backups[$i]}"
        local size=$(du -h "$file" | cut -f1)
        local date=$(stat -c %y "$file" | cut -d' ' -f1)
        local time=$(stat -c %y "$file" | cut -d' ' -f2 | cut -d'.' -f1)
        echo "$((i+1)). $(basename "$file")"
        echo "   大小: $size | 时间: $date $time"
    done
  
    read -p "请选择备份编号 (1-${#backups[@]}): " backup_choice
  
    if ! [[ "$backup_choice" =~ ^[0-9]+$ ]] || [ "$backup_choice" -lt 1 ] || [ "$backup_choice" -gt ${#backups[@]} ]; then
        echo -e "${RED}无效选择${NC}"
        return 1
    fi
  
    local selected_backup="${backups[$((backup_choice-1))]}"
  
    echo -e "${RED}警告: 这将覆盖容器 $container_name 的现有数据!${NC}"
    read -p "确认恢复? (y/N): " confirm
  
    if [[ ! $confirm =~ ^[Yy]$ ]]; then
        echo -e "${YELLOW}取消恢复${NC}"
        return
    fi
  
    # 执行恢复
    echo -e "${YELLOW}开始恢复...${NC}"
  
    # 步骤1: 确定恢复路径
    local restore_path=""
    local path_source=""
  
    # 选项1: 容器存在时使用其当前路径
    if docker inspect "$container_name" &>/dev/null; then
        restore_path=$(docker inspect --format='{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}{{break}}{{end}}{{end}}' "$container_name")
        path_source="当前容器配置"
    fi
  
    # 选项2: 用户手动输入路径
    if [ -z "$restore_path" ]; then
        echo -e "${YELLOW}容器不存在或无法获取路径${NC}"
        echo -e "${CYAN}请输入恢复路径 (例如: /vol3/@appshare/wizdata)${NC}"
        read -p "恢复路径: " restore_path
    
        if [ ! -d "$restore_path" ]; then
            echo -e "${RED}错误: 路径不存在${NC}"
            return 1
        fi
        path_source="手动输入"
    fi
  
    echo -e "${GREEN}恢复路径 ($path_source): $restore_path${NC}"
  
    # 步骤2: 验证备份内容
    echo -e "${YELLOW}备份内容预览:${NC}"
    tar -tzf "$selected_backup" | head -10
    echo -e "${YELLOW}... (共 $(tar -tzf "$selected_backup" | wc -l) 个文件)${NC}"
  
    # 步骤3: 停止容器(如果存在)
    if docker inspect "$container_name" &>/dev/null; then
        echo -e "${YELLOW}停止容器...${NC}"
        docker stop "$container_name"
    fi
  
    # 步骤4: 解压备份
    echo -e "${YELLOW}解压备份文件到: $restore_path${NC}"
    tar -xzf "$selected_backup" -C "$restore_path"
  
    if [ $? -eq 0 ]; then
        echo -e "${GREEN}数据恢复成功${NC}"
    
        # 步骤5: 启动容器(如果存在)
        if docker inspect "$container_name" &>/dev/null; then
            echo -e "${YELLOW}启动容器...${NC}"
            docker start "$container_name"
        fi
    else
        echo -e "${RED}数据恢复失败${NC}"
        return 1
    fi
  
    # 步骤6: 验证恢复
    echo -e "${YELLOW}验证恢复结果:${NC}"
    echo -e "恢复路径: $restore_path"
    echo -e "内容预览:"
    ls -la "$restore_path" | head -5
    echo -e "大小: $(du -sh "$restore_path")"
  
    echo -e "${GREEN}恢复完成!${NC}"
}

# 查看备份
function view_backups() {
    show_header
    echo -e "${CYAN}${BOLD}查看现有备份${NC}"
    echo -e "==============================================================="
  
    # 获取备份根目录
    read -p "请输入备份存储路径: " backup_root
  
    if [ -z "$backup_root" ]; then
        echo -e "${RED}备份路径不能为空${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 检查备份目录
    if [ ! -d "$backup_root" ]; then
        echo -e "${YELLOW}备份目录不存在: $backup_root${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    echo -e "${GREEN}备份根目录: $backup_root${NC}"
    echo ""
  
    local found=0
    for container_dir in "$backup_root"/*/; do
        if [ -d "$container_dir" ]; then
            local container_name=$(basename "$container_dir")
            local backups=($(ls -t "$container_dir"/*.tar.gz 2>/dev/null))
        
            if [ ${#backups[@]} -gt 0 ]; then
                found=1
                echo -e "${GREEN}容器: $container_name${NC}"
                echo -e "${BLUE}备份目录: $container_dir${NC}"
            
                for backup in "${backups[@]}"; do
                    local size=$(du -h "$backup" | cut -f1)
                    local date=$(stat -c %y "$backup" | cut -d' ' -f1-2)
                    local full_path=$(realpath "$backup")
                    echo "  - 文件名: $(basename "$backup")"
                    echo "    完整路径: $full_path"
                    echo "    大小: $size | 时间: $date"
                    echo ""
                done
                echo ""
            fi
        fi
    done
  
    if [ $found -eq 0 ]; then
        echo -e "${YELLOW}未找到任何备份文件${NC}"
    fi
  
    read -p "按任意键返回..." -n 1 -s
    echo
}

# 自动备份函数
function auto_backup() {
    local container_name="wizserver"  # 固定备份为知笔记容器
    local backup_root="$AUTO_BACKUP_ROOT"
  
    echo -e "${CYAN}${BOLD}开始自动备份: $container_name${NC}"
    echo -e "${GREEN}备份存储路径: $backup_root${NC}"
  
    # 检查容器是否存在
    if ! docker inspect "$container_name" &>/dev/null; then
        echo -e "${RED}错误: 容器 $container_name 不存在${NC}"
        return 1
    fi
  
    # 创建备份目录
    mkdir -p "$backup_root" || {
        echo -e "${RED}无法创建备份目录: $backup_root${NC}"
        return 1
    }
  
    # 执行备份
    if backup_container "$container_name" "$backup_root"; then
        echo -e "${GREEN}自动备份完成!${NC}"
        return 0
    else
        echo -e "${RED}自动备份失败!${NC}"
        return 1
    fi
}

# 安装定时任务
function install_cron_**() {
    show_header
    echo -e "${CYAN}${BOLD}安装自动备份定时任务${NC}"
    echo -e "==============================================================="
  
    # 获取脚本绝对路径
    local script_path=$(realpath "$0")
    local cron_entry="0 22 * * * $script_path --auto-backup"
  
    # 检查是否已存在定时任务
    if crontab -l | grep -Fq "$script_path --auto-backup"; then
        echo -e "${YELLOW}定时任务已存在:${NC}"
        crontab -l | grep -F "$script_path --auto-backup"
        echo -e "${YELLOW}无需重复安装${NC}"
        read -p "按任意键返回..." -n 1 -s
        echo
        return
    fi
  
    # 添加定时任务
    (crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
  
    if [ $? -eq 0 ]; then
        echo -e "${GREEN}定时任务安装成功!${NC}"
        echo -e "定时任务内容:"
        echo -e "${BLUE}$cron_entry${NC}"
        echo -e "将在每天 22:00 (晚上10点) 自动备份为知笔记容器"
    else
        echo -e "${RED}定时任务安装失败${NC}"
    fi
  
    read -p "按任意键返回..." -n 1 -s
    echo
}

# 脚本入口
function main() {
    # 检查参数
    if [ "$1" = "--auto-backup" ]; then
        auto_backup
        exit $?
    fi
  
    if [ "$1" = "--install-cron" ]; then
        install_cron_**
        exit $?
    fi
  
    # 进入主菜单
    main_menu
}

# 确保脚本有执行权限
if [ -f "$0" ]; then
    main "$@"
else
    echo "错误: 脚本文件不存在"
    exit 1
fi

收藏
送赞
分享
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则