用 marknb 替换 Python markdown,并添加一键安装与升级脚本。

部署时通过 install.sh 自动编译 marknb;日常更新用 upgrade.sh 拉代码并重编译。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cnb
2026-07-05 11:48:18 +08:00
co-authored by Cursor
parent 0333ab1fc1
commit cf8eba082f
8 changed files with 227 additions and 25 deletions
+2 -8
View File
@@ -3,11 +3,5 @@ $:
- runner:
cpus: 2
stages:
- name: ls
script: ls -al
- name: install dependents
script:
- apt install python3 python3-pip python3-venv -y
- python3 -m venv ./venv
- ./venv/bin/pip install -r requirements.txt
- echo "source $(pwd)/venv/bin/activate" >> ~/.zshrc
- name: install
script: bash scripts/install.sh
+3
View File
@@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so
# marknb (built by scripts/install.sh)
vendor/
# Distribution / packaging
.Python
build/
+57 -4
View File
@@ -43,16 +43,40 @@
- 单管理员后台:密码登录、CSRF、Session,够用且不繁琐。
- 备案号挂载:`BLOG_ICP_TEXT` / `BLOG_ICP_URL` 环境变量直接生效,国内合规。
- 反代友好:默认监听 `127.0.0.1:8765`,套 Nginx 即可上 HTTPS。
- Markdown 引擎:[marknb](https://cnb.cool/whcself/marknb) — 零堆分配 C 解析器,比 Python `markdown` 更快,支持 GFM 表格与 LaTeX 透传。
### 30 秒上手
```bash
git clone https://github.com/WHC2006/py-blog.git
git clone https://cnb.cool/whcself/py-blog.git
cd py-blog
pip install -r requirements.txt
bash scripts/install.sh
source venv/bin/activate
python blog.py
```
`install.sh` 会自动从 [marknb](https://cnb.cool/whcself/marknb) 拉取源码、编译为 `vendor/marknb/libmarknb.so`,并创建 `venv` 安装 Python 依赖。
### 升级
已部署的站点拉取新版本后,一条命令完成代码、marknb 与 pip 依赖更新:
```bash
cd py-blog
bash scripts/upgrade.sh
source venv/bin/activate
python blog.py # 重启服务
```
`upgrade.sh` 会依次:`git pull` py-blog → 重编译 marknb → `pip install -U` 依赖。若尚未安装过,会自动回退执行 `install.sh`
### 脚本一览
| 脚本 | 用途 |
| --- | --- |
| `scripts/install.sh` | 首次部署:系统依赖 + marknb 编译 + venv |
| `scripts/upgrade.sh` | 日常升级:拉代码 + 重编译 marknb + 更新 pip |
打开 <http://127.0.0.1:8765>,登录入口在 `/admin/login`
若没设置 `BLOG_ADMIN_PASSWORD`,**首次启动会在终端打印一次随机密码**,请立刻收好。
@@ -68,6 +92,7 @@ python blog.py
| `BLOG_SECRET_KEY` | — | Session 签名密钥;未设置则自动生成并持久化 |
| `BLOG_ICP_TEXT` | — | 页脚备案文案,例如 `京ICP备xxxxxx号-1` |
| `BLOG_ICP_URL` | — | 备案查询链接,例如 `https://beian.miit.gov.cn/` |
| `MARKNB_LIB` | `vendor/marknb/libmarknb.so` | marknb 动态库路径(可选) |
### Nginx 反代示例
@@ -143,16 +168,43 @@ But beginners also deserve a blog:
- Built-in ICP footer (`BLOG_ICP_TEXT` / `BLOG_ICP_URL`) for hosting in China.
- Reverse-proxy friendly: binds to `127.0.0.1:8765` by default; put Nginx
in front for HTTPS.
- Markdown engine: [marknb](https://cnb.cool/whcself/marknb) — zero-malloc C
parser, faster than Python `markdown`, with GFM tables and LaTeX passthrough.
### Quick start
```bash
git clone https://github.com/WHC2006/py-blog.git
git clone https://cnb.cool/whcself/py-blog.git
cd py-blog
pip install -r requirements.txt
bash scripts/install.sh
source venv/bin/activate
python blog.py
```
`install.sh` clones [marknb](https://cnb.cool/whcself/marknb), builds
`vendor/marknb/libmarknb.so`, and creates a `venv` with Python deps.
### Upgrade
On an existing deployment, one command updates code, marknb, and pip packages:
```bash
cd py-blog
bash scripts/upgrade.sh
source venv/bin/activate
python blog.py # restart the service
```
`upgrade.sh` runs `git pull` → rebuild marknb → `pip install -U`. Falls back
to `install.sh` if no `venv` exists yet.
### Scripts
| Script | Purpose |
| --- | --- |
| `scripts/install.sh` | First deploy: system deps + marknb build + venv |
| `scripts/upgrade.sh` | Routine upgrade: pull + rebuild marknb + pip update |
Open <http://127.0.0.1:8765>. Admin login lives at `/admin/login`.
If `BLOG_ADMIN_PASSWORD` is unset, **a random password is printed once** to
the terminal on first boot — copy it immediately.
@@ -169,6 +221,7 @@ the terminal on first boot — copy it immediately.
| `BLOG_SECRET_KEY` | — | Session signing key; auto-generated and persisted if unset |
| `BLOG_ICP_TEXT` | — | Footer ICP text (China hosting) |
| `BLOG_ICP_URL` | — | Footer ICP link |
| `MARKNB_LIB` | `vendor/marknb/libmarknb.so` | Path to marknb shared library (optional) |
### Nginx reverse proxy
+49 -12
View File
@@ -2,8 +2,8 @@
单文件博客:FastAPI + SQLite + Jinja2Markdown 预渲染为 HTML。
安装依赖:
pip install -r requirements.txt
# 或: pip install fastapi "uvicorn[standard]" jinja2 python-multipart markdown
bash scripts/install.sh
# 或手动: 编译 vendor/marknb/libmarknb.so 后 pip install -r requirements.txt
运行:
python blog.py
@@ -26,18 +26,18 @@
from __future__ import annotations
import asyncio
import ctypes
import hashlib
import os
import re
import secrets
import sqlite3
import sys
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
from urllib.parse import quote
import markdown
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import HTMLResponse, RedirectResponse
from jinja2 import BaseLoader, Environment, TemplateNotFound
@@ -193,19 +193,56 @@ def bootstrap_secrets() -> str:
# -----------------------------------------------------------------------------
# Markdown → HTML
# Markdown → HTML (marknb: https://cnb.cool/whcself/marknb)
# -----------------------------------------------------------------------------
_MD_EXTENSIONS = [
"markdown.extensions.fenced_code",
"markdown.extensions.tables",
"markdown.extensions.nl2br",
"markdown.extensions.sane_lists",
]
_MARKNB_DEFAULT = 65536
def _load_marknb() -> ctypes.CDLL:
lib_path = os.environ.get("MARKNB_LIB")
if not lib_path:
here = Path(__file__).resolve().parent
for candidate in (
here / "vendor" / "marknb" / "libmarknb.so",
Path("/usr/local/lib/libmarknb.so"),
):
if candidate.is_file():
lib_path = str(candidate)
break
if not lib_path:
print(
"找不到 marknb 动态库。请先运行: bash scripts/install.sh(升级: bash scripts/upgrade.sh",
file=sys.stderr,
)
sys.exit(1)
lib = ctypes.CDLL(lib_path)
lib.marknb_parse.argtypes = [
ctypes.c_char_p,
ctypes.c_size_t,
ctypes.c_char_p,
ctypes.c_size_t,
ctypes.POINTER(ctypes.c_size_t),
]
lib.marknb_parse.restype = ctypes.c_int
return lib
_MARKNB = _load_marknb()
def render_markdown(md_text: str) -> str:
return markdown.markdown(md_text, extensions=_MD_EXTENSIONS)
md = md_text.encode("utf-8")
cap = max(len(md) * 4 + 256, _MARKNB_DEFAULT)
limit = len(md) * 64 + 4096
while cap <= limit:
buf = ctypes.create_string_buffer(cap)
out_len = ctypes.c_size_t(0)
if _MARKNB.marknb_parse(md, len(md), buf, cap, ctypes.byref(out_len)) == 0:
return buf.raw[: out_len.value].decode("utf-8")
cap *= 2
raise RuntimeError("marknb: output buffer exhausted")
# -----------------------------------------------------------------------------
-1
View File
@@ -2,5 +2,4 @@ fastapi>=0.110,<1
uvicorn[standard]>=0.27,<1
jinja2>=3.1,<4
python-multipart>=0.0.9,<1
markdown>=3.5,<4
itsdangerous
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# marknb / py-blog 公共构建逻辑(供 install.sh、upgrade.sh 引用)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENDOR="$ROOT/vendor/marknb"
MARKNB_REPO="${MARKNB_REPO:-https://cnb.cool/whcself/marknb.git}"
MARKNB_SRC="$VENDOR/src"
LIB_PATH="$VENDOR/libmarknb.so"
apt_install() {
if ! command -v apt-get >/dev/null 2>&1; then
return 0
fi
local pkgs=(gcc make git python3 python3-pip python3-venv)
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends "${pkgs[@]}"
else
apt-get update -qq
apt-get install -y --no-install-recommends "${pkgs[@]}"
fi
}
build_marknb() {
mkdir -p "$VENDOR"
if [ -d "$MARKNB_SRC/.git" ]; then
git -C "$MARKNB_SRC" fetch --depth 1 origin main
git -C "$MARKNB_SRC" checkout -f main
git -C "$MARKNB_SRC" reset --hard origin/main
else
git clone --depth 1 --branch main "$MARKNB_REPO" "$MARKNB_SRC"
fi
local srcs=(
"$MARKNB_SRC/src/fsm.c"
"$MARKNB_SRC/src/table_stats.c"
"$MARKNB_SRC/src/line_index.c"
"$MARKNB_SRC/src/render.c"
"$MARKNB_SRC/src/marknb.c"
"$MARKNB_SRC/src/compat_cmark.c"
)
gcc -std=c11 -Wall -Wextra -O3 -fPIC -shared \
-I"$MARKNB_SRC/include" -I"$MARKNB_SRC/src" \
"${srcs[@]}" -lm -o "$LIB_PATH"
}
install_python() {
if [ ! -d "$ROOT/venv" ]; then
python3 -m venv "$ROOT/venv"
fi
"$ROOT/venv/bin/pip" install -U pip
"$ROOT/venv/bin/pip" install -r "$ROOT/requirements.txt"
}
upgrade_python() {
if [ ! -d "$ROOT/venv" ]; then
echo "未找到 venv,改为执行 install.sh" >&2
exec bash "$ROOT/scripts/install.sh"
fi
"$ROOT/venv/bin/pip" install -U pip
"$ROOT/venv/bin/pip" install -U -r "$ROOT/requirements.txt"
}
pull_py_blog() {
if [ ! -d "$ROOT/.git" ]; then
echo "当前目录不是 git 仓库,跳过 py-blog 代码拉取"
return 0
fi
local branch
branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
git -C "$ROOT" fetch origin "$branch"
git -C "$ROOT" merge --ff-only "origin/$branch"
}
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# 一键安装 py-blog 依赖:编译 marknb 动态库 + Python venv
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=common.sh
source "$ROOT/scripts/common.sh"
echo "==> 安装系统构建依赖(如有 apt)"
apt_install
echo "==> 拉取并编译 marknb → $LIB_PATH"
build_marknb
echo "==> 安装 Python 依赖到 venv"
install_python
echo ""
echo "安装完成。"
echo " marknb: $LIB_PATH"
echo " 启动: source venv/bin/activate && python blog.py"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# 升级已部署的 py-blog:拉取代码、重编译 marknb、更新 Python 依赖
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=common.sh
source "$ROOT/scripts/common.sh"
echo "==> 拉取 py-blog 最新代码"
pull_py_blog
echo "==> 重新编译 marknb → $LIB_PATH"
build_marknb
echo "==> 升级 Python 依赖"
upgrade_python
echo ""
echo "升级完成。"
echo " marknb: $LIB_PATH"
echo " 若 blog.py 正在运行,请重启服务使变更生效。"