Files
blog/deploy/editor-api/bootstrap.sh
T
zqlit 448b898e40 feat(远端): 恢复自建 Gitea 代码辅仓,OpenList 降为备选备份
用户定案:「自建 gitea 同步一下吧,openlist 也作为备选备份」。
于是把上一轮「撤销全部 git 辅仓」的决定部分回退:在线那一路回到自建 Gitea,
OpenList 上的加密 bundle 从「唯一异地备份」改为「备选异地备份」。

两层不是重复,而是失效模式不同:
  · 自建 Gitea(在线、可增量、可浏览)—— 不受任何第三方平台规则约束
  · 离线 bundle(离线、单一文件、完整历史)—— 平台全挂也能恢复

一、实际同步
- Gitea 位于 23.254.236.47:3001(Gitea 28.0.0),本机直连即可推,
  **不需要广州中转机**(旧结论「沙箱跑不通 git smart HTTP」只针对当时的代理)
- 远端停在 ed38f938(2026-10-04),落后 57 个提交,且是本地 HEAD 的**祖先**
  → 一次 fast-forward 追平,**未强推、未丢历史**
- 同步后 origin 与 gitea 均指向 5f27ad32

二、git 配置
- 恢复 remote `gitea`(凭据编在 URL 里,只落本机 .git/config,不入库)
- `pushall` 回到双推:
    !git push origin main; ec=$?; git remote | grep -qx gitea && git push gitea main; exit $ec
  · 用 `;` 而非 `&&` → 主仓挂了辅仓照样推
  · `exit $ec` → 退出码仍以主仓为准,辅仓成功不掩盖主仓失败
  · gitea 段带存在性前置判断 → 没配辅仓时静默跳过,不白报错

三、发布链路恢复 gitea(前一轮刚去掉,现按新定案加回)
- `deploy/editor-api/bootstrap.sh`
    · 新增 GITEA_URL / GITEA_USER / GITEA_PASS(凭据必须现给,不写死在脚本里)
    · PUSH_REMOTES 默认 → origin,gitea;没给 GITEA_PASS 则降级为 origin
    · 恢复「配 gitea 远端」一节;退役远端名单去掉 gitea(只留 gh / gitee)
      —— 否则新部署会把现行辅仓当成退役远端清掉
    · 结尾补一句「未启用 Gitea 辅仓」的提示
- `docker-compose.editor.yml`、`editor-api/server.mjs` → 默认 origin,gitea
- `editor-api/README.md` → 变量表同步;`editor-api/Dockerfile` → 注释补 http 情形

四、`scripts/setup-cnb-remotes.sh` 改回双远端模式
- 头部沿革、USAGE、凭据段全部重写;GITEA_PASS 走环境变量,不入库
- 退役远端循环只清 gh / gitee;pushall 写同一份定义

五、文档
- `架构总览.md`:§1.2 地址地图加「代码辅仓」行、备份行降为「备选」;
  §2 旅程图改双推;§5.1 推送重写(含别名实际定义与三条取舍);
  §5.2 远端改双行表 + Gitea 接入细节;§5.3 由「不走 git 远端」改为
  「远端与备份的分工」(三层各自的失效模式);§5.6 定位改备选;
  §6 待办 #2/#4/#7/#10 更新
- `CNB构建落地方案.md` §4.0 目标形态与定案块、§4.1 第 4 条
- `README.md` 发布流程图、推送说明、脚本表
- `scripts/backup-run.mjs` 头部注释:不再是「唯一备份」

实测:
  git push gitea main → ed38f938..5f27ad32  main -> main
  git pushall         → origin / gitea 双向同步,退出码 0
  bash -n 校验 bootstrap.sh 与 setup-cnb-remotes.sh 均通过
2026-10-06 21:42:08 +08:00

