From ee2e75b057225ad012af92caef59dc6adce4b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Wed, 18 Mar 2026 11:39:50 +0800 Subject: [PATCH] ops: add encrypted db snapshot publish/import scripts --- scripts/db_snapshot_import.sh | 99 +++++++++++++++++++++ scripts/db_snapshot_publish.sh | 151 +++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100755 scripts/db_snapshot_import.sh create mode 100755 scripts/db_snapshot_publish.sh diff --git a/scripts/db_snapshot_import.sh b/scripts/db_snapshot_import.sh new file mode 100755 index 0000000..f6ecfa7 --- /dev/null +++ b/scripts/db_snapshot_import.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 从“私有数据仓库”(Gitea)拉取最新加密快照并导入到本地数据库。 +# +# 用法: +# export SAASSHOP_DB_SNAPSHOT_KEY='你的强密码' +# export SAASSHOP_DATA_REPO_SSH='git@git.xxx:owner/saasshop-data(.wiki).git' +# bash scripts/db_snapshot_import.sh + +REPO_DIR=$(cd "$(dirname "$0")/.." && pwd) +cd "$REPO_DIR" + +DATA_REPO_SSH=${SAASSHOP_DATA_REPO_SSH:-""} +if [[ "$DATA_REPO_SSH" == "" && -f /app/working.secret/saasshop_data_repo_ssh ]]; then + DATA_REPO_SSH=$(cat /app/working.secret/saasshop_data_repo_ssh) +fi + +if [[ "$DATA_REPO_SSH" == "" ]]; then + echo "缺少数据仓库地址:" + echo "- 请设置环境变量 SAASSHOP_DATA_REPO_SSH" + echo "- 或创建文件 /app/working.secret/saasshop_data_repo_ssh(内容为 ssh 地址)" + exit 21 +fi + +if [[ "${SAASSHOP_DB_SNAPSHOT_KEY:-}" == "" ]]; then + echo "缺少解密密钥:请先 export SAASSHOP_DB_SNAPSHOT_KEY='...'" + exit 22 +fi + +ENV_FILE="$REPO_DIR/.env" +if [[ ! -f "$ENV_FILE" && -f /app/working.secret/laravel.env.snapshot ]]; then + ENV_FILE="/app/working.secret/laravel.env.snapshot" +fi + +if [[ ! -f "$ENV_FILE" ]]; then + echo "找不到 .env 或 /app/working.secret/laravel.env.snapshot,无法确定 DB 配置" + exit 23 +fi + +# shellcheck disable=SC1090 +set -a +source "$ENV_FILE" +set +a + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_DATABASE=${DB_DATABASE:-} +DB_USERNAME=${DB_USERNAME:-} +DB_PASSWORD=${DB_PASSWORD:-} + +if [[ "$DB_DATABASE" == "" || "$DB_USERNAME" == "" ]]; then + echo "DB 配置不完整:DB_DATABASE/DB_USERNAME 不能为空" + exit 24 +fi + +WORK_DIR="/tmp/saasshop-data-repo" +if [[ -d "$WORK_DIR/.git" ]]; then + echo "[data-repo] updating existing clone: $WORK_DIR" + git -C "$WORK_DIR" fetch origin + git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main + git -C "$WORK_DIR" pull --rebase origin main || true +else + rm -rf "$WORK_DIR" || true + echo "[data-repo] cloning: $DATA_REPO_SSH -> $WORK_DIR" + git clone "$DATA_REPO_SSH" "$WORK_DIR" + git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main +fi + +ENC_FILE="$WORK_DIR/snapshots/latest.sql.gz.enc" +MANIFEST="$WORK_DIR/snapshots/manifest.json" + +if [[ ! -f "$ENC_FILE" ]]; then + echo "数据仓未找到快照:$ENC_FILE" + exit 25 +fi + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR" || true' EXIT + +SQL_GZ="$TMP_DIR/latest.sql.gz" + +echo "[snapshot] decrypting ..." +openssl enc -d -aes-256-cbc -pbkdf2 \ + -pass env:SAASSHOP_DB_SNAPSHOT_KEY \ + -in "$ENC_FILE" -out "$SQL_GZ" + +echo "[snapshot] importing into mysql ($DB_DATABASE) ..." +# 注意:快照里使用了 --databases,会包含 CREATE DATABASE/USE +# 这里直接交给 mysql 执行。 +gzip -dc "$SQL_GZ" | mysql \ + --host="$DB_HOST" --port="$DB_PORT" \ + --user="$DB_USERNAME" --password="$DB_PASSWORD" + +echo "[done] imported snapshot" +if [[ -f "$MANIFEST" ]]; then + echo "[info] manifest:" + cat "$MANIFEST" +fi diff --git a/scripts/db_snapshot_publish.sh b/scripts/db_snapshot_publish.sh new file mode 100755 index 0000000..84d9e30 --- /dev/null +++ b/scripts/db_snapshot_publish.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 将当前数据库导出并加密后推送到“私有数据仓库”(Gitea)。 +# - 不把明文 SQL 放入 git +# - 数据仓库建议单独授权/独立权限 +# +# 依赖:mysqldump / mysql / openssl / gzip / git +# +# 用法: +# 1) 设置密钥(不要写入仓库): +# export SAASSHOP_DB_SNAPSHOT_KEY='你的强密码' +# 2) 可选设置数据仓库地址(若不设则从 /app/working.secret/saasshop_data_repo_ssh 读取): +# export SAASSHOP_DATA_REPO_SSH='git@git.xxx:owner/saasshop-data(.wiki).git' +# 3) 执行: +# bash scripts/db_snapshot_publish.sh + +REPO_DIR=$(cd "$(dirname "$0")/.." && pwd) +cd "$REPO_DIR" + +DATA_REPO_SSH=${SAASSHOP_DATA_REPO_SSH:-""} +if [[ "$DATA_REPO_SSH" == "" && -f /app/working.secret/saasshop_data_repo_ssh ]]; then + DATA_REPO_SSH=$(cat /app/working.secret/saasshop_data_repo_ssh) +fi + +if [[ "$DATA_REPO_SSH" == "" ]]; then + echo "缺少数据仓库地址:" + echo "- 请设置环境变量 SAASSHOP_DATA_REPO_SSH" + echo "- 或创建文件 /app/working.secret/saasshop_data_repo_ssh(内容为 ssh 地址)" + exit 11 +fi + +if [[ "${SAASSHOP_DB_SNAPSHOT_KEY:-}" == "" ]]; then + echo "缺少加密密钥:请先 export SAASSHOP_DB_SNAPSHOT_KEY='...'(不要写入仓库)" + exit 12 +fi + +# 读取 DB 连接(优先 .env,其次 /app/working.secret/laravel.env.snapshot) +ENV_FILE="$REPO_DIR/.env" +if [[ ! -f "$ENV_FILE" && -f /app/working.secret/laravel.env.snapshot ]]; then + ENV_FILE="/app/working.secret/laravel.env.snapshot" +fi + +if [[ ! -f "$ENV_FILE" ]]; then + echo "找不到 .env 或 /app/working.secret/laravel.env.snapshot,无法确定 DB 配置" + exit 13 +fi + +# shellcheck disable=SC1090 +set -a +source "$ENV_FILE" +set +a + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_DATABASE=${DB_DATABASE:-} +DB_USERNAME=${DB_USERNAME:-} +DB_PASSWORD=${DB_PASSWORD:-} + +if [[ "$DB_DATABASE" == "" || "$DB_USERNAME" == "" ]]; then + echo "DB 配置不完整:DB_DATABASE/DB_USERNAME 不能为空" + exit 14 +fi + +STAMP=$(date +%Y%m%d_%H%M%S) +OUT_DIR=$(mktemp -d) +SQL_GZ="$OUT_DIR/${DB_DATABASE}_${STAMP}.sql.gz" +ENC_FILE="$OUT_DIR/${DB_DATABASE}_${STAMP}.sql.gz.enc" +MANIFEST="$OUT_DIR/manifest.json" + +cleanup() { + rm -rf "$OUT_DIR" || true +} +trap cleanup EXIT + +echo "[snapshot] dumping database '$DB_DATABASE' ..." +# 导出策略:单库结构+数据;保留 routines/triggers/events;避免锁表 +# 注意:--databases 会在 SQL 中带 CREATE DATABASE/USE +mysqldump \ + --host="$DB_HOST" --port="$DB_PORT" \ + --user="$DB_USERNAME" --password="$DB_PASSWORD" \ + --databases "$DB_DATABASE" \ + --single-transaction --quick --skip-lock-tables \ + --routines --triggers --events \ + --hex-blob \ + --default-character-set=utf8mb4 \ + | gzip -9 > "$SQL_GZ" + +echo "[snapshot] encrypting ..." +openssl enc -aes-256-cbc -pbkdf2 -salt \ + -pass env:SAASSHOP_DB_SNAPSHOT_KEY \ + -in "$SQL_GZ" -out "$ENC_FILE" + +SHA256=$(sha256sum "$ENC_FILE" | awk '{print $1}') +SIZE=$(wc -c < "$ENC_FILE" | tr -d ' ') +CODE_HEAD=$(git rev-parse --short HEAD) +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +cat > "$MANIFEST" < $WORK_DIR" + git clone "$DATA_REPO_SSH" "$WORK_DIR" + git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main +fi + +mkdir -p "$WORK_DIR/snapshots" +cp -f "$ENC_FILE" "$WORK_DIR/snapshots/latest.sql.gz.enc" +cp -f "$MANIFEST" "$WORK_DIR/snapshots/manifest.json" + +# 同时保留一份带时间戳的历史(便于回滚/比对) +cp -f "$ENC_FILE" "$WORK_DIR/snapshots/${DB_DATABASE}_${STAMP}.sql.gz.enc" + +git -C "$WORK_DIR" add snapshots + +if git -C "$WORK_DIR" diff --cached --quiet; then + echo "[data-repo] no changes, skip commit" +else + git -C "$WORK_DIR" commit -m "snapshot: ${DB_DATABASE} ${STAMP} (code ${CODE_HEAD})" + git -C "$WORK_DIR" push origin main +fi + +echo "[done] snapshot published to data repo"