app.py 6.2 KB

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