From 4787fdbf22733a7d5478f9774439ea0aa35a6bdc Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Sat, 18 Jul 2026 08:05:39 +0800 Subject: [PATCH] refactor(config): consolidate into single config.json, storage backend selectable --- .gitignore | 1 + app/storage.py | 78 ++++++++++++++++++++++++------------------ config.example.json | 13 +++++++ s3_config.example.json | 9 ----- 4 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 config.example.json delete mode 100644 s3_config.example.json diff --git a/.gitignore b/.gitignore index f706380..e5d5b57 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ uploads/* .env server.log s3_config.json +config.json scripts/fix_*.py scripts/migrate_*.py scripts/test_*.py diff --git a/app/storage.py b/app/storage.py index 42d3560..d54dfcf 100644 --- a/app/storage.py +++ b/app/storage.py @@ -1,18 +1,22 @@ """Image storage abstraction: local filesystem or S3-compatible object storage. -S3 config is read from ``s3_config.json`` in the project root:: +All system configuration lives in ``config.json`` in the project root:: { - "enabled": true, - "endpoint": "https://s3.amazonaws.com", - "bucket": "my-mailova-bucket", - "region": "auto", - "access_key": "AKIA...", - "secret_key": "...", - "public_url": "https://cdn.example.com/mailova" + "storage": { + "backend": "local", + "s3": { + "endpoint": "https://s3.amazonaws.com", + "bucket": "my-mailova-bucket", + "region": "auto", + "access_key": "AKIA...", + "secret_key": "...", + "public_url": "https://cdn.example.com/mailova" + } + } } -When the file is missing or ``enabled`` is false, local storage is used. +``storage.backend``: ``"local"`` (default) or ``"s3"``. URL scheme ~~~~~~~~~~ @@ -33,7 +37,7 @@ from typing import Protocol from app.config import UPLOAD_DIR -S3_CONFIG_PATH = Path(__file__).resolve().parent.parent / "s3_config.json" +CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json" class StorageBackend(Protocol): @@ -128,40 +132,46 @@ def _strip_prefix(key: str) -> str: return key +# ── Config loading ───────────────────────────────────────────────── + +def load_config() -> dict: + """Load config.json. Returns empty dict if missing.""" + if not CONFIG_PATH.exists(): + return {} + try: + return json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + except Exception: + return {} + + # ── Singleton accessor ───────────────────────────────────────────── _backend: StorageBackend | None = None -def _load_s3_config() -> dict | None: - """Read s3_config.json. Returns None if not configured / disabled.""" - if not S3_CONFIG_PATH.exists(): - return None - try: - cfg = json.loads(S3_CONFIG_PATH.read_text(encoding="utf-8")) - if not cfg.get("enabled") or not cfg.get("bucket"): - return None - return cfg - except Exception: - return None - - def get_storage() -> StorageBackend: - """Get the active storage backend (S3 if configured, otherwise local).""" + """Get the active storage backend based on config.json.""" global _backend if _backend is not None: return _backend - s3_cfg = _load_s3_config() - if s3_cfg: - _backend = S3Storage( - endpoint_url=s3_cfg.get("endpoint", ""), - bucket=s3_cfg["bucket"], - access_key=s3_cfg.get("access_key", ""), - secret_key=s3_cfg.get("secret_key", ""), - region=s3_cfg.get("region", "auto"), - public_url=s3_cfg.get("public_url", ""), - ) + cfg = load_config() + backend = cfg.get("storage", {}).get("backend", "local") + + if backend == "s3": + s3 = cfg.get("storage", {}).get("s3", {}) + if not s3.get("bucket"): + # fallback to local if S3 is misconfigured + _backend = LocalStorage() + else: + _backend = S3Storage( + endpoint_url=s3.get("endpoint", ""), + bucket=s3["bucket"], + access_key=s3.get("access_key", ""), + secret_key=s3.get("secret_key", ""), + region=s3.get("region", "auto"), + public_url=s3.get("public_url", ""), + ) else: _backend = LocalStorage() return _backend diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..c1f4168 --- /dev/null +++ b/config.example.json @@ -0,0 +1,13 @@ +{ + "storage": { + "backend": "local", + "s3": { + "endpoint": "https://s3.amazonaws.com", + "bucket": "my-mailova-bucket", + "region": "auto", + "access_key": "AKIA...", + "secret_key": "...", + "public_url": "https://cdn.example.com/mailova" + } + } +} diff --git a/s3_config.example.json b/s3_config.example.json deleted file mode 100644 index d670ef2..0000000 --- a/s3_config.example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "enabled": false, - "endpoint": "https://s3.amazonaws.com", - "bucket": "my-mailova-bucket", - "region": "auto", - "access_key": "AKIA...", - "secret_key": "...", - "public_url": "https://cdn.example.com/mailova" -}