用 marknb 替换 Python markdown,并添加一键安装与升级脚本。
部署时通过 install.sh 自动编译 marknb;日常更新用 upgrade.sh 拉代码并重编译。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -6,6 +6,9 @@ __pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# marknb (built by scripts/install.sh)
|
||||
vendor/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
单文件博客:FastAPI + SQLite + Jinja2,Markdown 预渲染为 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")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+74
@@ -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"
|
||||
}
|
||||
Executable
+21
@@ -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"
|
||||
Executable
+21
@@ -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 正在运行,请重启服务使变更生效。"
|
||||
Reference in New Issue
Block a user