Files
vps-snapshot/vps-snapshot.sh
2026-02-02 10:39:42 +08:00

652 lines
21 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#===============================================================================
# VPS 快照备份脚本 v3.0
# 支持: Ubuntu, Debian, CentOS, Alpine
# 功能: 智能识别应用 + Docker迁移 + 数据备份 + Telegram通知
#===============================================================================
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"
LOG_FILE="/var/log/vps-snapshot.log"
print_banner() {
echo -e "${BLUE}"
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ VPS 快照备份脚本 v3.0 ║"
echo "║ 智能识别 + Docker迁移 + 数据备份 ║"
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}[ERROR] $1${NC}"; echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN] $1${NC}"; }
info() { echo -e "${CYAN}[INFO] $1${NC}"; }
#===============================================================================
# 系统检测
#===============================================================================
detect_os() {
if [ -f /etc/os-release ]; then . /etc/os-release; echo "$ID"
elif [ -f /etc/redhat-release ]; then echo "centos"
else echo "unknown"; fi
}
install_deps() {
local os=$(detect_os)
log "检测系统: $os"
case $os in
ubuntu|debian) apt-get update -qq && apt-get install -y -qq rsync sshpass curl tar gzip jq ;;
centos|rhel|fedora) yum install -y -q rsync sshpass curl tar gzip jq ;;
alpine) apk add --no-cache rsync sshpass curl tar gzip jq ;;
*) error "不支持的系统: $os"; exit 1 ;;
esac
}
#===============================================================================
# 智能识别应用
#===============================================================================
detect_apps() {
info "🔍 扫描已安装的应用..."
echo ""
local apps=()
# Docker
if command -v docker &>/dev/null && docker info &>/dev/null; then
local containers=$(docker ps -q | wc -l)
local images=$(docker images -q | wc -l)
echo -e " ${GREEN}${NC} Docker: $containers 个运行中容器, $images 个镜像"
apps+=("docker")
fi
# Docker Compose
if command -v docker-compose &>/dev/null || docker compose version &>/dev/null 2>&1; then
local compose_files=$(find /opt /root /home -name "docker-compose*.yml" -o -name "compose*.yml" 2>/dev/null | wc -l)
echo -e " ${GREEN}${NC} Docker Compose: $compose_files 个配置文件"
apps+=("compose")
fi
# Nginx
if command -v nginx &>/dev/null || [ -d /etc/nginx ]; then
echo -e " ${GREEN}${NC} Nginx"
apps+=("nginx")
fi
# MySQL/MariaDB
if command -v mysql &>/dev/null || [ -d /var/lib/mysql ]; then
echo -e " ${GREEN}${NC} MySQL/MariaDB"
apps+=("mysql")
fi
# PostgreSQL
if command -v psql &>/dev/null || [ -d /var/lib/postgresql ]; then
echo -e " ${GREEN}${NC} PostgreSQL"
apps+=("postgresql")
fi
# Redis
if command -v redis-cli &>/dev/null || [ -d /var/lib/redis ]; then
echo -e " ${GREEN}${NC} Redis"
apps+=("redis")
fi
# MongoDB
if command -v mongod &>/dev/null || [ -d /var/lib/mongodb ]; then
echo -e " ${GREEN}${NC} MongoDB"
apps+=("mongodb")
fi
# Node.js/NPM
if command -v node &>/dev/null; then
local node_ver=$(node -v 2>/dev/null || echo "unknown")
local npm_global=$(npm list -g --depth=0 2>/dev/null | wc -l)
echo -e " ${GREEN}${NC} Node.js $node_ver ($npm_global 个全局包)"
apps+=("nodejs")
fi
# PM2
if command -v pm2 &>/dev/null; then
local pm2_apps=$(pm2 jlist 2>/dev/null | jq length 2>/dev/null || echo "0")
echo -e " ${GREEN}${NC} PM2: $pm2_apps 个应用"
apps+=("pm2")
fi
# Python/Pip
if command -v python3 &>/dev/null; then
local py_ver=$(python3 --version 2>/dev/null | cut -d' ' -f2)
echo -e " ${GREEN}${NC} Python $py_ver"
apps+=("python")
fi
# PHP
if command -v php &>/dev/null; then
local php_ver=$(php -v 2>/dev/null | head -1 | cut -d' ' -f2)
echo -e " ${GREEN}${NC} PHP $php_ver"
apps+=("php")
fi
# 1Panel
if [ -d /opt/1panel ] || command -v 1pctl &>/dev/null; then
echo -e " ${GREEN}${NC} 1Panel"
apps+=("1panel")
fi
# 宝塔
if [ -d /www/server/panel ]; then
echo -e " ${GREEN}${NC} 宝塔面板"
apps+=("bt")
fi
echo ""
echo "${apps[@]}"
}
#===============================================================================
# Docker 专用迁移
#===============================================================================
docker_export() {
local output_dir="${1:-/var/snapshots}"
mkdir -p "$output_dir"
log "📦 导出 Docker 数据..."
# 导出所有镜像
local images=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>")
if [ -n "$images" ]; then
log "导出镜像..."
docker save $images | gzip > "$output_dir/docker-images.tar.gz"
info "镜像已保存: docker-images.tar.gz"
fi
# 导出容器配置
log "导出容器配置..."
docker ps -a --format '{{json .}}' > "$output_dir/docker-containers.json"
# 导出 volumes
local volumes=$(docker volume ls -q)
if [ -n "$volumes" ]; then
log "导出 Volumes..."
mkdir -p "$output_dir/volumes"
for vol in $volumes; do
docker run --rm -v "$vol:/data" -v "$output_dir/volumes:/backup" \
alpine tar czf "/backup/${vol}.tar.gz" -C /data . 2>/dev/null || true
done
info "Volumes 已保存"
fi
# 导出 docker-compose 文件
log "查找 docker-compose 文件..."
find /opt /root /home -name "docker-compose*.yml" -o -name "compose*.yml" 2>/dev/null | while read f; do
local dir=$(dirname "$f")
local name=$(echo "$dir" | tr '/' '_')
mkdir -p "$output_dir/compose"
cp -r "$dir" "$output_dir/compose/$name" 2>/dev/null || true
done
log "Docker 数据导出完成"
}
docker_import() {
local input_dir="${1:-/var/snapshots}"
log "📥 导入 Docker 数据..."
# 检查 Docker 是否安装
if ! command -v docker &>/dev/null; then
warn "Docker 未安装,正在安装..."
curl -fsSL https://get.docker.com | sh
systemctl start docker
systemctl enable docker
fi
# 导入镜像
if [ -f "$input_dir/docker-images.tar.gz" ]; then
log "导入镜像..."
gunzip -c "$input_dir/docker-images.tar.gz" | docker load
info "镜像导入完成"
fi
# 恢复 volumes
if [ -d "$input_dir/volumes" ]; then
log "恢复 Volumes..."
for vol_file in "$input_dir/volumes"/*.tar.gz; do
[ -f "$vol_file" ] || continue
local vol_name=$(basename "$vol_file" .tar.gz)
docker volume create "$vol_name" 2>/dev/null || true
docker run --rm -v "$vol_name:/data" -v "$input_dir/volumes:/backup" \
alpine tar xzf "/backup/${vol_name}.tar.gz" -C /data 2>/dev/null || true
done
info "Volumes 恢复完成"
fi
# 恢复 compose 项目
if [ -d "$input_dir/compose" ]; then
log "恢复 Compose 项目..."
for proj in "$input_dir/compose"/*; do
[ -d "$proj" ] || continue
local name=$(basename "$proj")
local target="/opt/${name//_//}"
mkdir -p "$(dirname "$target")"
cp -r "$proj" "$target" 2>/dev/null || true
done
info "Compose 项目已恢复到 /opt"
fi
log "Docker 数据导入完成"
}
#===============================================================================
# 应用数据备份
#===============================================================================
backup_app_data() {
local output_dir="${1:-/var/snapshots}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_file="$output_dir/app-data_${timestamp}.tar.gz"
mkdir -p "$output_dir"
log "📦 备份应用数据..."
local backup_paths=""
# Nginx
[ -d /etc/nginx ] && backup_paths+=" /etc/nginx"
[ -d /var/www ] && backup_paths+=" /var/www"
# MySQL
if [ -d /var/lib/mysql ]; then
log "导出 MySQL 数据库..."
mkdir -p "$output_dir/mysql"
if command -v mysqldump &>/dev/null; then
mysqldump --all-databases > "$output_dir/mysql/all-databases.sql" 2>/dev/null || true
fi
fi
# PostgreSQL
if [ -d /var/lib/postgresql ]; then
log "导出 PostgreSQL 数据库..."
mkdir -p "$output_dir/postgresql"
if command -v pg_dumpall &>/dev/null; then
sudo -u postgres pg_dumpall > "$output_dir/postgresql/all-databases.sql" 2>/dev/null || true
fi
fi
# Redis
[ -d /var/lib/redis ] && backup_paths+=" /var/lib/redis"
# MongoDB
if [ -d /var/lib/mongodb ]; then
log "导出 MongoDB..."
mkdir -p "$output_dir/mongodb"
if command -v mongodump &>/dev/null; then
mongodump --out "$output_dir/mongodb" 2>/dev/null || true
fi
fi
# Node.js/NPM 全局包
if command -v npm &>/dev/null; then
log "导出 NPM 全局包列表..."
npm list -g --depth=0 --json > "$output_dir/npm-global.json" 2>/dev/null || true
fi
# PM2 应用
if command -v pm2 &>/dev/null; then
log "导出 PM2 配置..."
pm2 save 2>/dev/null || true
[ -f ~/.pm2/dump.pm2 ] && cp ~/.pm2/dump.pm2 "$output_dir/pm2-dump.json"
fi
# Python pip
if command -v pip3 &>/dev/null; then
log "导出 pip 包列表..."
pip3 freeze > "$output_dir/pip-requirements.txt" 2>/dev/null || true
fi
# 1Panel
[ -d /opt/1panel ] && backup_paths+=" /opt/1panel"
# 宝塔
[ -d /www ] && backup_paths+=" /www"
# 通用数据目录
[ -d /opt ] && backup_paths+=" /opt"
[ -d /home ] && backup_paths+=" /home"
[ -d /root/.config ] && backup_paths+=" /root/.config"
# 打包
if [ -n "$backup_paths" ]; then
log "打包数据目录..."
tar --exclude='*.sock' --exclude='*.pid' --exclude='node_modules' \
--exclude='.npm' --exclude='.cache' --exclude='__pycache__' \
-czf "$backup_file" $backup_paths 2>/dev/null || true
info "数据已保存: $backup_file"
fi
echo "$backup_file"
}
#===============================================================================
# 一键迁移
#===============================================================================
do_migrate() {
print_banner
info "🚀 一键迁移向导"
echo ""
# 检测应用
local apps_output=$(detect_apps)
local apps=$(echo "$apps_output" | tail -1)
echo ""
read -p "输入目标服务器 IP: " target_ip
read -p "输入目标服务器端口 [22]: " target_port
target_port=${target_port:-22}
read -p "输入目标服务器用户 [root]: " target_user
target_user=${target_user:-root}
read -s -p "输入目标服务器密码: " target_pass
echo ""
# 测试连接
log "测试连接..."
if ! sshpass -p "$target_pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
-p "$target_port" "$target_user@$target_ip" "echo ok" &>/dev/null; then
error "无法连接到目标服务器"
return 1
fi
info "连接成功"
local tmp_dir="/tmp/migrate_$$"
mkdir -p "$tmp_dir"
# Docker 迁移
if [[ " $apps " =~ " docker " ]]; then
log "检测到 Docker开始导出..."
docker_export "$tmp_dir"
fi
# 应用数据备份
log "备份应用数据..."
backup_app_data "$tmp_dir"
# 打包所有数据
log "打包迁移数据..."
local migrate_file="/tmp/migrate_data.tar.gz"
tar -czf "$migrate_file" -C "$tmp_dir" . 2>/dev/null
local size=$(du -h "$migrate_file" | cut -f1)
info "迁移包大小: $size"
# 传输到目标服务器
log "传输到目标服务器..."
sshpass -p "$target_pass" rsync -avz --progress \
-e "ssh -o StrictHostKeyChecking=no -p $target_port" \
"$migrate_file" "$target_user@$target_ip:/tmp/"
# 在目标服务器上恢复
log "在目标服务器上恢复..."
sshpass -p "$target_pass" ssh -o StrictHostKeyChecking=no \
-p "$target_port" "$target_user@$target_ip" bash << 'REMOTE_SCRIPT'
set -e
cd /tmp
mkdir -p /tmp/restore_data
tar -xzf migrate_data.tar.gz -C /tmp/restore_data
# 安装 Docker
if [ -f /tmp/restore_data/docker-images.tar.gz ]; then
if ! command -v docker &>/dev/null; then
echo "安装 Docker..."
curl -fsSL https://get.docker.com | sh
systemctl start docker
systemctl enable docker
fi
echo "导入 Docker 镜像..."
gunzip -c /tmp/restore_data/docker-images.tar.gz | docker load
fi
# 恢复 volumes
if [ -d /tmp/restore_data/volumes ]; then
echo "恢复 Docker Volumes..."
for vol_file in /tmp/restore_data/volumes/*.tar.gz; do
[ -f "$vol_file" ] || continue
vol_name=$(basename "$vol_file" .tar.gz)
docker volume create "$vol_name" 2>/dev/null || true
docker run --rm -v "$vol_name:/data" -v "/tmp/restore_data/volumes:/backup" \
alpine tar xzf "/backup/${vol_name}.tar.gz" -C /data 2>/dev/null || true
done
fi
# 恢复应用数据
if ls /tmp/restore_data/app-data_*.tar.gz &>/dev/null; then
echo "恢复应用数据..."
tar -xzf /tmp/restore_data/app-data_*.tar.gz -C / 2>/dev/null || true
fi
# 恢复 NPM 全局包
if [ -f /tmp/restore_data/npm-global.json ] && command -v npm &>/dev/null; then
echo "恢复 NPM 全局包..."
cat /tmp/restore_data/npm-global.json | jq -r '.dependencies | keys[]' | \
xargs -I {} npm install -g {} 2>/dev/null || true
fi
# 恢复 pip 包
if [ -f /tmp/restore_data/pip-requirements.txt ] && command -v pip3 &>/dev/null; then
echo "恢复 pip 包..."
pip3 install -r /tmp/restore_data/pip-requirements.txt 2>/dev/null || true
fi
# 清理
rm -rf /tmp/restore_data /tmp/migrate_data.tar.gz
echo "✅ 迁移完成"
REMOTE_SCRIPT
# 清理本地临时文件
rm -rf "$tmp_dir" "$migrate_file"
log "🎉 迁移完成!"
echo ""
info "请登录目标服务器检查服务状态"
read -p "是否关闭当前服务器? (输入 '确认关闭' 执行): " confirm
if [ "$confirm" = "确认关闭" ]; then
warn "服务器将在 10 秒后关机..."
sleep 10
shutdown -h now
fi
}
#===============================================================================
# 快照备份
#===============================================================================
create_snapshot() {
local output_dir="${1:-/var/snapshots}"
local name="${2:-snapshot}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
local snapshot_file="$output_dir/${name}_${timestamp}.tar.gz"
mkdir -p "$output_dir"
log "📸 创建快照..."
# 检测应用并备份
detect_apps > /dev/null
# Docker 数据
if command -v docker &>/dev/null && docker info &>/dev/null; then
docker_export "$output_dir/docker_$timestamp"
fi
# 应用数据
backup_app_data "$output_dir"
# 打包
tar -czf "$snapshot_file" -C "$output_dir" . --exclude="*.tar.gz" 2>/dev/null || true
# 清理临时文件
rm -rf "$output_dir/docker_$timestamp" "$output_dir/mysql" "$output_dir/postgresql" "$output_dir/mongodb"
local size=$(du -h "$snapshot_file" | cut -f1)
log "快照已创建: $snapshot_file ($size)"
echo "$snapshot_file"
}
#===============================================================================
# Telegram 通知
#===============================================================================
send_tg() {
[ -z "$TG_BOT_TOKEN" ] || [ -z "$TG_CHAT_ID" ] && return
local msg="$1"
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="$TG_CHAT_ID" -d text="$msg" -d parse_mode="Markdown" > /dev/null
}
#===============================================================================
# 配置管理
#===============================================================================
load_config() {
[ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"
}
save_config() {
cat > "$CONFIG_FILE" << EOF
VPS_NAME="$VPS_NAME"
TG_BOT_TOKEN="$TG_BOT_TOKEN"
TG_CHAT_ID="$TG_CHAT_ID"
REMOTE_IP="$REMOTE_IP"
REMOTE_PORT="$REMOTE_PORT"
REMOTE_USER="$REMOTE_USER"
REMOTE_PASS="$REMOTE_PASS"
REMOTE_DIR="$REMOTE_DIR"
LOCAL_DIR="$LOCAL_DIR"
EOF
chmod 600 "$CONFIG_FILE"
}
do_setup() {
print_banner
info "⚙️ 配置向导"
echo ""
read -p "VPS 名称 [$(hostname)]: " VPS_NAME
VPS_NAME=${VPS_NAME:-$(hostname)}
read -p "本地快照目录 [/var/snapshots]: " LOCAL_DIR
LOCAL_DIR=${LOCAL_DIR:-/var/snapshots}
echo ""
info "远程备份配置 (可选,留空跳过)"
read -p "远程服务器 IP: " REMOTE_IP
if [ -n "$REMOTE_IP" ]; then
read -p "远程端口 [22]: " REMOTE_PORT
REMOTE_PORT=${REMOTE_PORT:-22}
read -p "远程用户 [root]: " REMOTE_USER
REMOTE_USER=${REMOTE_USER:-root}
read -s -p "远程密码: " REMOTE_PASS
echo ""
read -p "远程目录 [/backup/$VPS_NAME]: " REMOTE_DIR
REMOTE_DIR=${REMOTE_DIR:-/backup/$VPS_NAME}
fi
echo ""
info "Telegram 通知 (可选,留空跳过)"
read -p "Bot Token: " TG_BOT_TOKEN
[ -n "$TG_BOT_TOKEN" ] && read -p "Chat ID: " TG_CHAT_ID
save_config
log "配置已保存到 $CONFIG_FILE"
}
#===============================================================================
# 主菜单
#===============================================================================
show_menu() {
print_banner
load_config 2>/dev/null || true
echo -e "${CYAN}当前配置:${NC} ${VPS_NAME:-未配置}"
echo ""
echo " 1) 首次配置 / 重新配置"
echo " 2) 扫描已安装应用"
echo " 3) 创建快照备份"
echo " 4) 一键迁移到新服务器"
echo " 5) 导出 Docker 数据"
echo " 6) 导入 Docker 数据"
echo " 7) 查看本地快照"
echo " 8) 安装依赖"
echo " 0) 退出"
echo ""
read -p "请选择 [0-8]: " choice
case $choice in
1) do_setup ;;
2) detect_apps ;;
3)
load_config
create_snapshot "$LOCAL_DIR" "$VPS_NAME"
;;
4) do_migrate ;;
5)
read -p "输出目录 [/var/snapshots]: " dir
docker_export "${dir:-/var/snapshots}"
;;
6)
read -p "输入目录 [/var/snapshots]: " dir
docker_import "${dir:-/var/snapshots}"
;;
7)
load_config
echo ""
ls -lh "${LOCAL_DIR:-/var/snapshots}" 2>/dev/null || echo "无快照"
;;
8) install_deps ;;
0) exit 0 ;;
*) error "无效选项" ;;
esac
echo ""
read -p "按回车继续..."
show_menu
}
#===============================================================================
# 入口
#===============================================================================
case "${1:-}" in
setup) do_setup ;;
scan) detect_apps ;;
snapshot) create_snapshot "${2:-/var/snapshots}" "${3:-snapshot}" ;;
migrate) do_migrate ;;
docker-export) docker_export "${2:-/var/snapshots}" ;;
docker-import) docker_import "${2:-/var/snapshots}" ;;
help|--help|-h)
echo "用法: $0 [命令]"
echo ""
echo "命令:"
echo " setup 配置向导"
echo " scan 扫描已安装应用"
echo " snapshot 创建快照"
echo " migrate 一键迁移"
echo " docker-export 导出 Docker"
echo " docker-import 导入 Docker"
echo ""
echo "无参数运行显示交互菜单"
;;
"") show_menu ;;
*) error "未知命令: $1"; exit 1 ;;
esac