refactor(config): consolidate into single config.json, storage backend selectable

This commit is contained in:
2026-07-18 08:05:39 +08:00
parent 3a931fe752
commit 4787fdbf22
4 changed files with 58 additions and 43 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ uploads/*
.env .env
server.log server.log
s3_config.json s3_config.json
config.json
scripts/fix_*.py scripts/fix_*.py
scripts/migrate_*.py scripts/migrate_*.py
scripts/test_*.py scripts/test_*.py

View File

@@ -1,9 +1,11 @@
"""Image storage abstraction: local filesystem or S3-compatible object storage. """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, "storage": {
"backend": "local",
"s3": {
"endpoint": "https://s3.amazonaws.com", "endpoint": "https://s3.amazonaws.com",
"bucket": "my-mailova-bucket", "bucket": "my-mailova-bucket",
"region": "auto", "region": "auto",
@@ -11,8 +13,10 @@ S3 config is read from ``s3_config.json`` in the project root::
"secret_key": "...", "secret_key": "...",
"public_url": "https://cdn.example.com/mailova" "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 URL scheme
~~~~~~~~~~ ~~~~~~~~~~
@@ -33,7 +37,7 @@ from typing import Protocol
from app.config import UPLOAD_DIR 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): class StorageBackend(Protocol):
@@ -128,39 +132,45 @@ def _strip_prefix(key: str) -> str:
return key 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 ───────────────────────────────────────────── # ── Singleton accessor ─────────────────────────────────────────────
_backend: StorageBackend | None = None _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: 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 global _backend
if _backend is not None: if _backend is not None:
return _backend return _backend
s3_cfg = _load_s3_config() cfg = load_config()
if s3_cfg: 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( _backend = S3Storage(
endpoint_url=s3_cfg.get("endpoint", ""), endpoint_url=s3.get("endpoint", ""),
bucket=s3_cfg["bucket"], bucket=s3["bucket"],
access_key=s3_cfg.get("access_key", ""), access_key=s3.get("access_key", ""),
secret_key=s3_cfg.get("secret_key", ""), secret_key=s3.get("secret_key", ""),
region=s3_cfg.get("region", "auto"), region=s3.get("region", "auto"),
public_url=s3_cfg.get("public_url", ""), public_url=s3.get("public_url", ""),
) )
else: else:
_backend = LocalStorage() _backend = LocalStorage()

13
config.example.json Normal file
View File

@@ -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"
}
}
}

View File

@@ -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"
}