#!/usr/bin/env python3
"""极简博客 —— 单文件 Flask 应用,文章以 JSON 存在 PVC 上。"""
import os
import json
import re
import html
import datetime
import markdown
from flask import Flask, request, redirect, abort, render_template_string
DATA_DIR = os.environ.get("DATA_DIR", "/data")
DB_FILE = os.path.join(DATA_DIR, "posts.json")
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "change-me")
SITE_TITLE = os.environ.get("SITE_TITLE", "我的博客")
app = Flask(__name__)
def load_posts():
if not os.path.exists(DB_FILE):
return []
try:
with open(DB_FILE, encoding="utf-8") as f:
posts = json.load(f)
return sorted(posts, key=lambda p: p.get("created", ""), reverse=True)
except (json.JSONDecodeError, OSError):
return []
def save_posts(posts):
os.makedirs(DATA_DIR, exist_ok=True)
tmp = DB_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(posts, f, ensure_ascii=False, indent=2)
os.replace(tmp, DB_FILE)
def slugify(text):
slug = re.sub(r"[^\w\u4e00-\u9fff-]+", "-", text.strip().lower()).strip("-")
return slug[:60] or "post"
BASE_CSS = """
"""
LIST_TPL = """
{{ site }}""" + BASE_CSS + """
{% for p in posts %}
{{ p.created }} · {{ p.views }} 次阅读
{{ p.excerpt }}
{% else %}
还没有文章。点右上角"写文章"开始。
{% endfor %}
"""
POST_TPL = """
{{ p.title }} · {{ site }}""" + BASE_CSS + """
{{ p.title }}
{{ p.created }}
{{ body|safe }}
"""
NEW_TPL = """
写文章 · {{ site }}""" + BASE_CSS + """
{% if error %}
{% endif %}
"""
@app.get("/healthz")
def healthz():
return "ok", 200
@app.get("/")
def index():
posts = []
for p in load_posts():
posts.append({
"slug": p["slug"],
"title": p["title"],
"created": p["created"],
"views": p.get("views", 0),
"excerpt": p.get("excerpt", ""),
})
return render_template_string(LIST_TPL, site=SITE_TITLE, posts=posts)
@app.get("/post/")
def post_detail(slug):
posts = load_posts()
for p in posts:
if p["slug"] == slug:
p["views"] = p.get("views", 0) + 1
save_posts(posts)
body = markdown.markdown(p["body"], extensions=["fenced_code", "tables"])
return render_template_string(POST_TPL, site=SITE_TITLE, p=p, body=body)
abort(404)
@app.route("/new", methods=["GET", "POST"])
def new_post():
if request.method == "GET":
return render_template_string(NEW_TPL, site=SITE_TITLE, error=None, title="", body="")
title = (request.form.get("title") or "").strip()
body = (request.form.get("body") or "").strip()
token = request.form.get("token") or ""
if token != ADMIN_TOKEN:
return render_template_string(NEW_TPL, site=SITE_TITLE,
error="管理口令不正确", title=title, body=body), 403
if not title or not body:
return render_template_string(NEW_TPL, site=SITE_TITLE,
error="标题和正文都不能为空", title=title, body=body), 400
posts = load_posts()
base = slugify(title)
slug = base
n = 1
existing = {p["slug"] for p in posts}
while slug in existing:
n += 1
slug = f"{base}-{n}"
plain = re.sub(r"[#>*_`\[\]()!-]", "", body)
posts.append({
"slug": slug,
"title": title,
"body": body,
"excerpt": html.escape(plain[:120].replace("\n", " ")),
"created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"views": 0,
})
save_posts(posts)
return redirect(f"/post/{slug}")
@app.post("/post//delete")
def delete_post(slug):
token = request.form.get("token") or ""
if token != ADMIN_TOKEN:
abort(403)
posts = [p for p in load_posts() if p["slug"] != slug]
save_posts(posts)
return redirect("/")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5000")))