添加生产备份恢复脚本
This commit is contained in:
@@ -25,5 +25,6 @@ dist/
|
|||||||
|
|
||||||
# Docker / local data
|
# Docker / local data
|
||||||
.docker-data/
|
.docker-data/
|
||||||
|
backups/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
backend/api
|
backend/api
|
||||||
|
|||||||
Executable
+501
@@ -0,0 +1,501 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.prod.yml"
|
||||||
|
BACKEND_ENV="${ROOT_DIR}/backend/.env"
|
||||||
|
BACKUP_ROOT="${BACKUP_ROOT:-${ROOT_DIR}/backups}"
|
||||||
|
HELPER_IMAGE="${BACKUP_HELPER_IMAGE:-redis:7.4-alpine}"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[1;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf "${BLUE}[backup]${NC} %s\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
printf "${GREEN}[backup]${NC} %s\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
printf "${YELLOW}[backup]${NC} %s\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
printf "${RED}[backup]${NC} %s\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
log_error "缺少命令:$1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
用法: ./scripts/backup-prod.sh <命令> [选项]
|
||||||
|
|
||||||
|
命令:
|
||||||
|
backup 创建完整生产备份
|
||||||
|
restore <备份目录> 从备份目录恢复生产数据(需要 --yes)
|
||||||
|
list 列出本机备份目录
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--yes 确认执行破坏性恢复,只用于 restore
|
||||||
|
--no-stop backup 时不主动停止 backend/caddy,适合只读低风险窗口(不推荐)
|
||||||
|
-h, --help 显示帮助
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
BACKUP_ROOT=/path 备份根目录,默认 ./backups
|
||||||
|
BACKUP_HELPER_IMAGE=img 用于打包/还原 Docker volume 的镜像,默认 redis:7.4-alpine
|
||||||
|
|
||||||
|
备份内容:
|
||||||
|
- MySQL 逻辑备份 mysql.sql
|
||||||
|
- MinIO volume: minio_data.tgz
|
||||||
|
- Redis volume: redis_data.tgz
|
||||||
|
- Caddy 证书/配置 volume: caddy_data.tgz, caddy_config.tgz
|
||||||
|
- backend/.env、deploy 配置、backend/logs
|
||||||
|
- 当前 Git commit、Docker Compose 状态、goose 迁移状态
|
||||||
|
|
||||||
|
示例:
|
||||||
|
./scripts/backup-prod.sh backup
|
||||||
|
./scripts/backup-prod.sh list
|
||||||
|
./scripts/backup-prod.sh restore backups/20260618_153000 --yes
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
local caddy_domain caddy_email admin_base_path
|
||||||
|
caddy_domain="${CADDY_DOMAIN:-$(env_value CADDY_DOMAIN)}"
|
||||||
|
caddy_email="${CADDY_EMAIL:-$(env_value CADDY_EMAIL)}"
|
||||||
|
admin_base_path="${VITE_ADMIN_BASE_PATH:-$(env_value VITE_ADMIN_BASE_PATH)}"
|
||||||
|
|
||||||
|
CADDY_DOMAIN="${caddy_domain:-backup.localhost}" \
|
||||||
|
CADDY_EMAIL="${caddy_email:-backup@example.com}" \
|
||||||
|
VITE_ADMIN_BASE_PATH="${admin_base_path:-/manage-7x9k2p}" \
|
||||||
|
docker compose --env-file "${BACKEND_ENV}" -f "${COMPOSE_FILE}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env_file() {
|
||||||
|
if [[ ! -f "${BACKEND_ENV}" ]]; then
|
||||||
|
log_error "未找到 backend/.env,无法读取生产环境变量"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
env_value() {
|
||||||
|
local key="$1"
|
||||||
|
awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "${BACKEND_ENV}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local key="$1"
|
||||||
|
local value
|
||||||
|
value="$(env_value "${key}")"
|
||||||
|
if [[ -z "${value}" ]]; then
|
||||||
|
log_error "backend/.env 缺少或未填写 ${key}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "%s" "${value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
container_id() {
|
||||||
|
local service="$1"
|
||||||
|
local cid
|
||||||
|
cid="$(compose ps -a -q "${service}" 2>/dev/null || true)"
|
||||||
|
if [[ -z "${cid}" ]]; then
|
||||||
|
log "创建 ${service} 容器以挂载 volume..."
|
||||||
|
compose create "${service}" >/dev/null
|
||||||
|
cid="$(compose ps -a -q "${service}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "${cid}" ]]; then
|
||||||
|
log_error "无法找到 ${service} 容器"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "%s" "${cid}"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_was_running() {
|
||||||
|
local service="$1"
|
||||||
|
local cid status
|
||||||
|
cid="$(compose ps -q "${service}" 2>/dev/null || true)"
|
||||||
|
if [[ -z "${cid}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
status="$(docker inspect -f '{{.State.Status}}' "${cid}" 2>/dev/null || true)"
|
||||||
|
[[ "${status}" == "running" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
start_services() {
|
||||||
|
if [[ "$#" -eq 0 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "启动服务:$*"
|
||||||
|
compose up -d "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_services() {
|
||||||
|
if [[ "$#" -eq 0 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "停止服务:$*"
|
||||||
|
compose stop "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_recorded_services() {
|
||||||
|
if [[ "$#" -eq 0 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
start_services "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_service_ready() {
|
||||||
|
local service="$1"
|
||||||
|
local label="$2"
|
||||||
|
local timeout="${3:-120}"
|
||||||
|
local attempt cid status
|
||||||
|
|
||||||
|
log "等待 ${label} 就绪..."
|
||||||
|
for ((attempt = 1; attempt <= timeout; attempt++)); do
|
||||||
|
cid="$(compose ps -q "${service}" 2>/dev/null || true)"
|
||||||
|
if [[ -n "${cid}" ]]; then
|
||||||
|
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${cid}" 2>/dev/null || true)"
|
||||||
|
if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
|
||||||
|
log_success "${label} 已就绪"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
log_error "${label} 启动超时"
|
||||||
|
compose logs --tail=80 "${service}" || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remember_running() {
|
||||||
|
local service="$1"
|
||||||
|
local result_array_name="$2"
|
||||||
|
if service_was_running "${service}"; then
|
||||||
|
eval "${result_array_name}+=(\"${service}\")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tar_from_service_volume() {
|
||||||
|
local service="$1"
|
||||||
|
local source_dir="$2"
|
||||||
|
local backup_dir="$3"
|
||||||
|
local output_name="$4"
|
||||||
|
local cid
|
||||||
|
cid="$(container_id "${service}")"
|
||||||
|
log "备份 ${service}:${source_dir} -> ${output_name}"
|
||||||
|
docker run --rm \
|
||||||
|
--volumes-from "${cid}:ro" \
|
||||||
|
-v "${backup_dir}:/backup" \
|
||||||
|
"${HELPER_IMAGE}" \
|
||||||
|
sh -c "cd '${source_dir}' && tar czf '/backup/${output_name}' ."
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_service_volume() {
|
||||||
|
local service="$1"
|
||||||
|
local target_dir="$2"
|
||||||
|
local backup_dir="$3"
|
||||||
|
local input_name="$4"
|
||||||
|
local cid
|
||||||
|
if [[ ! -f "${backup_dir}/${input_name}" ]]; then
|
||||||
|
log_warn "跳过 ${service}:${target_dir},备份文件不存在:${input_name}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
cid="$(container_id "${service}")"
|
||||||
|
log "还原 ${input_name} -> ${service}:${target_dir}"
|
||||||
|
docker run --rm \
|
||||||
|
--volumes-from "${cid}" \
|
||||||
|
-v "${backup_dir}:/backup:ro" \
|
||||||
|
"${HELPER_IMAGE}" \
|
||||||
|
sh -c "rm -rf '${target_dir}'/* '${target_dir}'/.[!.]* '${target_dir}'/..?* 2>/dev/null || true; tar xzf '/backup/${input_name}' -C '${target_dir}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_if_exists() {
|
||||||
|
local source="$1"
|
||||||
|
local target="$2"
|
||||||
|
if [[ -e "${source}" ]]; then
|
||||||
|
cp -a "${source}" "${target}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_manifest() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
{
|
||||||
|
printf "created_at=%s\n" "$(date -Iseconds)"
|
||||||
|
printf "host=%s\n" "$(hostname)"
|
||||||
|
printf "git_commit=%s\n" "$(git -C "${ROOT_DIR}" rev-parse HEAD 2>/dev/null || true)"
|
||||||
|
printf "git_branch=%s\n" "$(git -C "${ROOT_DIR}" branch --show-current 2>/dev/null || true)"
|
||||||
|
printf "mysql_database=%s\n" "$(require_env MYSQL_DATABASE)"
|
||||||
|
printf "storage_bucket=%s\n" "$(require_env STORAGE_BUCKET)"
|
||||||
|
} > "${backup_dir}/manifest.env"
|
||||||
|
|
||||||
|
git -C "${ROOT_DIR}" log --oneline -5 > "${backup_dir}/git-log.txt" 2>/dev/null || true
|
||||||
|
compose ps > "${backup_dir}/docker-compose-ps.txt" 2>/dev/null || true
|
||||||
|
compose config > "${backup_dir}/docker-compose.config.rendered.yml" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_mysql() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
log "备份 MySQL -> mysql.sql"
|
||||||
|
compose exec -T mysql sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump -uroot --single-transaction --routines --triggers --events --hex-blob --default-character-set=utf8mb4 "$MYSQL_DATABASE"' \
|
||||||
|
> "${backup_dir}/mysql.sql"
|
||||||
|
compose exec -T mysql sh -c 'mysql --version' > "${backup_dir}/mysql-version.txt" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_goose_status() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
log "记录 goose 迁移状态"
|
||||||
|
compose run --rm --no-deps backend /app/goose -dir /app/migrations mysql "$(goose_dsn "$(require_env MYSQL_DSN)")" status \
|
||||||
|
> "${backup_dir}/goose-status.txt" 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
goose_dsn() {
|
||||||
|
local dsn="$1"
|
||||||
|
if [[ "${dsn}" != *"multiStatements="* ]]; then
|
||||||
|
if [[ "${dsn}" == *"?"* ]]; then
|
||||||
|
dsn="${dsn}&multiStatements=true"
|
||||||
|
else
|
||||||
|
dsn="${dsn}?multiStatements=true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf "%s" "${dsn}"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_files() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
log "备份配置文件"
|
||||||
|
mkdir -p "${backup_dir}/config"
|
||||||
|
copy_if_exists "${BACKEND_ENV}" "${backup_dir}/config/backend.env"
|
||||||
|
copy_if_exists "${COMPOSE_FILE}" "${backup_dir}/config/docker-compose.prod.yml"
|
||||||
|
copy_if_exists "${ROOT_DIR}/deploy/caddy/Caddyfile" "${backup_dir}/config/Caddyfile"
|
||||||
|
copy_if_exists "${ROOT_DIR}/deploy/mysql/my.cnf" "${backup_dir}/config/mysql.my.cnf"
|
||||||
|
|
||||||
|
if [[ -d "${ROOT_DIR}/backend/logs" ]]; then
|
||||||
|
log "备份后端日志 -> backend_logs.tgz"
|
||||||
|
tar czf "${backup_dir}/backend_logs.tgz" -C "${ROOT_DIR}/backend" logs
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_files() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
local safety_dir="${ROOT_DIR}/backups/pre-restore-files-$(date +%Y%m%d_%H%M%S)"
|
||||||
|
mkdir -p "${safety_dir}"
|
||||||
|
|
||||||
|
log "恢复配置文件(当前文件会先备份到 ${safety_dir})"
|
||||||
|
copy_if_exists "${BACKEND_ENV}" "${safety_dir}/backend.env"
|
||||||
|
copy_if_exists "${COMPOSE_FILE}" "${safety_dir}/docker-compose.prod.yml"
|
||||||
|
copy_if_exists "${ROOT_DIR}/deploy/caddy/Caddyfile" "${safety_dir}/Caddyfile"
|
||||||
|
copy_if_exists "${ROOT_DIR}/deploy/mysql/my.cnf" "${safety_dir}/mysql.my.cnf"
|
||||||
|
|
||||||
|
copy_if_exists "${backup_dir}/config/backend.env" "${BACKEND_ENV}"
|
||||||
|
copy_if_exists "${backup_dir}/config/docker-compose.prod.yml" "${COMPOSE_FILE}"
|
||||||
|
copy_if_exists "${backup_dir}/config/Caddyfile" "${ROOT_DIR}/deploy/caddy/Caddyfile"
|
||||||
|
copy_if_exists "${backup_dir}/config/mysql.my.cnf" "${ROOT_DIR}/deploy/mysql/my.cnf"
|
||||||
|
|
||||||
|
if [[ -f "${backup_dir}/backend_logs.tgz" ]]; then
|
||||||
|
log "恢复后端日志"
|
||||||
|
mkdir -p "${ROOT_DIR}/backend"
|
||||||
|
rm -rf "${ROOT_DIR}/backend/logs"
|
||||||
|
tar xzf "${backup_dir}/backend_logs.tgz" -C "${ROOT_DIR}/backend"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_restore_env() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
if [[ ! -f "${BACKEND_ENV}" ]]; then
|
||||||
|
if [[ ! -f "${backup_dir}/config/backend.env" ]]; then
|
||||||
|
log_error "当前缺少 backend/.env,备份中也没有 config/backend.env,无法恢复"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_warn "当前缺少 backend/.env,先从备份恢复环境变量文件"
|
||||||
|
mkdir -p "$(dirname "${BACKEND_ENV}")"
|
||||||
|
cp -a "${backup_dir}/config/backend.env" "${BACKEND_ENV}"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "${COMPOSE_FILE}" && -f "${backup_dir}/config/docker-compose.prod.yml" ]]; then
|
||||||
|
log_warn "当前缺少 deploy/docker-compose.prod.yml,先从备份恢复 Compose 文件"
|
||||||
|
mkdir -p "$(dirname "${COMPOSE_FILE}")"
|
||||||
|
cp -a "${backup_dir}/config/docker-compose.prod.yml" "${COMPOSE_FILE}"
|
||||||
|
fi
|
||||||
|
require_env_file
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_mysql() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
local database
|
||||||
|
database="$(require_env MYSQL_DATABASE)"
|
||||||
|
if [[ ! "${database}" =~ ^[A-Za-z0-9_]+$ ]]; then
|
||||||
|
log_error "MYSQL_DATABASE 只能包含字母、数字和下划线,当前值:${database}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "${backup_dir}/mysql.sql" ]]; then
|
||||||
|
log_error "缺少 MySQL 备份文件:${backup_dir}/mysql.sql"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
start_services mysql
|
||||||
|
wait_service_ready mysql "MySQL"
|
||||||
|
log "重建数据库 ${database}"
|
||||||
|
compose exec -T mysql sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot -e "DROP DATABASE IF EXISTS \`$MYSQL_DATABASE\`; CREATE DATABASE \`$MYSQL_DATABASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"'
|
||||||
|
log "导入 MySQL 备份"
|
||||||
|
compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot "$MYSQL_DATABASE"' < "${backup_dir}/mysql.sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_all() {
|
||||||
|
require_env_file
|
||||||
|
need_cmd docker
|
||||||
|
local no_stop="$1"
|
||||||
|
local backup_dir="${BACKUP_ROOT}/$(date +%Y%m%d_%H%M%S)"
|
||||||
|
local restart_services=()
|
||||||
|
local backup_ok=0
|
||||||
|
|
||||||
|
backup_failure_cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
trap - ERR
|
||||||
|
if [[ "${no_stop}" == "0" && "${backup_ok}" != "1" ]]; then
|
||||||
|
log_warn "备份失败,尝试恢复备份前运行的服务"
|
||||||
|
start_recorded_services "${restart_services[@]+"${restart_services[@]}"}" || true
|
||||||
|
fi
|
||||||
|
exit "${exit_code}"
|
||||||
|
}
|
||||||
|
trap backup_failure_cleanup ERR
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
mkdir -p "${backup_dir}"
|
||||||
|
chmod 700 "${backup_dir}"
|
||||||
|
log "备份目录:${backup_dir}"
|
||||||
|
|
||||||
|
if [[ "${no_stop}" == "0" ]]; then
|
||||||
|
remember_running caddy restart_services
|
||||||
|
remember_running backend restart_services
|
||||||
|
stop_services caddy backend
|
||||||
|
else
|
||||||
|
log_warn "已开启 --no-stop,备份期间仍可能有写入,建议只在低风险窗口使用"
|
||||||
|
fi
|
||||||
|
|
||||||
|
start_services mysql
|
||||||
|
wait_service_ready mysql "MySQL"
|
||||||
|
backup_mysql "${backup_dir}"
|
||||||
|
backup_goose_status "${backup_dir}"
|
||||||
|
|
||||||
|
if [[ "${no_stop}" == "0" ]]; then
|
||||||
|
remember_running minio restart_services
|
||||||
|
remember_running redis restart_services
|
||||||
|
stop_services minio redis caddy
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar_from_service_volume minio /data "${backup_dir}" minio_data.tgz
|
||||||
|
tar_from_service_volume redis /data "${backup_dir}" redis_data.tgz
|
||||||
|
tar_from_service_volume caddy /data "${backup_dir}" caddy_data.tgz
|
||||||
|
tar_from_service_volume caddy /config "${backup_dir}" caddy_config.tgz
|
||||||
|
backup_files "${backup_dir}"
|
||||||
|
write_manifest "${backup_dir}"
|
||||||
|
|
||||||
|
if [[ "${no_stop}" == "0" ]]; then
|
||||||
|
start_recorded_services "${restart_services[@]+"${restart_services[@]}"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_ok=1
|
||||||
|
trap - ERR
|
||||||
|
log_success "备份完成:${backup_dir}"
|
||||||
|
log_warn "备份包含生产密钥,请离线加密保存,不要提交到 Git。"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_all() {
|
||||||
|
need_cmd docker
|
||||||
|
local backup_dir="$1"
|
||||||
|
local yes="$2"
|
||||||
|
|
||||||
|
if [[ -z "${backup_dir}" || ! -d "${backup_dir}" ]]; then
|
||||||
|
log_error "备份目录不存在:${backup_dir}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "${yes}" != "1" ]]; then
|
||||||
|
log_error "restore 会覆盖当前生产数据库、MinIO、Redis、Caddy volume 和配置文件。确认执行请追加 --yes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
prepare_restore_env "${backup_dir}"
|
||||||
|
log_warn "即将从 ${backup_dir} 恢复,当前生产数据会被覆盖。"
|
||||||
|
stop_services backend caddy minio redis
|
||||||
|
restore_files "${backup_dir}"
|
||||||
|
restore_mysql "${backup_dir}"
|
||||||
|
|
||||||
|
restore_service_volume minio /data "${backup_dir}" minio_data.tgz
|
||||||
|
restore_service_volume redis /data "${backup_dir}" redis_data.tgz
|
||||||
|
restore_service_volume caddy /data "${backup_dir}" caddy_data.tgz
|
||||||
|
restore_service_volume caddy /config "${backup_dir}" caddy_config.tgz
|
||||||
|
|
||||||
|
log "启动生产服务"
|
||||||
|
compose up -d
|
||||||
|
log_success "恢复完成。建议立刻执行健康检查并抽查图片/二维码/订单数据。"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_backups() {
|
||||||
|
if [[ ! -d "${BACKUP_ROOT}" ]]; then
|
||||||
|
log_warn "备份目录不存在:${BACKUP_ROOT}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local command="${1:-}"
|
||||||
|
local no_stop=0
|
||||||
|
local yes=0
|
||||||
|
local restore_dir=""
|
||||||
|
|
||||||
|
if [[ -z "${command}" || "${command}" == "-h" || "${command}" == "--help" ]]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "${command}" in
|
||||||
|
backup)
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--no-stop) no_stop=1; shift ;;
|
||||||
|
-h|--help) show_help; exit 0 ;;
|
||||||
|
*) log_error "未知选项:$1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
backup_all "${no_stop}"
|
||||||
|
;;
|
||||||
|
restore)
|
||||||
|
restore_dir="${1:-}"
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--yes) yes=1; shift ;;
|
||||||
|
-h|--help) show_help; exit 0 ;;
|
||||||
|
*) log_error "未知选项:$1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
restore_all "${restore_dir}" "${yes}"
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
list_backups
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "未知命令:${command}"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user