launcher.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """DMS 一体化启动入口(PyInstaller 打包用)。
  2. 职责:
  3. 1. 设置运行时环境变量(.env、存储目录)
  4. 2. 延后导入并启动 Flask 应用
  5. 设计约束:
  6. - 必须在 ``import app`` 之前完成环境变量设置,
  7. 因为 app.py 在导入时即调用 ``init_dms(app)`` 读取配置。
  8. - 自实现 .env 解析,避免新增 python-dotenv 依赖。
  9. """
  10. from __future__ import annotations
  11. import os
  12. import shutil
  13. import sys
  14. from pathlib import Path
  15. def _app_root() -> Path:
  16. """exe 同级目录(用户可见、可写)。
  17. - PyInstaller 打包模式:``sys.executable`` 是 exe 文件本身
  18. - 开发模式:本文件位于 ``backend/launcher.py``,root 在上一级
  19. """
  20. if getattr(sys, "frozen", False):
  21. return Path(sys.executable).resolve().parent
  22. return Path(__file__).resolve().parent.parent
  23. def _load_dotenv(env_path: Path) -> None:
  24. """简易 .env 解析器。
  25. 仅做 ``key=value`` 解析,``setdefault`` 不覆盖已有环境变量。
  26. 不依赖 python-dotenv,避免在 requirements.txt 中新增依赖。
  27. """
  28. if not env_path.exists():
  29. return
  30. with open(env_path, "r", encoding="utf-8") as f:
  31. for raw in f:
  32. line = raw.strip()
  33. if not line or line.startswith("#") or "=" not in line:
  34. continue
  35. key, _, value = line.partition("=")
  36. key = key.strip()
  37. value = value.strip().strip('"').strip("'")
  38. os.environ.setdefault(key, value)
  39. def _ensure_runtime_dirs(root: Path) -> None:
  40. """在 exe 同级创建 dms-storage 子目录骨架,并写入环境变量。"""
  41. storage = root / "dms-storage"
  42. storage.mkdir(parents=True, exist_ok=True)
  43. for sub in (
  44. "extracted",
  45. "original",
  46. "preview",
  47. "quarantine",
  48. "recycle",
  49. "temporary",
  50. ):
  51. (storage / sub).mkdir(exist_ok=True)
  52. # 仅当 DMS_STORAGE_ROOT 未设置或为空时写入默认值。
  53. # 不能用 setdefault:_load_dotenv 会把 .env 中的空字符串注入环境变量,
  54. # 使 setdefault 误判为「已设置」,从而导致打包后默认存储路径失效。
  55. current_storage = os.environ.get("DMS_STORAGE_ROOT", "").strip()
  56. if not current_storage:
  57. os.environ["DMS_STORAGE_ROOT"] = str(storage)
  58. def _bootstrap_env() -> Path:
  59. """准备运行环境,返回 app_root。"""
  60. root = _app_root()
  61. env_file = root / ".env"
  62. env_example = root / ".env.example"
  63. # 首次启动:从模板复制 .env
  64. if not env_file.exists() and env_example.exists():
  65. shutil.copy(env_example, env_file)
  66. print("=" * 60)
  67. print("[首次启动] 已生成配置文件:")
  68. print(f" {env_file}")
  69. print("请编辑该文件填入数据库连接(DMS_DATABASE_URL)等信息,")
  70. print("保存后重新启动本程序。")
  71. print("=" * 60)
  72. _load_dotenv(env_file)
  73. _ensure_runtime_dirs(root)
  74. return root
  75. def main() -> None:
  76. root = _bootstrap_env()
  77. # 延后导入:确保上面的环境变量先生效
  78. from app import app
  79. host = os.environ.get("DMS_HOST", "0.0.0.0")
  80. port = int(os.environ.get("DMS_PORT", "9345"))
  81. print(f"[DMS] 启动中... 访问地址: http://localhost:{port}")
  82. print(f"[DMS] 工作目录: {root}")
  83. print(f"[DMS] 按 Ctrl+C 退出")
  84. print("-" * 60)
  85. app.run(host=host, port=port, debug=False, use_reloader=False)
  86. if __name__ == "__main__":
  87. main()