feat: separate writable data dir from program dir (LAN update groundwork)
- add get_data_dir() with CMBOT_DATA_DIR override, fallback to program dir - point config/logs/output at the data root; resources stay on program root - harden config/template writes with parents=True for a deep data root - add docs/10-lan-update.md (LAN auto-update design); default install root %LOCALAPPDATA%\CMBot for elevation-free self-update Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ def save_config(data):
|
||||
config_file = get_config_path(_CONFIG_FILENAME)
|
||||
|
||||
try:
|
||||
config_file.parent.mkdir(exist_ok=True)
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(str(config_file), "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logger.info("Config saved to %s", config_file)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -19,19 +20,41 @@ def is_supported_image(path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_app_dir():
|
||||
"""Return the application root directory as a Path.
|
||||
"""Return the program root directory (read-only program files) as a Path.
|
||||
|
||||
PyInstaller onedir: directory that contains the .exe.
|
||||
PyInstaller onedir: directory that contains the .exe. In the versioned
|
||||
install layout (docs/10-lan-update.md) this is versions/<x.y.z>/, which is
|
||||
replaced wholesale on update.
|
||||
Development: project root (three levels above this file:
|
||||
src/services/file_service.py -> src/services -> src -> project root).
|
||||
|
||||
Resources live here; writable user data does not — use get_data_dir().
|
||||
"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return Path(sys.executable).resolve().parent
|
||||
return Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def get_data_dir():
|
||||
"""Return the writable data root (config, templates, logs, output) as a Path.
|
||||
|
||||
Kept separate from the program root so that replacing the program on update
|
||||
never touches user data (docs/10-lan-update.md §5).
|
||||
|
||||
Resolution order:
|
||||
1. CMBOT_DATA_DIR environment variable, when set. The launcher points this
|
||||
at the install-wide data/ folder in the versioned layout.
|
||||
2. Fallback to get_app_dir() — the flat layout used in development and in
|
||||
the current onedir release, where data sits next to the program.
|
||||
"""
|
||||
env = os.environ.get("CMBOT_DATA_DIR", "").strip()
|
||||
if env:
|
||||
return Path(env)
|
||||
return get_app_dir()
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""Return absolute Path for a resource file.
|
||||
"""Return absolute Path for a resource file under the program root.
|
||||
|
||||
Development resources live under src/resources/. In a PyInstaller onedir
|
||||
build, resources are copied next to the executable under resources/.
|
||||
@@ -42,21 +65,21 @@ def get_resource_path(relative_path):
|
||||
|
||||
|
||||
def get_config_path(relative_path):
|
||||
"""Return absolute Path for a file under <app_dir>/config/."""
|
||||
return get_app_dir() / "config" / relative_path
|
||||
"""Return absolute Path for a file under <data_dir>/config/."""
|
||||
return get_data_dir() / "config" / relative_path
|
||||
|
||||
|
||||
def get_log_dir():
|
||||
"""Return <app_dir>/logs/ as a Path, creating the directory if absent."""
|
||||
d = get_app_dir() / "logs"
|
||||
d.mkdir(exist_ok=True)
|
||||
"""Return <data_dir>/logs/ as a Path, creating the directory if absent."""
|
||||
d = get_data_dir() / "logs"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def get_output_dir():
|
||||
"""Return <app_dir>/output/ as a Path, creating the directory if absent."""
|
||||
d = get_app_dir() / "output"
|
||||
d.mkdir(exist_ok=True)
|
||||
"""Return <data_dir>/output/ as a Path, creating the directory if absent."""
|
||||
d = get_data_dir() / "output"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def _save_custom_templates(templates: List[Template]):
|
||||
"""Write custom templates to JSON. Logs error on failure, does not raise."""
|
||||
path = _templates_file()
|
||||
try:
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"templates": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user