feat(migrate): Halo 全量内容迁移(压缩版)——6文/21图/1瞬间 URL 1:1
- 正文源=线上渲染页(Halo 快照层 patch 残迹:泥潭/赎罪仅 2 字符),
pandoc html→gfm + tags API 显示名换算后分类:5 书评(五档情绪色)+1 技术
- 图库 21 张:长边 2400/q86 压缩入仓(原图 196M→9.5M——公开仓历史膨胀
与 nginx 100M 推送上限双杀;原图在 Phase 0 备份 tar 中永久留档)
- 瞬间 1 条;URL 1:1(6 slug 全等,零 301)
- 本提交由 7 个本地提交重写而成(清除已进历史的 196M blob,未推送过);
因 reset --soft 暂存竞态,一并包含 spec 归档文件
(.spec/specs/archive/2026-10-24_migrate-blog-content/)
验证: check_urls.py 6==6;astro build 11 页 exit 0(8.67s);
EXIF 管线跑通;staging 更新;halo healthy+200 全程未动
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
7b71acf223
commit
aea21c8694
74 files changed
+1381
-97
No files matched your search
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Halo DB 只读导出(Phase 2 T1 补充源):posts + snapshots → .migration/。
|
||||
|
||||
公共 API 只有摘要、head-content 404;正文真源在 postgres extensions 表:
|
||||
/registry/blog.halo.run/posts/{name} → spec.headSnapshot 指针
|
||||
/registry/content.halo.run/snapshots/{uuid} → raw/content 全量
|
||||
data 列是 bytea,经 base64 通道取出后 json 解析(psql 文本输出会搅转义)。
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(".migration")
|
||||
|
||||
|
||||
def psql_b64(name: str) -> dict:
|
||||
sql = "SELECT encode(data,'base64') FROM extensions WHERE name = '%s';" % name
|
||||
raw = subprocess.run(
|
||||
["docker", "exec", "postgres-db", "psql", "-U", "wait4x", "-d", "halo_db", "-t", "-A", "-c", sql],
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout.strip()
|
||||
return json.loads(base64.b64decode(raw))
|
||||
|
||||
|
||||
def names_like(pattern: str) -> list[str]:
|
||||
sql = "SELECT name FROM extensions WHERE name LIKE '%s';" % pattern
|
||||
raw = subprocess.run(
|
||||
["docker", "exec", "postgres-db", "psql", "-U", "wait4x", "-d", "halo_db", "-t", "-A", "-c", sql],
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout.split()
|
||||
return raw
|
||||
|
||||
|
||||
def main() -> int:
|
||||
posts = [psql_b64(n) for n in names_like("%/posts/%")]
|
||||
snaps = {n.rsplit("/", 1)[1]: psql_b64(n) for n in names_like("%/snapshots/%")}
|
||||
|
||||
# 每篇 post 挂上 head snapshot 的 raw/content 全文
|
||||
# 结构:snapshot.spec.{rawPatch,contentPatch} 直接是字符串(实测解剖)
|
||||
for p in posts:
|
||||
spec = p.get("spec", {})
|
||||
# head 与 release 各挂一个 snapshot;实测存在"head 空、release 满"与
|
||||
# 反例(mian-sha),取 content 更长者作为迁移源
|
||||
cands = [snaps.get(k, {}).get("spec", {}) for k in (spec.get("releaseSnapshot"), spec.get("headSnapshot")) if k]
|
||||
snap = max(cands, key=lambda sp: len(sp.get("contentPatch", "") or ""), default={})
|
||||
p["_snapshot"] = {
|
||||
"rawType": snap.get("rawType"),
|
||||
"raw": snap.get("rawPatch", ""),
|
||||
"content": snap.get("contentPatch", ""),
|
||||
"snapshotCount": len(cands),
|
||||
}
|
||||
|
||||
(OUT / "posts_db.json").write_text(json.dumps(posts, ensure_ascii=False, indent=1))
|
||||
ok = sum(1 for p in posts if p["_snapshot"]["content"])
|
||||
print(f"db export: posts={len(posts)}(含正文 {ok});snapshots={len(snaps)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user