2026-05-17 22:11:46 +08:00
|
|
|
|
"""VvvebJS 網頁管理器 — Flask 主程式."""
|
2026-05-17 21:10:00 +08:00
|
|
|
|
|
2026-05-17 22:11:46 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import re
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from flask import Flask, Response, abort, jsonify, render_template, request, send_from_directory
|
|
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 路徑常數 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
BASE_DIR = Path(__file__).parent
|
|
|
|
|
|
WEBSITES_DIR = BASE_DIR / "websites"
|
|
|
|
|
|
BLANK_TEMPLATE = BASE_DIR / "static" / "Vvvebjs" / "new-page-blank-template.html"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 輔助函式 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def slugify(text: str) -> str:
|
|
|
|
|
|
"""將文字轉成 URL-safe slug."""
|
|
|
|
|
|
text = text.lower().strip()
|
|
|
|
|
|
text = re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)
|
|
|
|
|
|
text = re.sub(r"[\s_-]+", "-", text)
|
|
|
|
|
|
text = re.sub(r"^-+|-+$", "", text)
|
|
|
|
|
|
return text or "untitled"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _now_iso() -> str:
|
|
|
|
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _project_dir(slug: str) -> Path:
|
|
|
|
|
|
return WEBSITES_DIR / slug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _project_json_path(slug: str) -> Path:
|
|
|
|
|
|
return _project_dir(slug) / "project.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_project(slug: str) -> dict[str, Any]:
|
|
|
|
|
|
"""讀取專案設定,若不存在則回傳預設值."""
|
|
|
|
|
|
path = _project_json_path(slug)
|
|
|
|
|
|
if path.exists():
|
|
|
|
|
|
data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
return data
|
|
|
|
|
|
return {
|
|
|
|
|
|
"name": slug,
|
|
|
|
|
|
"slug": slug,
|
|
|
|
|
|
"description": "",
|
|
|
|
|
|
"created_at": _now_iso(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_project(slug: str, data: dict[str, Any]) -> None:
|
|
|
|
|
|
path = _project_json_path(slug)
|
|
|
|
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _list_pages(slug: str) -> list[str]:
|
|
|
|
|
|
"""列出專案目錄下所有 .html 頁面."""
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
if not proj_dir.exists():
|
|
|
|
|
|
return []
|
|
|
|
|
|
return sorted(p.name for p in proj_dir.glob("*.html"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _project_summary(slug: str) -> dict[str, Any]:
|
|
|
|
|
|
data = _load_project(slug)
|
|
|
|
|
|
pages = _list_pages(slug)
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
html_files = list(proj_dir.glob("*.html"))
|
|
|
|
|
|
if html_files:
|
|
|
|
|
|
last_mod = max(f.stat().st_mtime for f in html_files)
|
|
|
|
|
|
last_mod_str = datetime.fromtimestamp(last_mod, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
|
else:
|
|
|
|
|
|
last_mod_str = str(data.get("created_at", _now_iso()))
|
|
|
|
|
|
return {
|
|
|
|
|
|
**data,
|
|
|
|
|
|
"slug": slug,
|
|
|
|
|
|
"page_count": len(pages),
|
|
|
|
|
|
"pages": pages,
|
|
|
|
|
|
"last_modified": last_mod_str,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sanitize_file_path(slug: str, filename: str) -> Path | None:
|
|
|
|
|
|
"""驗證並回傳安全的檔案路徑(防止路徑遍歷)."""
|
|
|
|
|
|
proj_dir = _project_dir(slug).resolve()
|
|
|
|
|
|
target = (proj_dir / Path(filename).name).resolve()
|
|
|
|
|
|
if not str(target).startswith(str(proj_dir)):
|
|
|
|
|
|
return None
|
|
|
|
|
|
if target.suffix.lower() != ".html":
|
|
|
|
|
|
return None
|
|
|
|
|
|
return target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _copy_blank_template(dest: Path) -> None:
|
|
|
|
|
|
"""複製空白範本或建立最小 HTML."""
|
|
|
|
|
|
if BLANK_TEMPLATE.exists():
|
|
|
|
|
|
shutil.copy(BLANK_TEMPLATE, dest)
|
|
|
|
|
|
else:
|
|
|
|
|
|
dest.write_text(
|
|
|
|
|
|
"<!DOCTYPE html><html><head><title>My Page</title></head><body></body></html>",
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 路由:一般頁面 ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/") # type: ignore[untyped-decorator]
|
|
|
|
|
|
def index() -> Response:
|
|
|
|
|
|
from flask import redirect, url_for
|
|
|
|
|
|
return redirect(url_for("dashboard"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/dashboard") # type: ignore[untyped-decorator]
|
|
|
|
|
|
def dashboard() -> str:
|
|
|
|
|
|
return str(render_template("dashboard.html"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/editor/<slug>") # type: ignore[untyped-decorator]
|
|
|
|
|
|
def editor(slug: str) -> str:
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
if not proj_dir.exists():
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
pages = _list_pages(slug)
|
|
|
|
|
|
project = _load_project(slug)
|
|
|
|
|
|
|
|
|
|
|
|
pages_obj: dict[str, Any] = {}
|
|
|
|
|
|
for page in pages:
|
|
|
|
|
|
name = page.replace(".html", "")
|
|
|
|
|
|
pages_obj[name] = {
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"title": name.replace("-", " ").title(),
|
|
|
|
|
|
"filename": page,
|
|
|
|
|
|
"file": page,
|
|
|
|
|
|
"url": f"/sites/{slug}/{page}",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if not pages_obj:
|
|
|
|
|
|
idx_path = proj_dir / "index.html"
|
|
|
|
|
|
_copy_blank_template(idx_path)
|
|
|
|
|
|
pages_obj["index"] = {
|
|
|
|
|
|
"name": "index",
|
|
|
|
|
|
"title": "Index",
|
|
|
|
|
|
"filename": "index.html",
|
|
|
|
|
|
"file": "index.html",
|
|
|
|
|
|
"url": f"/sites/{slug}/index.html",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return str(render_template(
|
|
|
|
|
|
"editor.html",
|
|
|
|
|
|
slug=slug,
|
|
|
|
|
|
project=project,
|
|
|
|
|
|
pages_json=json.dumps(pages_obj, ensure_ascii=False),
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 路由:API ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/projects", methods=["GET"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_list_projects() -> Response:
|
|
|
|
|
|
WEBSITES_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
projects = [_project_summary(d.name) for d in sorted(WEBSITES_DIR.iterdir()) if d.is_dir()]
|
|
|
|
|
|
return jsonify(projects)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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 {}
|
|
|
|
|
|
name: str = str(body.get("name", "")).strip()
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return jsonify({"error": "名稱不能為空"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
slug = slugify(name)
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
base_slug = slug
|
|
|
|
|
|
counter = 1
|
|
|
|
|
|
while proj_dir.exists():
|
|
|
|
|
|
slug = f"{base_slug}-{counter}"
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
counter += 1
|
|
|
|
|
|
|
|
|
|
|
|
proj_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
data: dict[str, Any] = {
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"slug": slug,
|
|
|
|
|
|
"description": str(body.get("description", "")),
|
|
|
|
|
|
"created_at": _now_iso(),
|
|
|
|
|
|
}
|
|
|
|
|
|
_save_project(slug, data)
|
|
|
|
|
|
_copy_blank_template(proj_dir / "index.html")
|
|
|
|
|
|
return jsonify(_project_summary(slug)), 201
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/projects/<slug>", methods=["GET"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_get_project(slug: str) -> tuple[Response, int] | Response:
|
|
|
|
|
|
if not _project_dir(slug).exists():
|
|
|
|
|
|
return jsonify({"error": "專案不存在"}), 404
|
|
|
|
|
|
return jsonify(_project_summary(slug))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/projects/<slug>", methods=["PUT"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_update_project(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)
|
|
|
|
|
|
if "name" in body and str(body["name"]).strip():
|
|
|
|
|
|
data["name"] = str(body["name"]).strip()
|
|
|
|
|
|
if "description" in body:
|
|
|
|
|
|
data["description"] = str(body["description"])
|
|
|
|
|
|
data["updated_at"] = _now_iso()
|
|
|
|
|
|
_save_project(slug, data)
|
|
|
|
|
|
return jsonify(_project_summary(slug))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/projects/<slug>", methods=["DELETE"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_delete_project(slug: str) -> tuple[Response, int] | Response:
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
if not proj_dir.exists():
|
|
|
|
|
|
return jsonify({"error": "專案不存在"}), 404
|
|
|
|
|
|
shutil.rmtree(proj_dir)
|
|
|
|
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/projects/<slug>/pages", methods=["GET"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_list_pages(slug: str) -> tuple[Response, int] | Response:
|
|
|
|
|
|
if not _project_dir(slug).exists():
|
|
|
|
|
|
return jsonify({"error": "專案不存在"}), 404
|
|
|
|
|
|
return jsonify(_list_pages(slug))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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():
|
|
|
|
|
|
return jsonify({"error": "專案不存在"}), 404
|
|
|
|
|
|
body: dict[str, Any] = request.get_json(force=True) or {}
|
|
|
|
|
|
page_name: str = str(body.get("name", "")).strip()
|
|
|
|
|
|
if not page_name:
|
|
|
|
|
|
return jsonify({"error": "頁面名稱不能為空"}), 400
|
|
|
|
|
|
page_slug = slugify(page_name)
|
|
|
|
|
|
page_path = _project_dir(slug) / f"{page_slug}.html"
|
|
|
|
|
|
if page_path.exists():
|
|
|
|
|
|
return jsonify({"error": "頁面已存在"}), 409
|
|
|
|
|
|
_copy_blank_template(page_path)
|
|
|
|
|
|
return jsonify({"page": f"{page_slug}.html"}), 201
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/save", methods=["POST"]) # type: ignore[untyped-decorator]
|
|
|
|
|
|
def api_save() -> tuple[Response, int] | Response:
|
2026-05-17 22:44:11 +08:00
|
|
|
|
"""接受 vvvebjs 的儲存或新增頁面請求,寫入對應專案目錄."""
|
2026-05-17 22:11:46 +08:00
|
|
|
|
if request.is_json:
|
|
|
|
|
|
raw: dict[str, Any] = request.get_json(force=True) or {}
|
|
|
|
|
|
body: dict[str, Any] = raw
|
|
|
|
|
|
else:
|
|
|
|
|
|
form = dict(request.form)
|
|
|
|
|
|
body = {k: (v[0] if isinstance(v, list) else v) for k, v in form.items()}
|
|
|
|
|
|
|
|
|
|
|
|
slug: str = str(body.get("slug", "")).strip()
|
2026-05-18 12:45:57 +08:00
|
|
|
|
if not slug:
|
|
|
|
|
|
# Fallback to query parameter if not in body (e.g. for some raw requests)
|
|
|
|
|
|
slug = request.args.get("slug", "").strip()
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
if not slug:
|
|
|
|
|
|
return jsonify({"error": "缺少專案識別碼 (slug)"}), 400
|
2026-05-17 22:11:46 +08:00
|
|
|
|
|
|
|
|
|
|
if not _project_dir(slug).exists():
|
|
|
|
|
|
return jsonify({"error": "專案不存在"}), 404
|
|
|
|
|
|
|
2026-05-18 12:45:57 +08:00
|
|
|
|
action = request.args.get("action", "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
# ── 處理重新命名 / 複製頁面 ──
|
|
|
|
|
|
if action == "rename":
|
|
|
|
|
|
file = str(body.get("file", "")).strip()
|
|
|
|
|
|
newfile = str(body.get("newfile", "")).strip()
|
|
|
|
|
|
duplicate_str = str(body.get("duplicate", "")).strip().lower()
|
|
|
|
|
|
is_duplicate = (duplicate_str == "true")
|
|
|
|
|
|
|
|
|
|
|
|
if not file or not newfile:
|
|
|
|
|
|
return jsonify({"error": "缺少參數 file 或 newfile"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
old_path = _sanitize_file_path(slug, file)
|
|
|
|
|
|
new_path = _sanitize_file_path(slug, newfile)
|
|
|
|
|
|
|
|
|
|
|
|
if old_path is None or new_path is None:
|
|
|
|
|
|
return jsonify({"error": "不合法的頁面名稱"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
if not old_path.exists():
|
|
|
|
|
|
return jsonify({"error": "來源頁面不存在"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
if new_path.exists() and old_path != new_path:
|
|
|
|
|
|
return jsonify({"error": "目標頁面已存在"}), 409
|
|
|
|
|
|
|
|
|
|
|
|
if is_duplicate:
|
|
|
|
|
|
shutil.copy(old_path, new_path)
|
|
|
|
|
|
msg = "頁面複製成功"
|
|
|
|
|
|
else:
|
|
|
|
|
|
old_path.rename(new_path)
|
|
|
|
|
|
msg = "頁面重新命名成功"
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"ok": True,
|
|
|
|
|
|
"message": msg,
|
|
|
|
|
|
"newfile": new_path.name,
|
|
|
|
|
|
"url": f"/sites/{slug}/{new_path.name}"
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# ── 處理刪除頁面 ──
|
|
|
|
|
|
elif action == "delete":
|
|
|
|
|
|
file = str(body.get("file", "")).strip()
|
|
|
|
|
|
if not file:
|
|
|
|
|
|
return jsonify({"error": "缺少參數 file"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
if file.lower() == "index.html":
|
|
|
|
|
|
return jsonify({"error": "無法刪除主頁 index.html"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
safe_path = _sanitize_file_path(slug, file)
|
|
|
|
|
|
if safe_path is None:
|
|
|
|
|
|
return jsonify({"error": "不合法的頁面名稱"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
if not safe_path.exists():
|
|
|
|
|
|
return jsonify({"error": "頁面不存在"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
safe_path.unlink()
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"ok": True,
|
|
|
|
|
|
"message": "頁面已成功刪除"
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
# 1. 判斷是否為新增頁面請求 (含有 startTemplateUrl)
|
|
|
|
|
|
start_template_url = str(body.get("startTemplateUrl", "")).strip()
|
|
|
|
|
|
if start_template_url:
|
|
|
|
|
|
title = str(body.get("title", "")).strip() or "New Page"
|
|
|
|
|
|
# 取得安全的檔名 (只取檔名部分,例如 'about.html')
|
|
|
|
|
|
filename = Path(str(body.get("file", "untitled.html")).strip()).name
|
|
|
|
|
|
if not filename.endswith(".html"):
|
|
|
|
|
|
filename += ".html"
|
|
|
|
|
|
|
|
|
|
|
|
safe_path = _sanitize_file_path(slug, filename)
|
|
|
|
|
|
if safe_path is None:
|
|
|
|
|
|
return jsonify({"error": "不合法的頁面名稱"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
if safe_path.exists():
|
|
|
|
|
|
return jsonify({"error": "頁面已存在"}), 409
|
|
|
|
|
|
|
|
|
|
|
|
# 解析並複製樣板 (定位在 static/Vvvebjs 目錄下)
|
|
|
|
|
|
template_source = BASE_DIR / "static" / "Vvvebjs" / start_template_url
|
|
|
|
|
|
if template_source.exists() and template_source.is_file():
|
|
|
|
|
|
shutil.copy(template_source, safe_path)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_copy_blank_template(safe_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 回傳 VvvebJS FileManager 所期待的 JSON 格式
|
|
|
|
|
|
page_name = safe_path.stem
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
"ok": True,
|
|
|
|
|
|
"name": page_name,
|
|
|
|
|
|
"title": title,
|
|
|
|
|
|
"file": safe_path.name,
|
|
|
|
|
|
"url": f"/sites/{slug}/{safe_path.name}"
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 一般儲存頁面請求
|
|
|
|
|
|
filename = str(body.get("file", "")).strip()
|
|
|
|
|
|
html: str = str(body.get("html", "")).strip()
|
|
|
|
|
|
|
|
|
|
|
|
if not filename or not html:
|
|
|
|
|
|
return jsonify({"error": "缺少必要參數 file / html"}), 400
|
|
|
|
|
|
|
2026-05-17 22:11:46 +08:00
|
|
|
|
safe_path = _sanitize_file_path(slug, filename)
|
|
|
|
|
|
if safe_path is None:
|
|
|
|
|
|
return jsonify({"error": "不合法的檔案路徑"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
safe_path.write_text(html, encoding="utf-8")
|
|
|
|
|
|
return jsonify({"ok": True, "saved": safe_path.name})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
|
2026-05-17 22:11:46 +08:00
|
|
|
|
@app.route("/sites/<slug>/<path:filename>") # type: ignore[untyped-decorator]
|
|
|
|
|
|
def serve_site_file(slug: str, filename: str) -> Response:
|
|
|
|
|
|
proj_dir = _project_dir(slug)
|
|
|
|
|
|
if not proj_dir.exists():
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
return send_from_directory(proj_dir, filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 啟動 ────────────────────────────────────────────────────────────────────
|
2026-05-17 21:10:00 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-05-17 22:11:46 +08:00
|
|
|
|
WEBSITES_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
app.run(debug=True, port=5000)
|