refactor(config): consolidate into single config.json, storage backend selectable
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
"""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": {
|
||||||
"endpoint": "https://s3.amazonaws.com",
|
"backend": "local",
|
||||||
"bucket": "my-mailova-bucket",
|
"s3": {
|
||||||
"region": "auto",
|
"endpoint": "https://s3.amazonaws.com",
|
||||||
"access_key": "AKIA...",
|
"bucket": "my-mailova-bucket",
|
||||||
"secret_key": "...",
|
"region": "auto",
|
||||||
"public_url": "https://cdn.example.com/mailova"
|
"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
|
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,40 +132,46 @@ 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")
|
||||||
_backend = S3Storage(
|
|
||||||
endpoint_url=s3_cfg.get("endpoint", ""),
|
if backend == "s3":
|
||||||
bucket=s3_cfg["bucket"],
|
s3 = cfg.get("storage", {}).get("s3", {})
|
||||||
access_key=s3_cfg.get("access_key", ""),
|
if not s3.get("bucket"):
|
||||||
secret_key=s3_cfg.get("secret_key", ""),
|
# fallback to local if S3 is misconfigured
|
||||||
region=s3_cfg.get("region", "auto"),
|
_backend = LocalStorage()
|
||||||
public_url=s3_cfg.get("public_url", ""),
|
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:
|
else:
|
||||||
_backend = LocalStorage()
|
_backend = LocalStorage()
|
||||||
return _backend
|
return _backend
|
||||||
|
|||||||
13
config.example.json
Normal file
13
config.example.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user