546 lines
17 KiB
Bash
546 lines
17 KiB
Bash
#!/usr/bin/env bash
|
||
# FLNXI customer backup, restore, and verify helper for NAS/Linux hosts.
|
||
|
||
set -euo pipefail
|
||
|
||
ACTION="backup"
|
||
PROJECT_DIR="."
|
||
COMPOSE_FILE="docker-compose.prod.yml"
|
||
ENV_FILE=".env"
|
||
BACKUP_DIR="backups"
|
||
BACKUP_PATH=""
|
||
APP_SERVICE="app"
|
||
POSTGRES_SERVICE="postgres"
|
||
MINIO_SERVICE="minio"
|
||
HELPER_IMAGE="busybox:1.36"
|
||
SKIP_STOP_APP_FOR_BACKUP=0
|
||
SKIP_DATABASE_BACKUP=0
|
||
SKIP_OBJECT_STORAGE_BACKUP=0
|
||
SKIP_OBJECT_STORAGE_RESTORE=0
|
||
RESTORE_ENV=0
|
||
HEALTH_TIMEOUT_SECONDS=120
|
||
CONFIRM_RESTORE=""
|
||
RESTORE_CONFIRM_PHRASE="RESTORE_FLNXI_DATA"
|
||
|
||
info() {
|
||
printf '[INFO] %s\n' "$1"
|
||
}
|
||
|
||
ok() {
|
||
printf '[OK] %s\n' "$1"
|
||
}
|
||
|
||
warn() {
|
||
printf '[WARN] %s\n' "$1" >&2
|
||
}
|
||
|
||
fail() {
|
||
printf '[ERROR] %s\n' "$1" >&2
|
||
exit 1
|
||
}
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
FLNXI NAS/Linux 备份、恢复和验收脚本
|
||
|
||
用法:
|
||
bash scripts/backup-restore.sh --action backup|restore|verify [选项]
|
||
|
||
常用选项:
|
||
--project-dir <path> 部署目录,默认当前目录
|
||
--compose-file <file> Compose 文件名,默认 docker-compose.prod.yml
|
||
--env-file <file> 环境文件名,默认 .env
|
||
--backup-dir <dir> 备份目录名,默认 backups
|
||
--backup-path <path> 恢复时指定备份目录
|
||
--helper-image <image> 备份/恢复 MinIO 卷用的小型镜像,默认 busybox:1.36
|
||
--skip-stop-app-for-backup 备份时不暂停 app 写入
|
||
--skip-database-backup 跳过数据库备份
|
||
--skip-object-storage-backup 跳过对象存储备份
|
||
--skip-object-storage-restore 恢复时跳过对象存储
|
||
--restore-env 恢复备份中的 .env
|
||
--confirm-restore RESTORE_FLNXI_DATA 恢复确认短语
|
||
--health-timeout-seconds <sec> 健康检查等待秒数,默认 120
|
||
-h, --help 显示帮助
|
||
|
||
说明:
|
||
备份包包含 .env、数据库和客户素材,必须限权或加密保存,不能提交到 Git。
|
||
如果当前账号没有 Docker 权限,请用 sudo 运行本脚本。
|
||
EOF
|
||
}
|
||
|
||
normalize_action() {
|
||
case "$1" in
|
||
backup|Backup) printf 'backup' ;;
|
||
restore|Restore) printf 'restore' ;;
|
||
verify|Verify) printf 'verify' ;;
|
||
*) fail "未知动作:$1" ;;
|
||
esac
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--action)
|
||
ACTION="$(normalize_action "${2:?--action requires a value}")"
|
||
shift 2
|
||
;;
|
||
--project-dir)
|
||
PROJECT_DIR="${2:?--project-dir requires a value}"
|
||
shift 2
|
||
;;
|
||
--compose-file)
|
||
COMPOSE_FILE="${2:?--compose-file requires a value}"
|
||
shift 2
|
||
;;
|
||
--env-file)
|
||
ENV_FILE="${2:?--env-file requires a value}"
|
||
shift 2
|
||
;;
|
||
--backup-dir)
|
||
BACKUP_DIR="${2:?--backup-dir requires a value}"
|
||
shift 2
|
||
;;
|
||
--backup-path)
|
||
BACKUP_PATH="${2:?--backup-path requires a value}"
|
||
shift 2
|
||
;;
|
||
--app-service)
|
||
APP_SERVICE="${2:?--app-service requires a value}"
|
||
shift 2
|
||
;;
|
||
--postgres-service)
|
||
POSTGRES_SERVICE="${2:?--postgres-service requires a value}"
|
||
shift 2
|
||
;;
|
||
--minio-service)
|
||
MINIO_SERVICE="${2:?--minio-service requires a value}"
|
||
shift 2
|
||
;;
|
||
--helper-image)
|
||
HELPER_IMAGE="${2:?--helper-image requires a value}"
|
||
shift 2
|
||
;;
|
||
--skip-stop-app-for-backup)
|
||
SKIP_STOP_APP_FOR_BACKUP=1
|
||
shift
|
||
;;
|
||
--skip-database-backup)
|
||
SKIP_DATABASE_BACKUP=1
|
||
shift
|
||
;;
|
||
--skip-object-storage-backup)
|
||
SKIP_OBJECT_STORAGE_BACKUP=1
|
||
shift
|
||
;;
|
||
--skip-object-storage-restore)
|
||
SKIP_OBJECT_STORAGE_RESTORE=1
|
||
shift
|
||
;;
|
||
--restore-env)
|
||
RESTORE_ENV=1
|
||
shift
|
||
;;
|
||
--confirm-restore)
|
||
CONFIRM_RESTORE="${2:?--confirm-restore requires a value}"
|
||
shift 2
|
||
;;
|
||
--health-timeout-seconds)
|
||
HEALTH_TIMEOUT_SECONDS="${2:?--health-timeout-seconds requires a value}"
|
||
shift 2
|
||
;;
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
*)
|
||
fail "未知参数:$1"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
read_env_value() {
|
||
local key="$1"
|
||
local path="$2"
|
||
local line
|
||
if [[ ! -f "$path" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
line="$(grep -E "^[[:space:]]*${key}=" "$path" | tail -n 1 || true)"
|
||
[[ -z "$line" ]] && return 0
|
||
printf '%s' "$line" | sed -E "s/^[^=]*=//;s/\r$//;s/^['\"]//;s/['\"]$//"
|
||
}
|
||
|
||
json_escape() {
|
||
printf '%s' "${1:-}" | sed -E 's/\\/\\\\/g;s/"/\\"/g'
|
||
}
|
||
|
||
is_external_database() {
|
||
local database_url host
|
||
database_url="$(read_env_value "DATABASE_URL" "$ENV_PATH")"
|
||
[[ -z "$database_url" ]] && return 1
|
||
host="$(printf '%s' "$database_url" | sed -E 's#^[a-zA-Z0-9+.-]+://[^@]*@([^/:?]+).*#\1#')"
|
||
[[ "$host" != "postgres" && "$host" != "$database_url" ]]
|
||
}
|
||
|
||
is_external_object_storage() {
|
||
local endpoint host
|
||
endpoint="$(read_env_value "S3_ENDPOINT" "$ENV_PATH")"
|
||
[[ -z "$endpoint" ]] && return 1
|
||
host="$(printf '%s' "$endpoint" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#')"
|
||
[[ "$host" != "minio" ]]
|
||
}
|
||
|
||
run_docker() {
|
||
docker "$@"
|
||
}
|
||
|
||
run_compose() {
|
||
run_docker "${COMPOSE_ARGS[@]}" "$@"
|
||
}
|
||
|
||
service_container_id() {
|
||
local service="$1"
|
||
local container_id
|
||
container_id="$(run_compose ps -q "$service" | tr -d '\r')"
|
||
[[ -n "$container_id" ]] || fail "未找到服务容器:$service。请先确认服务已启动。"
|
||
printf '%s' "$container_id"
|
||
}
|
||
|
||
optional_service_container_id() {
|
||
local service="$1"
|
||
run_compose ps -q "$service" 2>/dev/null | tr -d '\r' || true
|
||
}
|
||
|
||
service_running() {
|
||
local service="$1"
|
||
local container_id status
|
||
container_id="$(optional_service_container_id "$service")"
|
||
[[ -n "$container_id" ]] || return 1
|
||
status="$(run_docker inspect "$container_id" --format '{{.State.Status}}' 2>/dev/null || true)"
|
||
[[ "$status" == "running" ]]
|
||
}
|
||
|
||
stop_app_for_backup() {
|
||
if [[ "$SKIP_STOP_APP_FOR_BACKUP" -eq 1 ]]; then
|
||
warn "已跳过备份停写入步骤;请确认客户现场没有人正在上传、编辑或删除素材。"
|
||
return 1
|
||
fi
|
||
|
||
if ! service_running "$APP_SERVICE"; then
|
||
info "$APP_SERVICE 服务未运行或不存在,跳过停写入步骤。"
|
||
return 1
|
||
fi
|
||
|
||
info "进入备份维护窗口:暂时停止 $APP_SERVICE 服务,避免备份期间继续写入数据库或素材文件。"
|
||
run_compose stop "$APP_SERVICE"
|
||
return 0
|
||
}
|
||
|
||
restart_app_after_backup() {
|
||
info "备份维护窗口结束:恢复启动 $APP_SERVICE 服务。"
|
||
run_compose up -d "$APP_SERVICE"
|
||
}
|
||
|
||
save_command_output() {
|
||
local path="$1"
|
||
shift
|
||
set +e
|
||
"$@" >"$path" 2>&1
|
||
local status=$?
|
||
set -e
|
||
if [[ "$status" -ne 0 ]]; then
|
||
printf '命令执行失败,退出码:%s\n' "$status" >>"$path"
|
||
fi
|
||
}
|
||
|
||
backup_database() {
|
||
local output_path="$1"
|
||
info "备份 Compose 内置 PostgreSQL"
|
||
run_compose up -d "$POSTGRES_SERVICE"
|
||
local container_id
|
||
container_id="$(service_container_id "$POSTGRES_SERVICE")"
|
||
|
||
run_compose exec -T "$POSTGRES_SERVICE" sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges -f /tmp/flnxi-backup.sql && test -s /tmp/flnxi-backup.sql'
|
||
run_docker cp "${container_id}:/tmp/flnxi-backup.sql" "$output_path"
|
||
[[ -s "$output_path" ]] || fail "数据库备份复制失败:$output_path"
|
||
run_compose exec -T "$POSTGRES_SERVICE" sh -c 'rm -f /tmp/flnxi-backup.sql'
|
||
ok "数据库备份完成:$output_path"
|
||
}
|
||
|
||
restore_database() {
|
||
local input_path="$1"
|
||
info "恢复 Compose 内置 PostgreSQL"
|
||
run_compose up -d "$POSTGRES_SERVICE"
|
||
local container_id
|
||
container_id="$(service_container_id "$POSTGRES_SERVICE")"
|
||
|
||
run_docker cp "$input_path" "${container_id}:/tmp/flnxi-restore.sql"
|
||
run_compose exec -T "$POSTGRES_SERVICE" sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f /tmp/flnxi-restore.sql'
|
||
run_compose exec -T "$POSTGRES_SERVICE" sh -c 'rm -f /tmp/flnxi-restore.sql'
|
||
ok "数据库恢复完成"
|
||
}
|
||
|
||
backup_object_storage() {
|
||
local volume_name="$1"
|
||
local output_path="$2"
|
||
local backup_dir
|
||
[[ -n "$volume_name" ]] || fail "MINIO_VOLUME_NAME 为空,无法备份内置对象存储卷。"
|
||
backup_dir="$(dirname "$output_path")"
|
||
info "备份 Compose 内置 MinIO 数据卷:$volume_name"
|
||
run_docker volume inspect "$volume_name" >/dev/null
|
||
run_docker run --rm -v "${volume_name}:/source:ro" -v "${backup_dir}:/backup" "$HELPER_IMAGE" sh -c "cd /source && tar -czf /backup/minio-data.tar.gz ."
|
||
[[ -s "$output_path" ]] || fail "对象存储备份失败。请确认 helper 镜像可用:$HELPER_IMAGE"
|
||
ok "对象存储备份完成:$output_path"
|
||
}
|
||
|
||
restore_object_storage() {
|
||
local volume_name="$1"
|
||
local input_path="$2"
|
||
local backup_dir
|
||
[[ -n "$volume_name" ]] || fail "MINIO_VOLUME_NAME 为空,无法恢复内置对象存储卷。"
|
||
backup_dir="$(dirname "$input_path")"
|
||
info "恢复 Compose 内置 MinIO 数据卷:$volume_name"
|
||
if ! run_docker volume inspect "$volume_name" >/dev/null 2>&1; then
|
||
run_docker volume create "$volume_name" >/dev/null
|
||
fi
|
||
run_docker run --rm -v "${volume_name}:/target" -v "${backup_dir}:/backup:ro" "$HELPER_IMAGE" sh -c "rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true; cd /target && tar -xzf /backup/minio-data.tar.gz"
|
||
ok "对象存储恢复完成"
|
||
}
|
||
|
||
backup_local_directory() {
|
||
local name="$1"
|
||
local output_path="$2"
|
||
[[ -d "$PROJECT_DIR_ABS/$name" ]] || return 1
|
||
tar -czf "$output_path" -C "$PROJECT_DIR_ABS" "$name"
|
||
ok "本地目录备份完成:$name -> $output_path"
|
||
return 0
|
||
}
|
||
|
||
restore_local_directory() {
|
||
local name="$1"
|
||
local input_path="$2"
|
||
[[ -f "$input_path" ]] || return 0
|
||
local target_path before_path
|
||
target_path="$PROJECT_DIR_ABS/$name"
|
||
if [[ -d "$target_path" ]]; then
|
||
before_path="$PROJECT_DIR_ABS/$name.before-restore-$(date +%Y%m%d-%H%M%S)"
|
||
mv "$target_path" "$before_path"
|
||
ok "$name 恢复前副本:$before_path"
|
||
fi
|
||
tar -xzf "$input_path" -C "$PROJECT_DIR_ABS"
|
||
ok "本地目录恢复完成:$name"
|
||
}
|
||
|
||
wait_health() {
|
||
local health_url="$1"
|
||
local timeout="$2"
|
||
local deadline
|
||
deadline=$(( $(date +%s) + timeout ))
|
||
while [[ "$(date +%s)" -le "$deadline" ]]; do
|
||
if curl -fsS "$health_url" >/dev/null 2>&1; then
|
||
ok "/api/health 正常"
|
||
return 0
|
||
fi
|
||
sleep 5
|
||
done
|
||
fail "/api/health 在 $timeout 秒内没有通过:$health_url"
|
||
}
|
||
|
||
verify_install() {
|
||
info "检查 Compose 配置"
|
||
run_compose config --quiet
|
||
|
||
info "查看容器状态"
|
||
run_compose ps
|
||
|
||
local public_url
|
||
public_url="$(read_env_value "APP_PUBLIC_URL" "$ENV_PATH")"
|
||
if [[ -z "$public_url" ]]; then
|
||
public_url="$(read_env_value "NEXTAUTH_URL" "$ENV_PATH")"
|
||
fi
|
||
|
||
if [[ -z "$public_url" ]]; then
|
||
warn "未配置 APP_PUBLIC_URL / NEXTAUTH_URL,跳过 /api/health HTTP 验收。"
|
||
return 0
|
||
fi
|
||
|
||
info "检查健康接口:${public_url%/}/api/health"
|
||
wait_health "${public_url%/}/api/health" "$HEALTH_TIMEOUT_SECONDS"
|
||
}
|
||
|
||
write_manifest() {
|
||
local target_dir="$1"
|
||
local minio_volume_name="$2"
|
||
local external_db="$3"
|
||
local external_object_storage="$4"
|
||
local data_backed="$5"
|
||
local uploads_backed="$6"
|
||
local app_stopped="$7"
|
||
cat >"$target_dir/manifest.json" <<EOF
|
||
{
|
||
"createdAt": "$(date +%Y-%m-%dT%H:%M:%S)",
|
||
"projectDir": "$(json_escape "$PROJECT_DIR_ABS")",
|
||
"composeFile": "$(json_escape "$COMPOSE_PATH")",
|
||
"envFile": "$(json_escape "$ENV_PATH")",
|
||
"appImage": "$(json_escape "$(read_env_value "APP_IMAGE" "$ENV_PATH")")",
|
||
"appPublicUrl": "$(json_escape "$(read_env_value "APP_PUBLIC_URL" "$ENV_PATH")")",
|
||
"postgresService": "$(json_escape "$POSTGRES_SERVICE")",
|
||
"minioVolumeName": "$(json_escape "$minio_volume_name")",
|
||
"externalDatabase": $external_db,
|
||
"externalObjectStorage": $external_object_storage,
|
||
"localDirectories": {
|
||
"data": $data_backed,
|
||
"uploads": $uploads_backed
|
||
},
|
||
"appStoppedForConsistentBackup": $app_stopped,
|
||
"restoreConfirmPhrase": "$RESTORE_CONFIRM_PHRASE"
|
||
}
|
||
EOF
|
||
}
|
||
|
||
backup_action_body() {
|
||
local timestamp backup_root target_dir minio_volume_name external_db external_object_storage
|
||
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||
backup_root="$PROJECT_DIR_ABS/$BACKUP_DIR"
|
||
target_dir="$backup_root/flnxi-backup-$timestamp"
|
||
mkdir -p "$target_dir"
|
||
|
||
cp "$COMPOSE_PATH" "$target_dir/docker-compose.prod.yml"
|
||
[[ -f "$ENV_PATH" ]] && cp "$ENV_PATH" "$target_dir/.env"
|
||
[[ -f "$PROJECT_DIR_ABS/VERSION.txt" ]] && cp "$PROJECT_DIR_ABS/VERSION.txt" "$target_dir/VERSION.txt"
|
||
|
||
cat >"$target_dir/README-SENSITIVE.txt" <<'EOF'
|
||
This backup may contain .env secrets, database data, license cache, and customer materials.
|
||
Store it in a controlled location. Do not commit it to Git.
|
||
EOF
|
||
|
||
save_command_output "$target_dir/compose-config.txt" docker "${COMPOSE_ARGS[@]}" config
|
||
save_command_output "$target_dir/compose-ps.txt" docker "${COMPOSE_ARGS[@]}" ps
|
||
save_command_output "$target_dir/docker-images.txt" docker images --digests
|
||
|
||
if is_external_database; then external_db=true; else external_db=false; fi
|
||
if [[ "$SKIP_DATABASE_BACKUP" -eq 1 ]]; then
|
||
printf '已按参数跳过数据库备份。请确认外部备份已存在。\n' >"$target_dir/DATABASE_BACKUP_SKIPPED.txt"
|
||
elif [[ "$external_db" == true ]]; then
|
||
printf '检测到 DATABASE_URL 指向外部数据库。本脚本不会自动备份外部数据库,请按客户现场数据库方案单独备份。\n' >"$target_dir/EXTERNAL_DATABASE_BACKUP_REQUIRED.txt"
|
||
else
|
||
backup_database "$target_dir/postgres.sql"
|
||
fi
|
||
|
||
minio_volume_name="$(read_env_value "MINIO_VOLUME_NAME" "$ENV_PATH")"
|
||
minio_volume_name="${minio_volume_name:-flnxi-minio-data}"
|
||
if is_external_object_storage; then external_object_storage=true; else external_object_storage=false; fi
|
||
if [[ "$SKIP_OBJECT_STORAGE_BACKUP" -eq 1 ]]; then
|
||
printf '已按参数跳过对象存储备份。请确认外部 S3 / MinIO 或 Docker 卷已有独立备份。\n' >"$target_dir/OBJECT_STORAGE_BACKUP_SKIPPED.txt"
|
||
elif [[ "$external_object_storage" == true ]]; then
|
||
printf '检测到 S3_ENDPOINT 指向外部 S3 / MinIO。本脚本不会自动备份外部对象存储,请按客户现场方案单独备份素材对象。\n' >"$target_dir/EXTERNAL_OBJECT_STORAGE_BACKUP_REQUIRED.txt"
|
||
else
|
||
backup_object_storage "$minio_volume_name" "$target_dir/minio-data.tar.gz"
|
||
fi
|
||
|
||
local data_backed uploads_backed
|
||
data_backed=false
|
||
uploads_backed=false
|
||
if backup_local_directory "data" "$target_dir/data.tar.gz"; then data_backed=true; fi
|
||
if backup_local_directory "uploads" "$target_dir/uploads.tar.gz"; then uploads_backed=true; fi
|
||
|
||
write_manifest "$target_dir" "$minio_volume_name" "$external_db" "$external_object_storage" "$data_backed" "$uploads_backed" "$APP_STOPPED_FOR_BACKUP"
|
||
ok "备份目录:$target_dir"
|
||
}
|
||
|
||
backup_action() {
|
||
APP_STOPPED_FOR_BACKUP=false
|
||
if stop_app_for_backup; then
|
||
APP_STOPPED_FOR_BACKUP=true
|
||
fi
|
||
|
||
set +e
|
||
backup_action_body
|
||
local status=$?
|
||
set -e
|
||
|
||
if [[ "$APP_STOPPED_FOR_BACKUP" == true ]]; then
|
||
restart_app_after_backup
|
||
if [[ "$status" -eq 0 ]]; then
|
||
verify_install
|
||
status=$?
|
||
fi
|
||
fi
|
||
|
||
return "$status"
|
||
}
|
||
|
||
restore_action() {
|
||
[[ -n "$BACKUP_PATH" ]] || fail "恢复必须传入 --backup-path。"
|
||
[[ "$CONFIRM_RESTORE" == "$RESTORE_CONFIRM_PHRASE" ]] || fail "恢复会覆盖客户数据。请显式传入 --confirm-restore $RESTORE_CONFIRM_PHRASE。"
|
||
|
||
local source_dir db_backup object_backup env_backup_path minio_volume_name
|
||
source_dir="$(cd "$BACKUP_PATH" && pwd)"
|
||
db_backup="$source_dir/postgres.sql"
|
||
object_backup="$source_dir/minio-data.tar.gz"
|
||
|
||
warn "即将恢复备份目录:$source_dir"
|
||
run_compose stop "$APP_SERVICE" || true
|
||
|
||
if [[ "$RESTORE_ENV" -eq 1 && -f "$source_dir/.env" ]]; then
|
||
env_backup_path="$PROJECT_DIR_ABS/.env.before-restore-$(date +%Y%m%d-%H%M%S)"
|
||
[[ -f "$ENV_PATH" ]] && cp "$ENV_PATH" "$env_backup_path"
|
||
cp "$source_dir/.env" "$ENV_PATH"
|
||
ok ".env 已恢复,恢复前副本:$env_backup_path"
|
||
fi
|
||
|
||
if [[ -f "$db_backup" ]]; then
|
||
restore_database "$db_backup"
|
||
else
|
||
warn "备份目录没有 postgres.sql,跳过数据库恢复。"
|
||
fi
|
||
|
||
if is_external_object_storage; then
|
||
warn "检测到当前 .env 使用外部对象存储,脚本不会恢复 Compose 内置 MinIO 卷;请按客户现场对象存储备份方案恢复。"
|
||
elif [[ "$SKIP_OBJECT_STORAGE_RESTORE" -eq 0 && -f "$object_backup" ]]; then
|
||
run_compose stop "$MINIO_SERVICE" || true
|
||
minio_volume_name="$(read_env_value "MINIO_VOLUME_NAME" "$ENV_PATH")"
|
||
minio_volume_name="${minio_volume_name:-flnxi-minio-data}"
|
||
restore_object_storage "$minio_volume_name" "$object_backup"
|
||
elif [[ "$SKIP_OBJECT_STORAGE_RESTORE" -eq 1 ]]; then
|
||
warn "已按参数跳过对象存储恢复。"
|
||
else
|
||
warn "备份目录没有 minio-data.tar.gz,跳过对象存储恢复。"
|
||
fi
|
||
|
||
restore_local_directory "data" "$source_dir/data.tar.gz"
|
||
restore_local_directory "uploads" "$source_dir/uploads.tar.gz"
|
||
|
||
run_compose up -d
|
||
verify_install
|
||
ok "恢复和基础验收完成"
|
||
}
|
||
|
||
command -v docker >/dev/null 2>&1 || fail "未找到 docker 命令"
|
||
docker compose version >/dev/null 2>&1 || fail "当前 Docker 不支持 compose v2,请先安装 Docker Compose V2"
|
||
command -v curl >/dev/null 2>&1 || fail "未找到 curl 命令"
|
||
command -v tar >/dev/null 2>&1 || fail "未找到 tar 命令"
|
||
|
||
PROJECT_DIR_ABS="$(cd "$PROJECT_DIR" && pwd)"
|
||
COMPOSE_PATH="$PROJECT_DIR_ABS/$COMPOSE_FILE"
|
||
ENV_PATH="$PROJECT_DIR_ABS/$ENV_FILE"
|
||
[[ -f "$COMPOSE_PATH" ]] || fail "Compose 文件不存在:$COMPOSE_PATH"
|
||
|
||
cd "$PROJECT_DIR_ABS"
|
||
COMPOSE_ARGS=(compose -f "$COMPOSE_PATH")
|
||
if [[ -f "$ENV_PATH" ]]; then
|
||
COMPOSE_ARGS+=(--env-file "$ENV_PATH")
|
||
fi
|
||
|
||
case "$ACTION" in
|
||
backup)
|
||
backup_action
|
||
;;
|
||
restore)
|
||
restore_action
|
||
;;
|
||
verify)
|
||
verify_install
|
||
;;
|
||
esac
|