216 lines
9.8 KiB
Bash
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.
#!/usr/bin/env bash
# =====================================================================
# editor-api 一键部署 / 迁移脚本
# =====================================================================
# 用途:在一台全新机器上(或从快过期的机器上迁走)用一条命令把
# 「在线编辑文章」的后端跑起来。所有状态都在两个目录里:
# $BLOG_DIR = 博客仓库的 git 工作区(内容就在这里)
# $APP_DIR = compose + .env(只有这一个文件是手写的)
# 迁移 = 复制这两个目录 / 或在新机器上重跑本脚本。
#
# 用法:
# CNB_TOKEN=xxx EDITOR_TOKEN=yyy bash bootstrap.sh
#
# 必填环境变量:
# CNB_TOKEN CNB 访问令牌(用户名固定 cnb)。只用于 git clone 与 push。
# EDITOR_TOKEN 与 Cloudflare Worker 里 EDITOR_TOKEN **完全相同**的共享令牌
# (Worker 转发时会带上,后端用它鉴权;浏览器永远看不到)
# ADMIN_PASS 后台登录页的密码(账号固定 admin,默认 3070210zz)。
# 后台登录走 editor-api 签发的 Cookie 会话,不再用 nginx basic auth。
#
# 可选环境变量(都有默认值):
# CNB_URL 默认 https://cnb.cool/zqlit/blog.git
# GITEA_URL 默认 http://23.254.236.47:3001/zqlit/blog.git(自建 Gitea 辅仓)
# GITEA_USER 默认 zqlit
# GITEA_PASS 可选:Gitea 的密码 / 访问令牌。**给了才会把 gitea 加进推送远端**,
# 否则 PUSH_REMOTES 自动降级成只推 origin(CNB 主仓)
# BLOG_DIR /srv/blog 仓库工作区
# APP_DIR /srv/editor-api compose + .env
# TRASH_DIR /srv/editor-trash 删除文章的回收目录(故意放在仓库外)
# BIND_PORT 8017 只绑 127.0.0.1
# PUSH_REMOTES origin,gitea 发布时依次推送的远端
# GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL / BLOG_BASE
#
# ★ 远端与备份定案(2026-10-06):
# - CNB(remote `origin`):**主仓**,唯一的 fetch 源,推送即触发 CNB 构建
# - 自建 Gitea(remote `gitea`,23.254.236.47:3001):**代码同步辅仓**,
# 只作代码留档与找回,不参与构建(.github/ 已删,Actions 早已停用)
# - GitHub(GH_URL / GH_SSH_KEY / remote `gh`):账号被平台标记、仓库被按
# AUP 清空 → 已彻底移除,别再往回加
# - Gitee:单仓 500MB / 单文件 50MB 两道硬门槛,本仓 `bin/linux/hugo`
# 是 83.1MB → 未采用
# - OpenList(中兴 F50 上的加密 bundle,scripts/backup-bundle.mjs,
# Windows 计划任务 Blog-BundleBackup 每天 03:30):**备选异地备份**,
# 与 git 远端无关,独立于上面三者
# 推送语义见 editor-api/src/git.mjs:逐个远端推、**主仓成功即算发布成功**,
# 辅仓失败只警告不阻断 —— 所以 gitea 挂了不会让文章发不出去。
# =====================================================================
set -euo pipefail
say() { printf '\033[1;36m==> %s\033[0m\n' "$*"; }
die() { printf '\033[1;31m!! %s\033[0m\n' "$*" >&2; exit 1; }
: "${CNB_TOKEN:?必须设置 CNB_TOKEN(CNB 访问令牌,用户名固定 cnb)}"
: "${EDITOR_TOKEN:?必须设置 EDITOR_TOKEN(与 Cloudflare Worker 里的一致)}"
: "${ADMIN_PASS:?必须设置 ADMIN_PASS(后台登录密码,账号固定 admin)}"
CNB_URL="${CNB_URL:-https://cnb.cool/zqlit/blog.git}"
GITEA_URL="${GITEA_URL:-http://23.254.236.47:3001/zqlit/blog.git}"
GITEA_USER="${GITEA_USER:-zqlit}"
BLOG_DIR="${BLOG_DIR:-/srv/blog}"
APP_DIR="${APP_DIR:-/srv/editor-api}"
TRASH_DIR="${TRASH_DIR:-/srv/editor-trash}"
BIND_PORT="${BIND_PORT:-8017}"
GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-blog-editor}"
GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-editor@usj.cc}"
BLOG_BASE="${BLOG_BASE:-}"
PUSH_REMOTES="${PUSH_REMOTES:-origin,gitea}"
# 没给 Gitea 凭据就只推主仓 —— 宁可少推一个辅仓,
# 也不要让每次发布都对着一个认证不过的远端空转一遍。
# (这里是有意直接覆盖成 origin:默认列表就是 origin,gitea,
# 没有 gitea 凭据等于只剩 origin;要推别的远端请连同 gitea 一起显式写全。)
if [ -z "${GITEA_PASS:-}" ]; then
PUSH_REMOTES="origin"
fi
command -v docker >/dev/null || die "没有 docker"
docker compose version >/dev/null 2>&1 || die "没有 docker compose"
say "1/5 准备工作目录"
mkdir -p "$BLOG_DIR" "$APP_DIR" "$TRASH_DIR"
# compose 的 volume 必须写绝对路径,这里统一转成绝对路径
BLOG_DIR="$(cd "$BLOG_DIR" && pwd)"
APP_DIR="$(cd "$APP_DIR" && pwd)"
TRASH_DIR="$(cd "$TRASH_DIR" && pwd)"
say "2/5 拉取/更新博客仓库 → $BLOG_DIR"
AUTH_URL="$(printf '%s' "$CNB_URL" | sed -E "s#^https://#https://cnb:${CNB_TOKEN}@#")"
if [ -d "$BLOG_DIR/.git" ]; then
git -C "$BLOG_DIR" remote set-url origin "$AUTH_URL"
git -C "$BLOG_DIR" fetch origin main
git -C "$BLOG_DIR" checkout main
git -C "$BLOG_DIR" reset --hard origin/main
else
git clone --branch main "$AUTH_URL" "$BLOG_DIR"
fi
# 自建 Gitea 辅仓:只有给了 GITEA_PASS 才配(没给就只推 origin,见上面 PUSH_REMOTES 降级)。
# 凭据直接编进远端 URL —— 容器里没有交互终端、也没有 credential helper,
# 这是唯一能让 `git push gitea` 免交互跑通的写法。
# 明文只落在 $APP_DIR/.env 与容器内的 .git/config,不会进仓库。
if [ -n "${GITEA_PASS:-}" ]; then
_gitea_auth="$(printf '%s' "$GITEA_URL" | sed -E "s#^(https?://)#\1${GITEA_USER}:${GITEA_PASS}@#")"
git -C "$BLOG_DIR" remote remove gitea 2>/dev/null || true
git -C "$BLOG_DIR" remote add gitea "$_gitea_auth"
unset _gitea_auth
fi
# 退役的远端:以前这一节会配 GitHub / Gitee 辅仓,两者都已退役
# (GitHub 被平台按 AUP 清空、Gitee 卡体积门槛)。若这份工作区是从旧部署续用的,
# 顺手把残留的退役远端清掉,免得后端按 PUSH_REMOTES 推送时撞上已失效的远端。
# ★ gitea **不在**退役名单里 —— 它是现行辅仓,别跟着一起清掉。
for _retired_remote in gh gitee; do
git -C "$BLOG_DIR" remote remove "$_retired_remote" 2>/dev/null || true
done
# 容器里要能 commit —— 顺手把身份写进仓库配置(不改全局)
git -C "$BLOG_DIR" config user.name "$GIT_AUTHOR_NAME"
git -C "$BLOG_DIR" config user.email "$GIT_AUTHOR_EMAIL"
say "3/5 写入 $APP_DIR/.env"
umask 077
# 用 printf 而不是 heredoc:令牌里若含 $ 或反引号,heredoc 会被 shell 展开
{
printf 'EDITOR_TOKEN=%s\n' "$EDITOR_TOKEN"
printf 'ADMIN_USER=%s\n' "${ADMIN_USER:-admin}"
printf 'ADMIN_PASS=%s\n' "$ADMIN_PASS"
printf 'BLOG_DIR=%s\n' "$BLOG_DIR"
printf 'TRASH_DIR=%s\n' "$TRASH_DIR"
printf 'BIND_PORT=%s\n' "$BIND_PORT"
printf 'PUSH_REMOTES=%s\n' "$PUSH_REMOTES"
printf 'GIT_AUTHOR_NAME=%s\n' "$GIT_AUTHOR_NAME"
printf 'GIT_AUTHOR_EMAIL=%s\n' "$GIT_AUTHOR_EMAIL"
printf 'BLOG_BASE=%s\n' "$BLOG_BASE"
} > "$APP_DIR/.env"
say "4/5 生成 compose 并启动容器"
# ★ 仓库与回收站必须落在**同一个挂载点**里。
# 若容器内把它们挂成两个独立 bind mount,rename(2) 跨挂载点必返回 EXDEV
# ——哪怕宿主机上它们同在一块盘上。后端虽已能退化成「复制+删除」,但那是白搬一遍数据。
# 做法:挂载 BLOG_DIR 的**父目录**,让容器内路径 == 宿主机路径。
MOUNT_ROOT="$(dirname "$BLOG_DIR")"
if [ "${TRASH_DIR#"$MOUNT_ROOT"/}" != "$TRASH_DIR" ]; then
# 回收站就在仓库父目录下 → 一个挂载点搞定
BLOG_IN_CT="$BLOG_DIR"
TRASH_IN_CT="$TRASH_DIR"
VOLUMES=" - $MOUNT_ROOT:$MOUNT_ROOT"
else
# 回收站被指到别处 → 只能两条独立挂载(后端会自动复制+删除,只是慢一点)
BLOG_IN_CT=/blog
TRASH_IN_CT=/app/trash
VOLUMES=" - $BLOG_DIR:/blog
- $TRASH_DIR:/app/trash"
fi
cat > "$APP_DIR/docker-compose.yml" <<EOF
# 本文件由 bootstrap.sh 生成 —— 想改配置请改 bootstrap.sh 或 .env 后重跑
services:
editor-api:
build: $BLOG_DIR/editor-api
image: editor-api:local
container_name: editor-api
restart: unless-stopped
ports:
- "127.0.0.1:$BIND_PORT:8017"
environment:
EDITOR_TOKEN: \${EDITOR_TOKEN:?}
BLOG_ROOT: $BLOG_IN_CT
TRASH_DIR: $TRASH_IN_CT
BIND_HOST: 0.0.0.0
GIT_BRANCH: main
PUSH_REMOTES: \${PUSH_REMOTES:-origin,gitea}
GIT_AUTHOR_NAME: \${GIT_AUTHOR_NAME:-blog-editor}
GIT_AUTHOR_EMAIL: \${GIT_AUTHOR_EMAIL:-editor@usj.cc}
BLOG_BASE: \${BLOG_BASE:-}
ADMIN_USER: \${ADMIN_USER:-admin}
ADMIN_PASS: \${ADMIN_PASS:?}
MAX_UPLOAD_MB: "20"
GIT_PATHS: content,static
volumes:
$VOLUMES
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8017/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
EOF
cd "$APP_DIR"
docker compose up -d --build
say "5/5 健康检查"
for i in $(seq 1 30); do
if curl -fsS -H "X-Editor-Token: $EDITOR_TOKEN" "http://127.0.0.1:$BIND_PORT/health" >/tmp/editor-health.json 2>/dev/null; then
cat /tmp/editor-health.json; echo
say "部署完成。接下来:"
echo " · 宿主机 nginx(1Panel 站点)加反代:location ^~ / { proxy_pass http://127.0.0.1:$BIND_PORT; ... }"
echo " · Cloudflare Worker 变量:EDITOR_API_BASE=https://<指向本机的域名> EDITOR_TOKEN=$EDITOR_TOKEN"
echo " · 推送远端:PUSH_REMOTES=$PUSH_REMOTES"
if [ -z "${GITEA_PASS:-}" ]; then
echo " · 未提供 GITEA_PASS → 本次只推 origin,Gitea 辅仓未启用"
echo " (要启用:GITEA_PASS='<Gitea 密码或令牌>' 重跑本脚本)"
fi
exit 0
fi
sleep 2
done
die "健康检查失败,看日志:cd $APP_DIR && docker compose logs --tail=80"