128 lines
5.0 KiB
Bash
128 lines
5.0 KiB
Bash
#!/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/"
|