From f7d52e8cb00156b4bb29a45ff45fe0c3232829dd Mon Sep 17 00:00:00 2001 From: ila Date: Mon, 15 Jun 2026 16:48:37 +0800 Subject: [PATCH] feat: add path helper functions to file_service - get_app_dir(): project root in dev, exe dir in PyInstaller bundle - get_resource_path/get_config_path: paths under app dir - get_log_dir/get_output_dir: auto-create and return logs/ and output/ - Refactor log_service to use get_log_dir() instead of own __file__ calc Co-Authored-By: Claude Sonnet 4.6 --- src/services/file_service.py | 46 +++++++++++++++++++++++++++++++++++ src/services/log_service.py | 47 ++++++++++++++++++++++-------------- tasks.md | 18 +++++++------- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/services/file_service.py b/src/services/file_service.py index e8b8c0f..e8c25cb 100644 --- a/src/services/file_service.py +++ b/src/services/file_service.py @@ -1,5 +1,51 @@ +import logging +import sys +from pathlib import Path + +logger = logging.getLogger(__name__) + SUPPORTED_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"} def is_supported_image(path): return path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS + + +# --------------------------------------------------------------------------- +# Path helpers +# --------------------------------------------------------------------------- + +def get_app_dir(): + """Return the application root directory as a Path. + + PyInstaller onedir: directory that contains the .exe. + Development: project root (three levels above this file: + src/services/file_service.py -> src/services -> src -> project root). + """ + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return Path(__file__).resolve().parent.parent.parent + + +def get_resource_path(relative_path): + """Return absolute Path for a file under /resources/.""" + return get_app_dir() / "resources" / relative_path + + +def get_config_path(relative_path): + """Return absolute Path for a file under /config/.""" + return get_app_dir() / "config" / relative_path + + +def get_log_dir(): + """Return /logs/ as a Path, creating the directory if absent.""" + d = get_app_dir() / "logs" + d.mkdir(exist_ok=True) + return d + + +def get_output_dir(): + """Return /output/ as a Path, creating the directory if absent.""" + d = get_app_dir() / "output" + d.mkdir(exist_ok=True) + return d diff --git a/src/services/log_service.py b/src/services/log_service.py index af4f219..01a73e0 100644 --- a/src/services/log_service.py +++ b/src/services/log_service.py @@ -6,28 +6,32 @@ from datetime import datetime def setup_logging(log_dir=None): """Initialize file + console logging. Call once at application startup. - log_dir: absolute path to log directory. Defaults to /logs/. - If the directory cannot be created, falls back to console-only logging. + log_dir: str or Path to log directory. Defaults to /logs/ via + file_service.get_log_dir(). Falls back to console-only if the + directory cannot be created. """ if log_dir is None: - # src/services/log_service.py -> src/services -> src -> project root - _here = os.path.dirname(os.path.abspath(__file__)) - log_dir = os.path.join(os.path.dirname(os.path.dirname(_here)), "logs") - - try: - os.makedirs(log_dir, exist_ok=True) - except OSError as exc: - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(name)s %(levelname)s %(message)s", - ) - logging.getLogger(__name__).warning( - "Cannot create log directory %s: %s. Falling back to console only.", log_dir, exc - ) - return + try: + from services.file_service import get_log_dir + log_dir = get_log_dir() + except OSError as exc: + _setup_console_only() + logging.getLogger(__name__).warning( + "Cannot create log directory: %s. Falling back to console only.", exc + ) + return + else: + try: + os.makedirs(str(log_dir), exist_ok=True) + except OSError as exc: + _setup_console_only() + logging.getLogger(__name__).warning( + "Cannot create log directory %s: %s. Falling back to console only.", log_dir, exc + ) + return timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = os.path.join(log_dir, "app_{}.log".format(timestamp)) + log_file = os.path.join(str(log_dir), "app_{}.log".format(timestamp)) formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") @@ -43,3 +47,10 @@ def setup_logging(log_dir=None): root.addHandler(sh) logging.getLogger(__name__).info("Logging initialized. Log file: %s", log_file) + + +def _setup_console_only(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s %(message)s", + ) diff --git a/tasks.md b/tasks.md index f25c6bb..7257413 100644 --- a/tasks.md +++ b/tasks.md @@ -87,18 +87,18 @@ 任务: -- [ ] 在 `src/services/file_service.py` 中新增路径辅助函数(不单独建模块) -- [ ] 实现 `get_app_dir()`:返回程序根目录,兼容开发环境和 PyInstaller 打包环境 -- [ ] 实现 `get_resource_path(relative_path)`:返回资源文件绝对路径 -- [ ] 实现 `get_config_path(relative_path)`:返回配置文件绝对路径 -- [ ] 实现 `get_log_dir()`:返回日志目录路径 -- [ ] 实现 `get_output_dir()`:返回默认输出目录路径 -- [ ] 保证 Windows 中文路径可用 +- [x] 在 `src/services/file_service.py` 中新增路径辅助函数(不单独建模块) +- [x] 实现 `get_app_dir()`:返回程序根目录,兼容开发环境和 PyInstaller 打包环境 +- [x] 实现 `get_resource_path(relative_path)`:返回资源文件绝对路径 +- [x] 实现 `get_config_path(relative_path)`:返回配置文件绝对路径 +- [x] 实现 `get_log_dir()`:返回日志目录路径 +- [x] 实现 `get_output_dir()`:返回默认输出目录路径 +- [x] 保证 Windows 中文路径可用 验收: -- [ ] 路径函数不使用开发机绝对路径 -- [ ] 程序目录下缺少 `logs/` 或 `output/` 时可自动创建 +- [x] 路径函数不使用开发机绝对路径 +- [x] 程序目录下缺少 `logs/` 或 `output/` 时可自动创建 ### 1.3 配置服务