新增管理器
This commit is contained in:
129
main.py
129
main.py
@@ -8,8 +8,13 @@ import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from flask import Flask, Response, abort, jsonify, render_template, request, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from utils.media_manager import save_upload, list_media, delete_media
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -132,6 +137,43 @@ def _copy_blank_template(dest: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def build_page_tree(slug: str, max_depth: int = 3) -> dict[str, Any]:
|
||||
"""遞歸建構專案的頁面樹狀結構(根據資料夾結構)。"""
|
||||
proj_dir = _project_dir(slug)
|
||||
|
||||
def scan_folder(folder: Path, current_depth: int = 0):
|
||||
items: list[dict[str, Any]] = []
|
||||
if current_depth > max_depth:
|
||||
return items
|
||||
try:
|
||||
names = sorted([p.name for p in folder.iterdir()])
|
||||
except Exception:
|
||||
return items
|
||||
|
||||
for name in names:
|
||||
if name.startswith("."):
|
||||
continue
|
||||
full = folder / name
|
||||
if full.is_dir():
|
||||
children = scan_folder(full, current_depth + 1)
|
||||
items.append({
|
||||
"type": "folder",
|
||||
"name": name,
|
||||
"title": name.replace("-", " ").title(),
|
||||
"children": children,
|
||||
})
|
||||
elif full.is_file() and full.suffix.lower() == ".html":
|
||||
rel = str(full.relative_to(proj_dir)).replace("\\", "/")
|
||||
items.append({
|
||||
"type": "file",
|
||||
"name": rel,
|
||||
"title": Path(name).stem.replace("-", " ").title(),
|
||||
})
|
||||
return items
|
||||
|
||||
return {"root": scan_folder(proj_dir, 0)}
|
||||
|
||||
|
||||
# ── 路由:一般頁面 ──────────────────────────────────────────────────────────
|
||||
|
||||
@app.route("/") # type: ignore[untyped-decorator]
|
||||
@@ -195,6 +237,40 @@ def api_list_projects() -> Response:
|
||||
return jsonify(projects)
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/settings", methods=["GET"]) # type: ignore[untyped-decorator]
|
||||
def api_get_settings(slug: str) -> tuple[Response, int] | Response:
|
||||
if not _project_dir(slug).exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
data = _load_project(slug)
|
||||
settings = data.get("settings", {})
|
||||
# provide sensible defaults
|
||||
defaults = {
|
||||
"title": data.get("name", slug),
|
||||
"description": data.get("description", ""),
|
||||
"logo_url": None,
|
||||
"favicon_url": None,
|
||||
"primary_color": "#007bff",
|
||||
"secondary_color": "#6c757d",
|
||||
}
|
||||
merged = {**defaults, **settings}
|
||||
return jsonify(merged)
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/settings", methods=["PUT"]) # type: ignore[untyped-decorator]
|
||||
def api_put_settings(slug: str) -> tuple[Response, int] | Response:
|
||||
if not _project_dir(slug).exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
body: dict[str, Any] = request.get_json(force=True) or {}
|
||||
data = _load_project(slug)
|
||||
settings = data.get("settings", {})
|
||||
settings.update(body)
|
||||
data["settings"] = settings
|
||||
data["updated_at"] = _now_iso()
|
||||
_save_project(slug, data)
|
||||
return jsonify({"ok": True, "settings": settings})
|
||||
|
||||
|
||||
|
||||
@app.route("/api/projects", methods=["POST"]) # type: ignore[untyped-decorator]
|
||||
def api_create_project() -> tuple[Response, int]:
|
||||
body: dict[str, Any] = request.get_json(force=True) or {}
|
||||
@@ -261,6 +337,59 @@ def api_list_pages(slug: str) -> tuple[Response, int] | Response:
|
||||
return jsonify(_list_pages(slug))
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/pages-tree", methods=["GET"]) # type: ignore[untyped-decorator]
|
||||
def api_pages_tree(slug: str) -> tuple[Response, int] | Response:
|
||||
if not _project_dir(slug).exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
tree = build_page_tree(slug, max_depth=5)
|
||||
return jsonify(tree)
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/media/upload", methods=["POST"]) # type: ignore[untyped-decorator]
|
||||
def api_media_upload(slug: str) -> tuple[Response, int] | Response:
|
||||
proj_dir = _project_dir(slug)
|
||||
if not proj_dir.exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "缺少檔案 (file)"}), 400
|
||||
f = request.files["file"]
|
||||
if f.filename == "":
|
||||
return jsonify({"error": "檔名為空"}), 400
|
||||
# basic validation
|
||||
filename = secure_filename(f.filename)
|
||||
meta = save_upload(proj_dir, f)
|
||||
# substitute slug in returned urls
|
||||
if isinstance(meta.get("url"), str):
|
||||
meta["url"] = meta["url"].replace("{slug}", slug)
|
||||
if isinstance(meta.get("thumb"), str):
|
||||
meta["thumb"] = meta["thumb"].replace("{slug}", slug)
|
||||
return jsonify({"ok": True, "file": meta})
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/media/list", methods=["GET"]) # type: ignore[untyped-decorator]
|
||||
def api_media_list(slug: str) -> tuple[Response, int] | Response:
|
||||
proj_dir = _project_dir(slug)
|
||||
if not proj_dir.exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
items = list_media(proj_dir)
|
||||
# patch urls
|
||||
for it in items:
|
||||
if "filename" in it and "date" in it:
|
||||
it["url"] = f"/sites/{slug}/media/images/{it['date']}/{it['filename']}"
|
||||
return jsonify(items)
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/media/<path:rel_path>", methods=["DELETE"]) # type: ignore[untyped-decorator]
|
||||
def api_media_delete(slug: str, rel_path: str) -> tuple[Response, int] | Response:
|
||||
proj_dir = _project_dir(slug)
|
||||
if not proj_dir.exists():
|
||||
return jsonify({"error": "專案不存在"}), 404
|
||||
ok = delete_media(proj_dir, rel_path)
|
||||
if not ok:
|
||||
return jsonify({"error": "刪除失敗或檔案不存在"}), 400
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/projects/<slug>/pages", methods=["POST"]) # type: ignore[untyped-decorator]
|
||||
def api_create_page(slug: str) -> tuple[Response, int]:
|
||||
if not _project_dir(slug).exists():
|
||||
|
||||
Reference in New Issue
Block a user