#!/bin/bash # ============================================================ # MyBlog 服务器端更新脚本(由本地 deploy/update.ps1 上传并执行) # 职责:备份 -> 解压新代码 -> 数据库迁移 -> 重启服务 -> 验证 # 用法:bash /tmp/update_server.sh [域名] # 适用:Ubuntu + systemd(myblog 服务);需要 root 或已配置免密 sudo # 安全说明: # - 更新前自动备份到 /root/blog-backups(含数据库与上传文件),可随时回滚 # - 只增量覆盖代码文件,不删除服务器上的任何文件(不会加 --delete) # - 数据库迁移是幂等的:重复执行只会补齐缺失的列,不会损坏数据 # ============================================================ set -eu # bash 支持 pipefail(dash/sh 下自动跳过),保证失败即退出 if [ -n "${BASH_VERSION:-}" ]; then set -o pipefail; fi BLOG_DIR="/var/www/blog" PKG="/tmp/myblog.tar.gz" BACKUP_DIR="/root/blog-backups" DOMAIN="${1:-}" # 非 root 用户运行时自动加 sudo(前提:已配置免密 sudo) SUDO="" if [ "$(id -u)" -ne 0 ]; then SUDO="sudo" fi echo "== [1/5] 更新前备份 ==" $SUDO mkdir -p "$BACKUP_DIR" TS=$(date +%Y%m%d_%H%M%S) $SUDO tar -czf "$BACKUP_DIR/blog_backup_$TS.tar.gz" --exclude=venv --exclude='*/__pycache__' -C "$BLOG_DIR" . echo "备份完成:$BACKUP_DIR/blog_backup_$TS.tar.gz" echo "== [2/5] 解压新代码 ==" if [ ! -f "$PKG" ]; then echo "错误:找不到更新包 $PKG,请先通过本地脚本上传" >&2 exit 1 fi $SUDO tar -xzf "$PKG" -C "$BLOG_DIR" $SUDO chown -R blog:blog "$BLOG_DIR" # 前端缓存指纹:每次部署自动生成新版本号,浏览器强制拉取最新 JS/CSS,无需手动 Ctrl+F5 VERSION=$(date +%Y%m%d%H%M%S) $SUDO sed -i "s/__VERSION__/${VERSION}/g" "$BLOG_DIR/frontend/index.html" echo "前端版本号:${VERSION}" # 把 Windows 上传的脚本统一转成 LF 行尾,防止 bash 解析 CR 报错 find "$BLOG_DIR/deploy" -maxdepth 2 -name '*.sh' -type f -exec sed -i 's/\r$//' {} + echo "解压完成,文件属主已修正为 blog:blog" echo "== [3/5] 数据库迁移(幂等) ==" python3 - "$BLOG_DIR" <<'PY' import sqlite3, sys from pathlib import Path base = Path(sys.argv[1]) db = base / "backend" / "blog.db" # 读取 .env 的 DATABASE_URL(与后端 database.py 的解析方式一致,留空用默认) try: for line in (base / ".env").read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith("DATABASE_URL="): val = line.split("=", 1)[1].strip().strip('"').strip("'") if val.startswith("sqlite:///"): p = val[len("sqlite:///"):] db = Path(p) if p.startswith("/") else base / p break except OSError: pass print("目标数据库:", db) con = sqlite3.connect(db) cur = con.cursor() def ensure_table(table, ddl): rows = cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,)).fetchall() if not rows: cur.execute(ddl) print(f"建表:{table}") else: print(f"已存在:{table}") def ensure_column(table, col, ddl): cols = [r[1] for r in cur.execute(f"PRAGMA table_info({table})").fetchall()] if col not in cols: cur.execute(f"ALTER TABLE {table} ADD COLUMN {ddl}") print(f"迁移:{table}.{col} 已新增") else: print(f"已存在:{table}.{col}") ensure_column("articles", "category", "category VARCHAR(30) NOT NULL DEFAULT 'life'") ensure_column("users", "token_version", "token_version INTEGER NOT NULL DEFAULT 0") ensure_column("users", "avatar_updated_time", "avatar_updated_time DATETIME") ensure_column("friends", "created_time", "created_time DATETIME") ensure_table("projects", """ CREATE TABLE IF NOT EXISTS projects ( id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(100) NOT NULL, description TEXT, tech VARCHAR(500), project_type VARCHAR(20) NOT NULL DEFAULT 'static', demo_url VARCHAR(500), download_url VARCHAR(500), github_url VARCHAR(500), created_time DATETIME NOT NULL, updated_time DATETIME NOT NULL )""") ensure_column("projects", "visibility", "visibility VARCHAR(20) NOT NULL DEFAULT 'public'") con.commit() con.close() print("数据库迁移完成") PY echo "== [4/5] 重启后端服务 ==" $SUDO systemctl restart myblog sleep 3 if ! $SUDO systemctl is-active --quiet myblog; then echo "错误:myblog 服务未正常运行,请查看日志:journalctl -u myblog -n 50" >&2 exit 1 fi echo "myblog 服务运行正常" echo "== [5/5] 验证 ==" echo "--- 后端接口(应返回 success:true) ---" curl -s http://127.0.0.1:8080/api/article/list | head -c 200 echo echo "--- 新前端是否生效(发布管理弹窗标记 pub-confirm 数量) ---" grep -c "pub-confirm" "$BLOG_DIR/frontend/manage.js" || true if [ -n "$DOMAIN" ]; then echo "--- 线上 HTTPS($DOMAIN) ---" curl -sk -o /dev/null -w "%{http_code}\n" "https://$DOMAIN/" || echo "HTTPS 检查失败(可能是证书或网络问题)" fi echo "更新完成。如需回滚:备份在 $BACKUP_DIR/"