Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
# MyBlog 部署指南(Ubuntu + Nginx + FastAPI)
|
||||
|
||||
本指南覆盖:服务器初始化 → 上传项目 → 安装依赖 → 配置 Nginx / systemd → 开启 HTTPS → 验证上线。
|
||||
假设你的阿里云服务器为 **Ubuntu 22.04/24.04**,已有公网 IP 与可登录的账号(root 或 sudo 用户)。
|
||||
|
||||
> 架构速览:Nginx 提供前端静态文件与 `/uploads/`,并把 `/api/*` 反代到
|
||||
> `127.0.0.1:8080` 的 FastAPI(只监听本机,公网无法直连);后端使用 SQLite,数据在
|
||||
> `/var/www/blog/backend/blog.db`。
|
||||
|
||||
---
|
||||
|
||||
## 一、本地准备:确认要上传的内容
|
||||
|
||||
项目根目录(`D:\MyBlog`)需要上传:
|
||||
|
||||
| 路径 | 说明 | 是否上传 |
|
||||
| --- | --- | --- |
|
||||
| `frontend/` | 前端全部文件(index.html / *.js / style.css) | ✅ |
|
||||
| `backend/` | 后端代码(不含 `blog.db`、`__pycache__`) | ✅ |
|
||||
| `uploads/` | 上传目录(可传已有文件,如头像 favicon.png) | ✅(可为空) |
|
||||
| `requirements.txt` | Python 依赖清单 | ✅ |
|
||||
| `.env.example` | 环境变量模板 | ✅ |
|
||||
| `README.md` | 项目说明 | ✅ |
|
||||
| `deploy/` | 部署文件(nginx.conf / myblog.service / 本指南) | ✅ |
|
||||
| `.env` | 含密钥与邮箱授权码,**不要上传**,到服务器上手动创建 | ❌ |
|
||||
| `backend/blog.db` | 本地测试数据库,**不要上传**,服务器上重新初始化 | ❌ |
|
||||
|
||||
### 1.1 Windows 打包并上传(自带 tar 与 scp)
|
||||
|
||||
```powershell
|
||||
# 在项目目录 D:\MyBlog 下执行:打包(排除本地环境文件)
|
||||
tar -czf myblog.tar.gz frontend backend uploads deploy requirements.txt .env.example README.md
|
||||
|
||||
# 上传到服务器 /tmp(把 root@IP 换成你的实际账号和公网 IP)
|
||||
scp myblog.tar.gz root@你的公网IP:/tmp/
|
||||
```
|
||||
|
||||
> 提示:Windows 10 1803+ 自带 `tar` / `scp`。也可用 WinSCP / Xftp 直接拖拽,
|
||||
> 或使用 Git Bash / WSL 里的 `rsync -avz --exclude='.env' --exclude='backend/blog.db' ./ root@IP:/var/www/blog/`。
|
||||
|
||||
---
|
||||
|
||||
## 二、阿里云安全组开放端口
|
||||
|
||||
登录阿里云控制台 → ECS 实例 → 安全组 → 配置规则 → 入方向,**仅开放以下端口**:
|
||||
|
||||
| 端口 | 用途 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| 22 | SSH 远程管理 | 建议“指定源”只放行你的家庭/办公 IP |
|
||||
| 80 | HTTP(Nginx) | 0.0.0.0/0 |
|
||||
| 443 | HTTPS(Nginx) | 0.0.0.0/0(配合 certbot) |
|
||||
| 25565 | Minecraft(独立服务,与博客无关) | 按需开放 |
|
||||
|
||||
⚠️ **8080 端口千万不要开放**:FastAPI 只监听 `127.0.0.1:8080`,公网即使访问 8080 也会被拒绝;
|
||||
如果安全组放行 8080 而 Nginx 代理配置有误,等于把后端裸奔在公网上。
|
||||
|
||||
---
|
||||
|
||||
## 三、服务器初始化
|
||||
|
||||
SSH 登录服务器后执行:
|
||||
|
||||
```bash
|
||||
# 1. 系统更新 + 安装 Nginx / Python3 / venv / pip
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y nginx python3 python3-venv python3-pip
|
||||
|
||||
# 2. 创建博客专用系统账号(禁止登录,只用于运行后端)
|
||||
sudo useradd -r -m -s /usr/sbin/nologin blog
|
||||
|
||||
# 3. 创建部署目录
|
||||
sudo mkdir -p /var/www/blog
|
||||
sudo chown -R blog:blog /var/www/blog
|
||||
|
||||
# 4. 解压上传的项目包(由 /tmp 解压到 /var/www/blog)
|
||||
sudo mkdir -p /var/www/blog
|
||||
cd /tmp && sudo tar -xzf myblog.tar.gz -C /var/www/blog/
|
||||
sudo chown -R blog:blog /var/www/blog
|
||||
|
||||
# 5. 准备证书校验目录(Nginx 配置里用到)
|
||||
sudo mkdir -p /var/www/blog/.well-known/acme-challenge
|
||||
sudo chown -R blog:blog /var/www/blog/.well-known
|
||||
|
||||
# 6. 确认前端文件就位(页面引用绝对路径 /main.js,root 必须指向 frontend)
|
||||
ls /var/www/blog/frontend/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、配置 .env(服务器上手动创建)
|
||||
|
||||
`.env` 含密钥与授权码,必须到服务器上创建,不要从本地直接上传:
|
||||
|
||||
```bash
|
||||
cd /var/www/blog
|
||||
sudo -u blog cp .env.example .env
|
||||
sudo -u blog nano .env # 或 vi
|
||||
```
|
||||
|
||||
必填项(`nano` 里按 `Ctrl+O` 保存,`Ctrl+X` 退出):
|
||||
|
||||
```ini
|
||||
# 强随机密钥:在服务器上执行下面命令生成,然后粘贴进来
|
||||
# python3 -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
JWT_SECRET=这里粘贴生成的随机密钥
|
||||
|
||||
# 博主账号(首次执行 python -m backend.seed 时创建)
|
||||
BLOGGER_USERNAME=孤竹居士
|
||||
BLOGGER_EMAIL=guzhujushi2008@163.com
|
||||
BLOGGER_PASSWORD=你的登录密码
|
||||
|
||||
# SMTP 邮箱验证码(163 邮箱 + 授权码)
|
||||
SMTP_HOST=smtp.163.com
|
||||
SMTP_PORT=465
|
||||
SMTP_USER=guzhujushi2008@163.com
|
||||
SMTP_AUTH_CODE=你的163授权码
|
||||
EMAIL_FROM=guzhujushi2008@163.com
|
||||
```
|
||||
|
||||
其余可选变量(限流、验证码、评论长度等)不填会使用默认值,说明见 `.env.example`。
|
||||
设置文件权限,防止他人读取密钥:
|
||||
|
||||
```bash
|
||||
sudo chmod 600 /var/www/blog/.env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、安装依赖 + 初始化数据库
|
||||
|
||||
```bash
|
||||
# 1. 创建虚拟环境并安装依赖(建议先换成国内 pip 镜像,速度更快)
|
||||
cd /var/www/blog
|
||||
sudo -u blog python3 -m venv venv
|
||||
sudo -u blog venv/bin/pip install -r requirements.txt
|
||||
# 可选加速:-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 2. 初始化数据库表(自动创建 backend/blog.db)
|
||||
sudo -u blog venv/bin/python -m backend.database
|
||||
|
||||
# 3. 创建唯一博主账号(读取 .env 里的 BLOGGER_*,重复执行会跳过)
|
||||
sudo -u blog venv/bin/python -m backend.seed
|
||||
|
||||
# 4. 检查上传目录可写
|
||||
sudo -u blog mkdir -p uploads/avatar uploads/article uploads/project
|
||||
sudo chown -R blog:blog /var/www/blog/uploads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、部署 Nginx 配置
|
||||
|
||||
```bash
|
||||
# 1. 复制配置文件(内容见 deploy/nginx.conf)
|
||||
sudo cp /var/www/blog/deploy/nginx.conf /etc/nginx/sites-available/myblog
|
||||
|
||||
# 2. 把 server_name 改成你的域名(或公网 IP)
|
||||
sudo nano /etc/nginx/sites-available/myblog
|
||||
|
||||
# 3. 启用站点(删除默认站点,避免冲突)
|
||||
sudo rm -f /etc/nginx/sites-enabled/default
|
||||
sudo ln -s /etc/nginx/sites-available/myblog /etc/nginx/sites-enabled/
|
||||
|
||||
# 4. 校验配置并重载
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、部署 systemd 服务
|
||||
|
||||
```bash
|
||||
# 1. 复制服务文件(内容见 deploy/myblog.service)
|
||||
sudo cp /var/www/blog/deploy/myblog.service /etc/systemd/system/
|
||||
|
||||
# 2. 加载并启动
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now myblog
|
||||
|
||||
# 3. 确认状态:Active: active (running) 表示成功
|
||||
sudo systemctl status myblog
|
||||
```
|
||||
|
||||
**手动启动方式(调试用,生产请用 systemd)**:
|
||||
|
||||
```bash
|
||||
cd /var/www/blog
|
||||
sudo -u blog /var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、开启 HTTPS(Let's Encrypt,免费证书)
|
||||
|
||||
```bash
|
||||
# 1. 安装 certbot 的 Nginx 插件
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
|
||||
# 2. 一键签发并自动改写 Nginx 配置(会自动加 443 监听与自动续期)
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
|
||||
# 3. 证书自动续期(certbot 已内置定时任务,可验证)
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
HTTPS 配置完成后:
|
||||
- 80 端口会 301 跳转到 443(certbot 自动处理)
|
||||
- 记得把安全组 443 放行(见第二节)
|
||||
- 建议把 `.env` 里的 `EMAIL_FROM` 等保持不变即可
|
||||
|
||||
---
|
||||
|
||||
## 九、上线验证清单
|
||||
|
||||
```bash
|
||||
# 1. 后端接口是否通(应返回 {"success":true,...})
|
||||
curl http://127.0.0.1:8080/api/article/list
|
||||
|
||||
# 2. 前端首页是否可访问(应返回 index.html)
|
||||
curl -I http://127.0.0.1:8080/ # 本机访问后端正常(证明服务活着)
|
||||
curl -I http://你的公网IP/ # 公网走 Nginx 访问首页
|
||||
# 公网直接访问 http://公网IP:8080 应超时/拒绝(8080 未放行且只监听本机)
|
||||
|
||||
# 3. SPA 刷新是否正常(直接访问子路由应回退到 index.html)
|
||||
curl -I http://你的公网IP/partition/4
|
||||
|
||||
# 4. 上传目录是否可访问(favicon 应为 200)
|
||||
curl -I http://你的公网IP/uploads/avatar/favicon.png
|
||||
|
||||
# 5. 查看后端日志是否有报错
|
||||
journalctl -u myblog -f
|
||||
|
||||
# 6. 浏览器验证全流程:
|
||||
# 打开首页 → 注册/登录 → 文章列表 → 文章详情 → 评论/点赞 → 好友申请
|
||||
# (博主)登录 → 发文管理 → 上传头像/封面/项目 → 审批好友申请
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、常见问题
|
||||
|
||||
| 现象 | 排查方向 |
|
||||
| --- | --- |
|
||||
| 页面能打开但接口报错 | `journalctl -u myblog` 看后端日志;确认 `.env` 里 `JWT_SECRET` 已配置 |
|
||||
| 上传图片 404 | `uploads/` 目录权限:`sudo chown -R blog:blog /var/www/blog/uploads` |
|
||||
| 刷新子页面 404 | Nginx `try_files $uri $uri/ /index.html;` 是否在 `location /` 中 |
|
||||
| 博主登录失败多次被锁 | 安全组限流策略(默认 5 次/15 分钟),等窗口过期或调大 `.env` 阈值 |
|
||||
| 验证码发不出去 | 检查 `.env` 的 SMTP 配置与 163 授权码;服务器 465 端口出方向是否被云安全组限制 |
|
||||
| 修改代码后不生效 | 后端:`sudo systemctl restart myblog`;前端:用 `deploy/update.ps1` 部署(自动换版本号,浏览器强制拉新文件);若仍异常,Ctrl+F5 强刷一次 |
|
||||
| 8080 被公网扫到 | 确认 uvicorn 只监听 `127.0.0.1`,且安全组未放行 8080 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、日常运维
|
||||
|
||||
```bash
|
||||
# 查看状态 / 重启 / 日志
|
||||
sudo systemctl status myblog
|
||||
sudo systemctl restart myblog
|
||||
journalctl -u myblog -n 100
|
||||
|
||||
# 备份数据库与上传文件(SQLite 单文件 + 目录)
|
||||
sudo tar -czf backup_$(date +%F).tar.gz -C /var/www/blog backend/blog.db uploads
|
||||
|
||||
# 升级代码流程:见下方「更新代码到服务器(增量同步 + 自动重启)」
|
||||
```
|
||||
|
||||
### 更新代码到服务器(增量同步 + 自动重启)
|
||||
|
||||
修改完本地代码后,把改动同步到服务器并重启后端,**一条命令完成**:
|
||||
|
||||
```bash
|
||||
# 在本地 Git Bash / WSL 的项目目录执行(root 登录时无需 sudo)
|
||||
rsync -avz \
|
||||
--exclude='.env' \
|
||||
--exclude='backend/blog.db' \
|
||||
--exclude='venv' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='uploads/' \
|
||||
./ root@你的公网IP:/var/www/blog/ \
|
||||
&& ssh root@你的公网IP 'systemctl restart myblog'
|
||||
```
|
||||
|
||||
命令做了什么:
|
||||
- `rsync` 只把**本地改动过的文件**增量同步到服务器,未变化的自动跳过;
|
||||
`&&` 表示同步成功后继续通过 SSH 执行重启 → 改完代码跑这一条,就是一次完整更新。
|
||||
- 只改前端(`frontend/`)时后端无需重启;重启约 1 秒、无副作用,想省心可每次都执行。
|
||||
|
||||
注意:
|
||||
- **不要加 `--delete`**:会删除服务器上本地没有的文件(例如用户通过网页上传的图片)。
|
||||
- 排除 `uploads/`:它是服务器上的运行时数据(网页上传的文件只存在于服务器)。
|
||||
若本地新增了静态资源(如头像 favicon)需要上传,去掉 `--exclude='uploads/'` 再执行。
|
||||
- 非 root 用户:把 `root@` 换成你的用户名,并先在服务器上配置该命令的免密 sudo:
|
||||
|
||||
```bash
|
||||
# 服务器上执行(username 换成你的登录用户)
|
||||
sudo visudo -f /etc/sudoers.d/blog-update
|
||||
# 在文件里加入下面一行后保存:
|
||||
# username ALL=(ALL) NOPASSWD: /bin/systemctl restart myblog
|
||||
```
|
||||
|
||||
之后本地执行:
|
||||
|
||||
```bash
|
||||
rsync -avz --exclude='.env' --exclude='backend/blog.db' --exclude='venv' --exclude='__pycache__' --exclude='uploads/' ./ username@你的公网IP:/var/www/blog/ && ssh username@你的公网IP 'sudo systemctl restart myblog'
|
||||
```
|
||||
|
||||
验证更新是否生效:
|
||||
|
||||
```bash
|
||||
ssh root@你的公网IP 'systemctl status myblog'
|
||||
curl -s http://你的公网IP/api/article/list # 应返回 {"success":true,...}
|
||||
```
|
||||
|
||||
|
||||
### 一键更新脚本(推荐,Windows 上直接用)
|
||||
|
||||
项目自带两个脚本,不需要 rsync / Git Bash,Windows 打开 PowerShell 即可:
|
||||
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `deploy/update.ps1` | 本地一键脚本:打包代码 -> 上传 -> 触发服务器更新 |
|
||||
| `deploy/update_server.sh` | 服务器端脚本:备份 -> 解压 -> 数据库迁移 -> 重启 -> 验证 |
|
||||
| `deploy/deploy.conf` | 配置服务器地址与域名(`SERVER=` / `DOMAIN=`) |
|
||||
|
||||
用法:
|
||||
|
||||
```powershell
|
||||
# 在项目根目录 D:\MyBlog 下执行
|
||||
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1
|
||||
|
||||
# 或指定服务器(覆盖 deploy.conf)
|
||||
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -Server root@8.145.36.108
|
||||
|
||||
# 需要把本地 uploads/ 一并同步时(首次部署 / 新增静态资源)
|
||||
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -IncludeUploads
|
||||
```
|
||||
|
||||
脚本会做的事(与上面 rsync 方案等价且更省心):
|
||||
- 打包时自动排除 `.env`、`backend/blog.db`、`__pycache__`、`venv`,默认也排除 `uploads/`
|
||||
- 每次更新前在服务器上自动备份到 `/root/blog-backups/`(含数据库与上传文件),可回滚
|
||||
- 自动执行幂等数据库迁移(补齐 `articles.category` 等新列),并重启 `myblog` 服务
|
||||
- 结束后自动验证后端接口与线上 HTTPS 是否正常
|
||||
|
||||
### 前端缓存自动清理(更新后无需手动 Ctrl+F5)
|
||||
|
||||
页面引用的 `style.css` / `main.js` 已带版本号 `?v=__VERSION__`:
|
||||
|
||||
- 每次执行 `deploy/update.ps1` 部署时,服务器端会自动把 `__VERSION__` 替换成**当前时间戳**
|
||||
- 版本号一变化,浏览器就会重新下载最新 JS/CSS,旧缓存自动失效,无需手动清理
|
||||
- Nginx 同时对前端文件返回 `Cache-Control: no-cache`(每次重新校验),双保险
|
||||
- 如需手动验证:`curl -s https://你的域名/ | grep main.js` 应能看到带版本号的引用
|
||||
|
||||
### SSH 白名单自动更新(家庭公网 IP 变化不锁死)
|
||||
|
||||
家庭宽带公网 IP 经常变化,若安全组 22 端口只放行固定 IP,换 IP 后 SSH 会被自己挡在门外。
|
||||
本项目提供“家庭端定时上报 + 服务器端自动更新安全组”的完整方案,文件均在 `deploy/ipwatch/`。
|
||||
|
||||
```
|
||||
家庭电脑 report.ps1(每 30 分钟)---> 博客 /myip 获取当前公网 IP
|
||||
| IP 有变化时
|
||||
└--> POST /api/ipwatch/report(携带密钥)
|
||||
服务器调用阿里云 ECS API:先新增新 IP 规则,成功后再删除旧规则(绝不锁死 SSH)
|
||||
```
|
||||
|
||||
**服务器端配置(一次性):**
|
||||
|
||||
1. 在服务器 `.env` 中追加以下变量(AccessKey 只放服务器,不要发给家庭端):
|
||||
|
||||
```ini
|
||||
# 生成随机上报密钥:python3 -c "import secrets; print(secrets.token_urlsafe(24))"
|
||||
IPWATCH_SECRET=这里粘贴随机密钥
|
||||
ALIYUN_AK_ID=你的RAM子账号AK
|
||||
ALIYUN_AK_SECRET=你的RAM子账号SK
|
||||
IPWATCH_SECURITY_GROUP_ID=sg-0jl65y8luej10xz8neli
|
||||
IPWATCH_REGION=cn-wulanchabu
|
||||
IPWATCH_PORT=22
|
||||
```
|
||||
|
||||
2. RAM 子账号只授予该安全组的查询/增/删规则权限(`ecs:DescribeSecurityGroupAttribute`、
|
||||
`ecs:AuthorizeSecurityGroup`、`ecs:RevokeSecurityGroup`),不要使用主账号密钥。
|
||||
3. 重新部署代码后重启服务:`sudo systemctl restart myblog`
|
||||
4. 验证服务状态:`curl -s http://127.0.0.1:8080/api/ipwatch/status`,应返回 `configured: true`。
|
||||
|
||||
**家庭端安装(Windows,一次性):**
|
||||
|
||||
```powershell
|
||||
cd deploy\ipwatch
|
||||
Copy-Item ipwatch.conf.example ipwatch.conf
|
||||
# 用记事本编辑 ipwatch.conf:SECRET 填与服务器 IPWATCH_SECRET 相同的值
|
||||
powershell -ExecutionPolicy Bypass -File .\install.ps1 # 注册每 30 分钟的计划任务
|
||||
Get-Content .\ipwatch.log # 查看首次运行结果
|
||||
```
|
||||
|
||||
**验证方法:**
|
||||
|
||||
- 手动模拟一次 IP 变化:调用 `POST /api/ipwatch/report` 上报一个测试 IP,再到阿里云控制台
|
||||
或 `deploy/ipwatch` 检查安全组 22 端口规则,确认“先加后删”;随后再上报回真实 IP。
|
||||
- 说明:测试期间新规则替换旧规则,SSH 会短暂不可用,测试后必须恢复真实 IP。
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- 安全组中其它端口(80/443/25565 等)和 `0.0.0.0/0` 规则不会被脚本触碰,
|
||||
它只操作“tcp 22/22 且来源为单个 IP(/32)”的规则。
|
||||
- 上报接口按来源 IP 限流(默认 60 秒一次,`IPWATCH_REPORT_INTERVAL_SECONDS` 可调)。
|
||||
- 若上报密钥泄露:改服务器 `.env` 的 `IPWATCH_SECRET` → 重启服务 → 同步改家庭端 `ipwatch.conf`。
|
||||
- 阿里云 AccessKey 建议定期在 RAM 控制台轮换,轮换后同步更新服务器 `.env`。
|
||||
|
||||
|
||||
### JS→WASM 预编译(把上传项目的 JS 编译为 WASM)
|
||||
|
||||
后端提供“把项目演示目录里的 .js 预编译为 .wasm”的功能(基于 Javy / QuickJS),
|
||||
编译产物与报告存放在 `uploads/demos/{项目id}/wasm/`。
|
||||
|
||||
**重要说明(先看这里):**
|
||||
|
||||
- 浏览器页面**仍然运行原始 JS**。Javy 产物是“QuickJS 解释器 + 字节码”,不能操作
|
||||
DOM,速度也不如浏览器自带的 V8 JIT,所以它**不能替代**演示页里的 JS。
|
||||
- 这个功能的真正价值是**服务端沙箱执行**:把不可信的 JS 关进 WASM 沙箱里运行,
|
||||
隔离文件/网络/系统访问(符合“禁止直接运行用户上传代码”的安全规范)。
|
||||
|
||||
**安装编译工具(一次性,服务器上执行):**
|
||||
|
||||
```bash
|
||||
cd /var/www/blog
|
||||
bash deploy/install_javy.sh # 下载约 14MB,安装到 /usr/local/bin/javy
|
||||
javy --version # 验证:应输出 javy 9.1.0
|
||||
```
|
||||
|
||||
体积与性能分析:
|
||||
|
||||
- Javy 官方 Linux 工具包:约 14MB(下载)/ 约 40MB(解压后),磁盘足够即可。
|
||||
- 编译产物大小:每个 .wasm 约 300KB~1MB(内含 QuickJS 运行时快照 + 你的 JS 字节码)。
|
||||
- 编译速度:普通几十 KB 的 JS 文件 1 秒内完成;总耗时与文件数成正比。
|
||||
- 运行性能:QuickJS 是解释器,同类计算基准通常比 V8 JIT 慢数倍到十几倍;
|
||||
用于沙箱隔离/短任务没问题,不适合做重计算。
|
||||
|
||||
**下载压缩包也会被一起打包为 wasm:**
|
||||
|
||||
- 每次预编译时,除了演示目录里的 .js,还会把项目的下载压缩包(uploads/project/{uuid}.zip)
|
||||
内的全部 JS 合并打包为单个 wasm,输出到同目录 `uploads/project/{uuid}.wasm`(可直接下载)。
|
||||
- 说明:zip 本身不是 JS,无法直接编译;这里是把 zip 内的 .js 按文件名排序拼接成一个
|
||||
临时 bundle 再交给 Javy,产物是“压缩包的 wasm 版本”(演示页仍运行原始 JS)。
|
||||
- 总量上限默认 10MB(`WASM_BUNDLE_MAX_SIZE_MB` 可调),超过会跳过打包并在报告中注明。
|
||||
|
||||
**使用方式(任选其一):**
|
||||
|
||||
```bash
|
||||
# 方式一:服务器上手动触发(推荐先用这个验证)
|
||||
bash deploy/precompile_wasm.sh 1
|
||||
|
||||
# 方式二:博主登录后调用接口
|
||||
curl -X POST http://127.0.0.1:8080/api/project/1/precompile -H "Authorization: Bearer <token>"
|
||||
|
||||
# 查看最近一次编译报告(公开接口)
|
||||
curl -s http://127.0.0.1:8080/api/project/1/wasm-report
|
||||
```
|
||||
|
||||
**可选配置(追加到服务器 .env,重启生效):**
|
||||
|
||||
```ini
|
||||
WASM_COMPILE_ENABLED=true # 总开关
|
||||
JAVY_PATH=/usr/local/bin/javy # 编译工具路径
|
||||
WASM_MAX_JS_SIZE_MB=3 # 单个 JS 超过 3MB 跳过
|
||||
WASM_TIMEOUT_SECONDS=60 # 单文件编译超时
|
||||
```
|
||||
|
||||
运行验证(可选):编译产物可用 wasmtime 执行(服务器已装 v47.0.3,路径 /usr/local/bin/wasmtime):
|
||||
|
||||
`ash
|
||||
wasmtime run uploads/demos/1/wasm/data.wasm # 纯数据脚本可正常退出
|
||||
`
|
||||
|
||||
注意:Javy 下载源是 GitHub Releases,服务器需能访问 GitHub;若失败可在本机下载后
|
||||
`scp` 到服务器再解压安装(包名:javy-x86_64-linux-v9.1.0.gz)。
|
||||
@@ -0,0 +1,5 @@
|
||||
# MyBlog 部署配置(供 update.ps1 读取,可自行修改)
|
||||
# 服务器地址:SSH 登录格式 user@公网IP(必须已配置 SSH 免密登录)
|
||||
SERVER=root@8.145.36.108
|
||||
# 域名:用于更新后验证 HTTPS 是否正常;没有域名可留空
|
||||
DOMAIN=guzhujushi.cn
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 安装 Javy(JS→WASM 编译工具)到服务器
|
||||
# ------------------------------------------------------------
|
||||
# 用法:bash deploy/install_javy.sh
|
||||
# 作用:从 GitHub 官方 Releases 下载 Javy(约 14MB 单文件),
|
||||
# 校验 SHA256 后安装到 /usr/local/bin/javy
|
||||
# 幂等:已安装且版本一致时直接跳过,可重复执行
|
||||
# 体积说明:下载包约 14MB,解压后约 40MB,仅占磁盘少量空间
|
||||
# ============================================================
|
||||
set -eu
|
||||
|
||||
JAVY_VERSION="${JAVY_VERSION:-9.1.0}"
|
||||
JAVY_BIN="/usr/local/bin/javy"
|
||||
URL="https://github.com/bytecodealliance/javy/releases/download/v${JAVY_VERSION}/javy-x86_64-linux-v${JAVY_VERSION}.gz"
|
||||
SHA_URL="${URL}.sha256"
|
||||
|
||||
# 已安装且版本匹配 -> 跳过(避免重复下载)
|
||||
if [ -x "$JAVY_BIN" ] && "$JAVY_BIN" --version 2>/dev/null | grep -q "$JAVY_VERSION"; then
|
||||
echo "Javy ${JAVY_VERSION} 已安装,跳过下载:$($JAVY_BIN --version)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "== 下载 Javy ${JAVY_VERSION}(约 14MB)=="
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
curl -fL -sS -o "$TMP/javy.gz" "$URL"
|
||||
curl -fL -sS -o "$TMP/javy.gz.sha256" "$SHA_URL"
|
||||
|
||||
echo "== 校验 SHA256 =="
|
||||
EXPECT=$(awk '{print $1}' "$TMP/javy.gz.sha256")
|
||||
ACTUAL=$(sha256sum "$TMP/javy.gz" | awk '{print $1}')
|
||||
if [ "$EXPECT" != "$ACTUAL" ]; then
|
||||
echo "校验失败:期望 $EXPECT,实际 $ACTUAL" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "校验通过"
|
||||
|
||||
gunzip -f "$TMP/javy.gz"
|
||||
install -m 0755 "$TMP/javy" "$JAVY_BIN"
|
||||
echo "安装完成:$("$JAVY_BIN" --version)"
|
||||
echo "路径:$JAVY_BIN"
|
||||
@@ -0,0 +1,39 @@
|
||||
# MyBlog IP 白名单自动更新(家庭端)
|
||||
|
||||
## 这是什么
|
||||
|
||||
家庭宽带的公网 IP 会不定期变化。阿里云安全组 22 端口如果只放行固定 IP,
|
||||
换 IP 后你就会被自己挡在门外。
|
||||
|
||||
这套方案让家庭电脑定时检测自己的公网 IP,一旦变化就通知服务器,
|
||||
服务器自动调用阿里云 API 把安全组 22 端口白名单更新为最新 IP。
|
||||
|
||||
流程:
|
||||
1. report.ps1 请求 博客/myip 获取当前公网 IP
|
||||
2. 与上次记录比较,有变化时 POST /api/ipwatch/report
|
||||
3. 服务器调用阿里云 API 更新安全组(先加新规则、后删旧规则)
|
||||
|
||||
## 家庭端配置步骤(Windows)
|
||||
|
||||
1. 复制 ipwatch.conf.example 为 ipwatch.conf,填写:
|
||||
- SERVER:博客地址(默认 https://guzhujushi.cn)
|
||||
- SECRET:与服务器 .env 中 IPWATCH_SECRET 相同
|
||||
2. 运行安装脚本(注册每 30 分钟的计划任务并立即执行一次):
|
||||
powershell -ExecutionPolicy Bypass -File .\install.ps1
|
||||
3. 查看日志确认成功:
|
||||
Get-Content .\ipwatch.log
|
||||
正常会看到:白名单更新成功:你的公网IP(服务器:白名单更新成功)
|
||||
或:公网 IP 未变化:xxx(表示无需更新,正常)。
|
||||
|
||||
## 手动运行 / 卸载
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File .\report.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 家庭端只保存 IPWATCH_SECRET(随机字符串);阿里云 AccessKey 只存在服务器
|
||||
.env 中,不要复制到家庭端或提交到 git。
|
||||
- 如果 IPWATCH_SECRET 泄露:登录服务器改 .env 里的值,再同步修改本目录
|
||||
ipwatch.conf,最后重启后端服务即可。
|
||||
- 服务器端每次上报限流 60 秒一次,且只增删 22 端口 /32 单 IP 规则。
|
||||
@@ -0,0 +1,33 @@
|
||||
# ============================================================
|
||||
# 安装 MyBlog IP 白名单定时任务(家庭 Windows 电脑)
|
||||
# 作用:注册 Windows 计划任务,每 30 分钟自动运行 report.ps1
|
||||
# 前置:已复制 ipwatch.conf.example 为 ipwatch.conf 并填写
|
||||
# 用法:powershell -ExecutionPolicy Bypass -File .\install.ps1
|
||||
# 说明:计划任务在“当前登录用户”下运行;如需开机无登录也运行,
|
||||
# 请右键任务 -> 属性勾选“不管用户是否登录都要运行”
|
||||
# ============================================================
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = $PSScriptRoot
|
||||
$Report = Join-Path $ScriptDir "report.ps1"
|
||||
$ConfFile = Join-Path $ScriptDir "ipwatch.conf"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ConfFile)) {
|
||||
Write-Host "缺少 ipwatch.conf:请先复制 ipwatch.conf.example 并填写 SERVER / SECRET" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$TaskName = "MyBlogIPWatch"
|
||||
# /TR 里对带空格的脚本路径加引号,PowerShell 会整体作为一个参数传给 schtasks
|
||||
$Action = "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$Report`""
|
||||
|
||||
& schtasks /Create /F /TN $TaskName /SC MINUTE /MO 30 /TR $Action | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "创建计划任务失败,请确认以管理员身份运行或检查 schtasks 输出" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 创建后立即运行一次,马上验证配置是否正确
|
||||
& schtasks /Run /TN $TaskName | Out-Null
|
||||
Write-Host "计划任务 $TaskName 已创建:每 30 分钟自动检查公网 IP。"
|
||||
Write-Host "日志文件:$(Join-Path $ScriptDir 'ipwatch.log')"
|
||||
Write-Host "(首次运行结果可在日志中查看;卸载请运行 uninstall.ps1)"
|
||||
@@ -0,0 +1,7 @@
|
||||
# MyBlog IP 白名单自动上报配置(家庭 Windows 电脑)
|
||||
# 用法:复制本文件为 ipwatch.conf 并填写下面两项,再运行 install.ps1
|
||||
#
|
||||
# SERVER:博客地址(必须能访问 /myip 与 /api/ipwatch/report),一般填域名
|
||||
# SECRET:必须与服务器 .env 里的 IPWATCH_SECRET 完全一致
|
||||
SERVER=https://guzhujushi.cn
|
||||
SECRET=请填写与服务器 IPWATCH_SECRET 相同的值
|
||||
@@ -0,0 +1,77 @@
|
||||
# ============================================================
|
||||
# MyBlog IP 白名单自动上报脚本(在家庭 Windows 电脑上运行)
|
||||
# 作用:读取当前公网 IP -> 与上次记录比较 -> 有变化就通知服务器
|
||||
# 更新阿里云安全组 22 端口白名单(避免换 IP 后 SSH 连不上)
|
||||
# 用法:
|
||||
# 手动运行: powershell -ExecutionPolicy Bypass -File .\report.ps1
|
||||
# 定时运行: 先运行 install.ps1(注册每 30 分钟执行一次的计划任务)
|
||||
# 退出码:0=成功(含无需更新) 1=配置错误 2=取IP失败 3=IP不合法 4=服务器拒绝 5=网络错误
|
||||
# ============================================================
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = $PSScriptRoot
|
||||
$ConfFile = Join-Path $ScriptDir "ipwatch.conf"
|
||||
$LastFile = Join-Path $ScriptDir "lastip.txt"
|
||||
$LogFile = Join-Path $ScriptDir "ipwatch.log"
|
||||
|
||||
# ---------- 1. 读取配置 ----------
|
||||
$Server = ""
|
||||
$Secret = ""
|
||||
if (Test-Path -LiteralPath $ConfFile) {
|
||||
Get-Content -LiteralPath $ConfFile -Encoding UTF8 | ForEach-Object {
|
||||
$line = $_.Trim()
|
||||
if ($line -match '^SERVER\s*=\s*(.+)$') { $Server = $Matches[1].Trim().Trim('"') }
|
||||
elseif ($line -match '^SECRET\s*=\s*(.+)$') { $Secret = $Matches[1].Trim().Trim('"') }
|
||||
}
|
||||
}
|
||||
if (-not $Server -or -not $Secret) {
|
||||
Write-Host "配置不完整:请复制 ipwatch.conf.example 为 ipwatch.conf,并填写 SERVER 与 SECRET" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------- 2. 日志工具 ----------
|
||||
function Write-Log([string]$message) {
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $message"
|
||||
try { Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 } catch { }
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
# ---------- 3. 获取当前公网 IP(走博客的 /myip 接口) ----------
|
||||
# 旧系统默认不启用 TLS1.2,先强制开启,否则 HTTPS 请求会失败
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
try {
|
||||
$current = (Invoke-RestMethod -Uri "$Server/myip" -TimeoutSec 20).ToString().Trim()
|
||||
} catch {
|
||||
Write-Log "获取公网 IP 失败:$($_.Exception.Message)"
|
||||
exit 2
|
||||
}
|
||||
if ($current -notmatch '^\d{1,3}(\.\d{1,3}){3}$') {
|
||||
Write-Log "服务器返回的不是合法 IP:$current"
|
||||
exit 3
|
||||
}
|
||||
|
||||
# ---------- 4. 与上次记录比较,没变化就不打扰服务器 ----------
|
||||
$last = ""
|
||||
if (Test-Path -LiteralPath $LastFile) {
|
||||
$last = (Get-Content -LiteralPath $LastFile -Raw -Encoding UTF8).Trim()
|
||||
}
|
||||
if ($current -eq $last) {
|
||||
Write-Host "公网 IP 未变化:$current"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------- 5. IP 有变化:上报给服务器,由服务器更新安全组 ----------
|
||||
try {
|
||||
$body = @{ secret = $Secret; ip = $current } | ConvertTo-Json
|
||||
$resp = Invoke-RestMethod -Uri "$Server/api/ipwatch/report" -Method Post -Body $body -ContentType "application/json; charset=utf-8" -TimeoutSec 40
|
||||
if ($resp.success) {
|
||||
Set-Content -LiteralPath $LastFile -Value $current -Encoding UTF8
|
||||
Write-Log "白名单更新成功:$current(服务器:$($resp.message))"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Log "服务器返回失败:$($resp.message)"
|
||||
exit 4
|
||||
}
|
||||
} catch {
|
||||
Write-Log "上报失败:$($_.Exception.Message)"
|
||||
exit 5
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# 卸载 MyBlog IP 白名单定时任务
|
||||
$ErrorActionPreference = "Stop"
|
||||
& schtasks /Delete /F /TN "MyBlogIPWatch" | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { Write-Host "计划任务 MyBlogIPWatch 已删除" }
|
||||
@@ -0,0 +1,49 @@
|
||||
# ============================================================
|
||||
# MyBlog 后端 systemd 服务
|
||||
# ------------------------------------------------------------
|
||||
# 部署位置:/etc/systemd/system/myblog.service
|
||||
# 生效命令:
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable --now myblog # 开机自启 + 立即启动
|
||||
# 常用命令:
|
||||
# sudo systemctl status myblog # 查看状态
|
||||
# sudo systemctl restart myblog # 重启
|
||||
# journalctl -u myblog -f # 跟踪日志
|
||||
# ============================================================
|
||||
|
||||
[Unit]
|
||||
Description=MyBlog FastAPI Backend
|
||||
# 等待网络就绪后再启动(uvicorn 需要绑定端口)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# 专用运行账号:系统用户、禁止登录,只拥有项目文件权限
|
||||
User=blog
|
||||
Group=blog
|
||||
|
||||
# 工作目录必须是项目根目录(backend 包、.env、uploads 都相对它定位)
|
||||
WorkingDirectory=/var/www/blog
|
||||
|
||||
# 启动命令:使用项目虚拟环境里的 uvicorn,只监听本机 8080(禁止公网直连)
|
||||
ExecStart=/var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
|
||||
|
||||
# 崩溃后 3 秒自动重启;开机自启由 enable 管理
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
# 环境变量说明:
|
||||
# .env 不需要写进 EnvironmentFile —— 应用启动时会在代码里
|
||||
# 自动读取项目根目录 .env(backend/database.py 的 load_dotenv),
|
||||
# 这样也能避免 systemd 解析中文值/注释行时出错。
|
||||
# 部署时请务必在 /var/www/blog/.env 中配置 JWT_SECRET,否则后端拒绝启动。
|
||||
|
||||
# ---------- 安全加固(可选) ----------
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
# 允许写入:上传目录(用户上传文件)与 backend 目录(SQLite 数据库 backend/blog.db)
|
||||
ReadWritePaths=/var/www/blog/uploads /var/www/blog/backend
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,106 @@
|
||||
# ============================================================
|
||||
# MyBlog Nginx 站点配置
|
||||
# ------------------------------------------------------------
|
||||
# 部署位置:/etc/nginx/sites-available/myblog
|
||||
# 启用命令:sudo ln -s /etc/nginx/sites-available/myblog /etc/nginx/sites-enabled/
|
||||
# 校验命令:sudo nginx -t
|
||||
# 重载命令:sudo systemctl reload nginx
|
||||
#
|
||||
# 目录约定(与全局架构一致):
|
||||
# /var/www/blog/frontend 前端静态文件(index.html / *.js / style.css)
|
||||
# /var/www/blog/uploads 上传文件(avatar / article / project)
|
||||
# /var/www/blog/backend 后端代码
|
||||
# /var/www/blog/.env 环境变量(应用启动时自动加载)
|
||||
# ============================================================
|
||||
|
||||
# ============ HTTP 80:普通访问 + 证书校验 ============
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
# 改成你的域名(或服务器公网 IP);certbot 会自动填入证书相关配置
|
||||
server_name your-domain.com;
|
||||
# 允许上传大小上限:头像最大 4MB、文章视频最大 100MB,留出余量(默认 1MB 会拦截上传)
|
||||
client_max_body_size 110m;
|
||||
|
||||
# ---------- 安全响应头(防点击劫持 / MIME 嗅探 / 泄露来源页) ----------
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# 说明:前端 HTML 内已有 CSP meta;如需更强防护(frame-ancestors 等),
|
||||
# 可在此追加:add_header Content-Security-Policy "default-src 'self'; ..." always;
|
||||
|
||||
# Let's Encrypt 证书签发校验目录(certbot --webroot 模式使用)
|
||||
# 需要先创建:sudo mkdir -p /var/www/blog/.well-known/acme-challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/blog;
|
||||
}
|
||||
|
||||
# ---------- /myip 返回客户端公网 IP(SSH 白名单自动更新用) ----------
|
||||
# 家庭端脚本先请求此地址获取当前公网 IP,变化后再调用 /api/ipwatch/report
|
||||
# 注意:certbot 生成 443 的 server 块时,需要把这一段也复制进去
|
||||
location = /myip {
|
||||
default_type text/plain;
|
||||
return 200 $remote_addr;
|
||||
}
|
||||
|
||||
# ---------- /api/* 反向代理到 FastAPI ----------
|
||||
# FastAPI 只监听 127.0.0.1:8080,公网无法直连,必须走 Nginx 代理
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# WASM 预编译等长任务可能超过 60s,放宽到 180s
|
||||
proxy_read_timeout 180s;
|
||||
}
|
||||
|
||||
# 说明:服务器 /etc/nginx/mime.types 需追加一行 application/wasm wasm;(已执行)
|
||||
# ---------- /uploads/* 静态文件 ----------
|
||||
# 头像、文章图片、项目压缩包由 Nginx 直接提供,不经过 Python
|
||||
location /uploads/ {
|
||||
alias /var/www/blog/uploads/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# ---------- /demo/* 在线演示(L0 静态托管) ----------
|
||||
# 上传的项目 zip 若为纯 HTML/CSS/JS,后端解压到 uploads/demos/{项目id}/,
|
||||
# 此处由 Nginx 直接提供(不经过 Python,禁止执行)
|
||||
location /demo/ {
|
||||
alias /var/www/blog/uploads/demos/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# ---------- 前端静态文件(SPA) ----------
|
||||
# root 必须指向 frontend 目录:页面里引用的是 /main.js /style.css 等绝对路径
|
||||
location / {
|
||||
root /var/www/blog/frontend;
|
||||
index index.html;
|
||||
# SPA 刷新支持:
|
||||
# 先找真实文件($uri),再找目录($uri/),
|
||||
# 都找不到就回退到 index.html,由前端 History API 路由接管
|
||||
try_files $uri $uri/ /index.html;
|
||||
# 每次重新校验,避免部署后浏览器继续用旧版 JS 导致页面渲染异常
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
}
|
||||
|
||||
# ============ HTTPS 443:用 certbot 一键开启 ============
|
||||
# 推荐方式(自动改写本配置文件并管理续期):
|
||||
# sudo apt install certbot python3-certbot-nginx
|
||||
# sudo certbot --nginx -d your-domain.com
|
||||
# 执行成功后,本文件会被自动加入 443 监听与证书路径,无需手写。
|
||||
# 若要手写,参考下面的骨架(把上面 /api/、/uploads/、/ 三段复制进来即可):
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# listen [::]:443 ssl;
|
||||
# server_name your-domain.com;
|
||||
# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# # ... 同上三个 location 块 ...
|
||||
# }
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 手动触发项目 JS→WASM 预编译(在服务器上执行)
|
||||
# ------------------------------------------------------------
|
||||
# 用法:bash deploy/precompile_wasm.sh <项目ID>
|
||||
# 示例:bash deploy/precompile_wasm.sh 1 # 预编译项目一
|
||||
# 效果:uploads/demos/{id}/wasm/ 下生成 .wasm 与 report.json
|
||||
# 等价接口:POST /api/project/{id}/precompile(仅博主)
|
||||
# ============================================================
|
||||
set -eu
|
||||
PROJECT_ID="${1:?用法:bash deploy/precompile_wasm.sh <项目ID>}"
|
||||
cd /var/www/blog
|
||||
/var/www/blog/venv/bin/python -m backend.services.wasm_builder "$PROJECT_ID"
|
||||
# 以 root 执行时,把产物属主修正为 blog,保证 Nginx 可读
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
chown -R blog:blog "/var/www/blog/uploads/demos/${PROJECT_ID}/wasm"
|
||||
fi
|
||||
@@ -0,0 +1,94 @@
|
||||
# ============================================================
|
||||
# MyBlog 一键更新脚本(在 Windows 上运行)
|
||||
# 作用:打包本地代码 -> 上传到服务器 -> 服务器自动 备份/迁移/重启/验证
|
||||
# 前置条件:
|
||||
# 1. 已配置 SSH 免密登录(首次需执行:ssh-keygen 后 ssh-copy-id 服务器)
|
||||
# 2. 服务器上已按 deploy/DEPLOY.md 完成首次部署(myblog 服务存在)
|
||||
# 用法(在项目根目录 D:\MyBlog 打开 PowerShell):
|
||||
# powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1
|
||||
# 或直接指定服务器:.\deploy\update.ps1 -Server root@8.145.36.108
|
||||
# 常用参数:
|
||||
# -Server 服务器地址,默认读取 deploy/deploy.conf(缺省用 root@8.145.36.108)
|
||||
# -IncludeUploads 同时同步本地 uploads/(首次部署或新增静态资源时用)
|
||||
# 安全说明:
|
||||
# - 永远不打包 .env 与 backend/blog.db(密钥与数据库只在服务器上)
|
||||
# - 默认排除 uploads/:网页上传的文件只在服务器,本地打包不覆盖
|
||||
# ============================================================
|
||||
|
||||
param(
|
||||
[string]$Server = "",
|
||||
[switch]$IncludeUploads
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = $PSScriptRoot # deploy 目录
|
||||
$ProjectRoot = Split-Path -Parent $ScriptDir # 项目根目录
|
||||
$PackageName = "myblog.tar.gz"
|
||||
$RemoteScript = "update_server.sh"
|
||||
|
||||
function Write-Step([string]$msg) { Write-Host ""; Write-Host "== $msg ==" -ForegroundColor Cyan }
|
||||
|
||||
# ---------- 1. 确定服务器地址(命令行参数 > deploy.conf > 默认值) ----------
|
||||
if (-not $Server -and (Test-Path (Join-Path $ScriptDir "deploy.conf"))) {
|
||||
$confLine = Get-Content (Join-Path $ScriptDir "deploy.conf") | Where-Object { $_ -match '^\s*SERVER\s*=' } | Select-Object -First 1
|
||||
if ($confLine) { $Server = ($confLine -split '=', 2)[1].Trim() }
|
||||
$domainLine = Get-Content (Join-Path $ScriptDir "deploy.conf") | Where-Object { $_ -match '^\s*DOMAIN\s*=' } | Select-Object -First 1
|
||||
if ($domainLine) { $Domain = ($domainLine -split '=', 2)[1].Trim() }
|
||||
}
|
||||
if (-not $Server) { $Server = "root@8.145.36.108" }
|
||||
Write-Host "目标服务器:$Server"
|
||||
if ($Domain) { Write-Host "验证域名:$Domain" }
|
||||
|
||||
# ---------- 2. 检查依赖工具 ----------
|
||||
foreach ($tool in @("tar", "scp", "ssh")) {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
|
||||
throw "缺少工具 $tool,请先安装 OpenSSH 客户端(Windows 设置 -> 可选功能)。"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------- 3. 打包本地代码(排除密钥/数据库/缓存/上传目录) ----------
|
||||
Write-Step "打包本地代码"
|
||||
$tmpPkg = Join-Path $env:TEMP $PackageName
|
||||
if (Test-Path $tmpPkg) { Remove-Item -LiteralPath $tmpPkg -Force }
|
||||
Push-Location $ProjectRoot
|
||||
try {
|
||||
# 排除本地敏感运行数据:数据库、密钥配置、IPWatch 家庭端密钥与日志
|
||||
$excludes = @("--exclude=backend/blog.db", "--exclude=backend/__pycache__",
|
||||
"--exclude=backend/routers/__pycache__",␍
|
||||
"--exclude=backend/services/__pycache__", "--exclude=venv",
|
||||
"--exclude=deploy/ipwatch/ipwatch.conf",
|
||||
"--exclude=deploy/ipwatch/lastip.txt",
|
||||
"--exclude=deploy/ipwatch/ipwatch.log")
|
||||
if (-not $IncludeUploads) { $excludes += "--exclude=uploads" }
|
||||
$targets = @("frontend", "backend", "deploy", "requirements.txt", ".env.example", "README.md")
|
||||
& tar -czf $tmpPkg @excludes @targets
|
||||
if ($LASTEXITCODE -ne 0) { throw "tar 打包失败" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
# 校验包内不含敏感文件(防止配置写错把密钥带上服务器)
|
||||
$bad = tar -tzf $tmpPkg | Select-String "(^|/)\.env$|blog\.db|__pycache__|^uploads/|ipwatch\.(conf|log)$|lastip\.txt" | Select-Object -First 5
|
||||
if ($bad) { throw "更新包内含不应上传的文件:$($bad -join ', ')" }
|
||||
Write-Host ("更新包已生成:{0}({1:N0} KB)" -f $tmpPkg, ((Get-Item $tmpPkg).Length / 1KB))
|
||||
|
||||
# ---------- 4. 上传到服务器 ----------
|
||||
Write-Step "上传更新包与服务器脚本"
|
||||
& scp -o BatchMode=yes -o StrictHostKeyChecking=accept-new $tmpPkg "$($Server):/tmp/$PackageName"
|
||||
if ($LASTEXITCODE -ne 0) { throw "上传失败:请确认已配置 SSH 免密登录" }
|
||||
& scp -o BatchMode=yes (Join-Path $ScriptDir $RemoteScript) "$($Server):/tmp/$RemoteScript"
|
||||
if ($LASTEXITCODE -ne 0) { throw "上传服务器脚本失败" }
|
||||
Write-Host "上传完成"
|
||||
|
||||
# ---------- 5. 执行服务器端更新(备份/解压/迁移/重启/验证) ----------
|
||||
Write-Step "执行服务器端更新(备份、迁移、重启、验证)"
|
||||
$domainArg = if ($Domain) { " '$Domain'" } else { "" }
|
||||
# 服务器端脚本若带 Windows 换行(CRLF)会导致 bash 解析失败,先统一转成 LF 再执行
|
||||
& ssh -o BatchMode=yes $Server "sed -i 's/\r`$//' /tmp/$RemoteScript && bash /tmp/$RemoteScript$domainArg"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "服务器端更新失败,请检查上方输出。可用备份回滚:/root/blog-backups/" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✅ 更新完成!请打开 https://$Domain 验证。" -ForegroundColor Green
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/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/"
|
||||
Reference in New Issue
Block a user