chore: consolidate runtime data layout under single data/ root directory

Migrate all runtime artifacts (database, uploads, VAPID keys, locks, bot state, container workspaces, zip staging) from scattered locations (`var/`, `devplacepy/static/uploads/`) into a unified `data/` directory. Update `config.py` as single source of truth with `DATA_PATHS` registry and `ensure_data_dirs()`, add `devplace migrate-data` CLI command with CRC verification and idempotent relocation, adjust `.dockerignore`, `.env.example`, `.gitignore`, `Dockerfile`, `Makefile`, `README.md`, `AGENTS.md`, `CLAUDE.md`, and all import paths in `attachments.py` to reference `config.UPLOADS_DIR`/`ATTACHMENTS_DIR` instead of computing from `STATIC_DIR`.
This commit is contained in:
2026-06-13 19:06:43 +00:00
parent de745f7a75
commit 1294c95bed
24 changed files with 378 additions and 80 deletions
+49 -9
View File
@@ -9,8 +9,29 @@ load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_DIR = BASE_DIR / "devplacepy" / "static"
TEMPLATES_DIR = BASE_DIR / "devplacepy" / "templates"
# Single source of truth for every runtime/user-generated artifact. Everything the
# app creates or modifies at runtime lives under DATA_DIR, never inside the package
# and never served via /static. Overridable by DEVPLACE_DATA_DIR (point at a volume
# in production). Each path below is derived from DATA_DIR; no other module computes
# a runtime path from scratch.
DATA_DIR = Path(environ.get("DEVPLACE_DATA_DIR", str(BASE_DIR / "data")))
UPLOADS_DIR = DATA_DIR / "uploads"
ATTACHMENTS_DIR = UPLOADS_DIR / "attachments"
PROJECT_FILES_DIR = UPLOADS_DIR / "project_files"
CONTAINER_WORKSPACES_DIR = DATA_DIR / "container_workspaces"
ZIPS_DIR = DATA_DIR / "zips"
ZIP_STAGING_DIR = DATA_DIR / "zip_staging"
FORK_STAGING_DIR = DATA_DIR / "fork_staging"
KEYS_DIR = DATA_DIR / "keys"
BOT_DIR = DATA_DIR / "bot"
LOCKS_DIR = DATA_DIR / "locks"
DEVII_TASKS_DB = DATA_DIR / "devii_tasks.db"
DEVII_LESSONS_DB = DATA_DIR / "devii_lessons.db"
DATABASE_URL = environ.get(
"DEVPLACE_DATABASE_URL", f"sqlite:///{BASE_DIR / 'devplace.db'}"
"DEVPLACE_DATABASE_URL", f"sqlite:///{DATA_DIR / 'devplace.db'}"
)
SECRET_KEY = environ.get("SECRET_KEY", "devplace-secret-key-change-in-production")
SECONDS_PER_DAY = 86400
@@ -25,12 +46,9 @@ INTERNAL_BASE_URL = environ.get(
INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions"
INTERNAL_MODEL = "molodetz"
SERVICE_LOCK_FILE = BASE_DIR / "devplace-services.lock"
INIT_LOCK_FILE = BASE_DIR / "devplace-init.lock"
SERVICE_LOCK_FILE = LOCKS_DIR / "devplace-services.lock"
INIT_LOCK_FILE = LOCKS_DIR / "devplace-init.lock"
# Container data lives OUTSIDE the package (never served via /static, never watched by --reload).
DATA_DIR = Path(environ.get("DEVPLACE_DATA_DIR", str(BASE_DIR / "var")))
CONTAINER_WORKSPACES_DIR = DATA_DIR / "container_workspaces"
# Every container instance runs this one prebuilt image (built once via `make ppy`).
CONTAINER_IMAGE = environ.get("DEVPLACE_CONTAINER_IMAGE", "ppy:latest")
# Override host the /p/<slug> ingress proxy dials, with the published host port,
@@ -41,7 +59,29 @@ CONTAINER_IMAGE = environ.get("DEVPLACE_CONTAINER_IMAGE", "ppy:latest")
# port-publishing layer (docker-proxy / iptables DNAT / loopback) entirely.
CONTAINER_PROXY_HOST = environ.get("DEVPLACE_CONTAINER_PROXY_HOST", "").strip()
VAPID_PRIVATE_KEY_FILE = BASE_DIR / "notification-private.pem"
VAPID_PRIVATE_KEY_PKCS8_FILE = BASE_DIR / "notification-private.pkcs8.pem"
VAPID_PUBLIC_KEY_FILE = BASE_DIR / "notification-public.pem"
VAPID_PRIVATE_KEY_FILE = KEYS_DIR / "notification-private.pem"
VAPID_PRIVATE_KEY_PKCS8_FILE = KEYS_DIR / "notification-private.pkcs8.pem"
VAPID_PUBLIC_KEY_FILE = KEYS_DIR / "notification-public.pem"
VAPID_SUB = environ.get("DEVPLACE_VAPID_SUB", "mailto:retoor@molodetz.nl")
# Documented registry of every runtime directory. ensure_data_dirs() creates them
# all at startup so the tree always exists before the DB, keys, locks, uploads, and
# job staging are written.
DATA_PATHS: dict[str, Path] = {
"data": DATA_DIR,
"uploads": UPLOADS_DIR,
"attachments": ATTACHMENTS_DIR,
"project_files": PROJECT_FILES_DIR,
"container_workspaces": CONTAINER_WORKSPACES_DIR,
"zips": ZIPS_DIR,
"zip_staging": ZIP_STAGING_DIR,
"fork_staging": FORK_STAGING_DIR,
"keys": KEYS_DIR,
"bot": BOT_DIR,
"locks": LOCKS_DIR,
}
def ensure_data_dirs() -> None:
for path in DATA_PATHS.values():
path.mkdir(parents=True, exist_ok=True)