app.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import re
  2. import json
  3. import time
  4. import shutil
  5. from datetime import datetime, timezone
  6. from pathlib import Path
  7. from flask import (
  8. Flask,
  9. render_template,
  10. request,
  11. redirect,
  12. url_for,
  13. send_from_directory,
  14. abort,
  15. jsonify,
  16. )
  17. from werkzeug.utils import secure_filename
  18. import config
  19. from helpers import (
  20. allowed_file,
  21. load_index,
  22. save_index,
  23. fix_image_paths,
  24. extract_summary,
  25. extract_thumbnail,
  26. render_markdown,
  27. generate_static_page,
  28. )
  29. app = Flask(__name__)
  30. app.config.from_object(config)
  31. # 确保必要的目录存在
  32. for dir_path in [
  33. config.UPLOAD_FOLDER,
  34. config.GENERATED_FOLDER,
  35. config.POSTS_DATA_FOLDER,
  36. config.INDEX_FILE.parent,
  37. ]:
  38. dir_path.mkdir(parents=True, exist_ok=True)
  39. # 如果索引文件不存在,创建空列表
  40. if not config.INDEX_FILE.exists():
  41. with open(config.INDEX_FILE, "w", encoding="utf-8") as f:
  42. json.dump([], f, ensure_ascii=False, indent=2)
  43. # ==================== 路由 ====================
  44. @app.route("/")
  45. def index():
  46. """首页:瀑布流文章列表(展示完整博文)"""
  47. posts = load_index()
  48. # 按日期倒序排列
  49. posts.sort(key=lambda p: p.get("date", ""), reverse=True)
  50. # 为每篇文章获取内容
  51. for post in posts:
  52. post_id = post.get("id", "")
  53. content = ""
  54. # 优先从原始 Markdown 文件渲染内容(即使 generated 文件不存在也能显示)
  55. md_path = config.POSTS_DATA_FOLDER / post_id / "content.md"
  56. if md_path.exists():
  57. try:
  58. with open(md_path, "r", encoding="utf-8") as f:
  59. md_content = f.read()
  60. fixed_md = fix_image_paths(md_content, post_id)
  61. html_body = render_markdown(fixed_md)
  62. content = html_body
  63. except Exception:
  64. content = ""
  65. else:
  66. # 回退到生成的静态 HTML 文件
  67. generated_path = config.GENERATED_FOLDER / f"{post_id}.html"
  68. if generated_path.exists():
  69. try:
  70. with open(generated_path, "r", encoding="utf-8") as f:
  71. html_content = f.read()
  72. soup = BeautifulSoup(html_content, "html.parser")
  73. card_summary = soup.find("div", class_="card-summary")
  74. if card_summary:
  75. content = card_summary.decode_contents()
  76. except Exception:
  77. content = ""
  78. post["content"] = content
  79. return render_template("index.html", posts=posts)
  80. @app.route("/upload", methods=["GET", "POST"])
  81. def upload():
  82. """上传文章"""
  83. if request.method == "GET":
  84. return render_template("upload.html")
  85. # POST 处理
  86. title = request.form.get("title", "").strip()
  87. if not title:
  88. return "标题不能为空", 400
  89. markdown_file = request.files.get("markdown_file")
  90. if not markdown_file or markdown_file.filename == "":
  91. return "请选择 Markdown 文件", 400
  92. if not allowed_file(markdown_file.filename):
  93. return "不支持的文件类型,请上传 .md 文件", 400
  94. # 生成唯一 ID
  95. post_id = str(int(time.time() * 1000))
  96. # 创建目录
  97. upload_dir = config.UPLOAD_FOLDER / post_id
  98. upload_dir.mkdir(parents=True, exist_ok=True)
  99. posts_data_dir = config.POSTS_DATA_FOLDER / post_id
  100. posts_data_dir.mkdir(parents=True, exist_ok=True)
  101. # 保存 Markdown 文件
  102. md_path = posts_data_dir / "content.md"
  103. markdown_file.save(str(md_path))
  104. # 处理图片上传
  105. images = request.files.getlist("images")
  106. for img in images:
  107. if img and img.filename:
  108. filename = secure_filename(img.filename)
  109. if filename:
  110. img.save(str(upload_dir / filename))
  111. # 读取 Markdown 内容
  112. with open(md_path, "r", encoding="utf-8") as f:
  113. md_content = f.read()
  114. # 修正图片路径
  115. fixed_md = fix_image_paths(md_content, post_id)
  116. # 渲染 HTML
  117. html_body = render_markdown(fixed_md)
  118. # 提取摘要
  119. summary = extract_summary(html_body)
  120. # 提取缩略图
  121. thumbnail = extract_thumbnail(html_body)
  122. # 生成日期
  123. date_iso = datetime.now(timezone.utc).isoformat()
  124. # 生成静态页面
  125. generate_static_page(post_id, title, html_body, date_iso, thumbnail)
  126. # 更新索引
  127. index = load_index()
  128. index.append(
  129. {
  130. "id": post_id,
  131. "title": title,
  132. "date": date_iso,
  133. "summary": summary,
  134. "thumbnail": thumbnail,
  135. }
  136. )
  137. # 按日期倒序排序
  138. index.sort(key=lambda p: p.get("date", ""), reverse=True)
  139. save_index(index)
  140. return redirect(url_for("index"))
  141. @app.route("/post/<post_id>")
  142. def view_post(post_id: str):
  143. """查看文章详情"""
  144. # 安全验证:只允许字母数字和下划线
  145. if not re.match(r"^[a-zA-Z0-9_]+$", post_id):
  146. abort(404)
  147. try:
  148. return send_from_directory(
  149. config.GENERATED_FOLDER,
  150. f"{post_id}.html",
  151. )
  152. except FileNotFoundError:
  153. abort(404)
  154. @app.route("/admin")
  155. def admin():
  156. """管理页面"""
  157. posts = load_index()
  158. return render_template("admin.html", posts=posts)
  159. @app.route("/admin/delete/<post_id>", methods=["POST"])
  160. def delete_post(post_id: str):
  161. """删除文章"""
  162. # 安全验证
  163. if not re.match(r"^[a-zA-Z0-9_]+$", post_id):
  164. abort(404)
  165. # 删除生成的静态文件
  166. generated_file = config.GENERATED_FOLDER / f"{post_id}.html"
  167. if generated_file.exists():
  168. generated_file.unlink()
  169. # 删除 posts_data 目录
  170. posts_data_dir = config.POSTS_DATA_FOLDER / post_id
  171. if posts_data_dir.exists():
  172. shutil.rmtree(posts_data_dir)
  173. # 删除上传的图片目录
  174. upload_dir = config.UPLOAD_FOLDER / post_id
  175. if upload_dir.exists():
  176. shutil.rmtree(upload_dir)
  177. # 从索引中移除
  178. index = load_index()
  179. index = [p for p in index if p["id"] != post_id]
  180. save_index(index)
  181. return redirect(url_for("admin"))
  182. @app.errorhandler(404)
  183. def not_found(e):
  184. return render_template("404.html"), 404
  185. if __name__ == "__main__":
  186. app.run(host="0.0.0.0", port=5000, debug=True)