feat(db-hardening): Add database persistence, backups, and MinIO assets storage

Database Hardening:
- Add docker-compose.db.yml with persistent PostgreSQL volume
- Add automatic DB backups every 12h (7 days, 4 weeks, 6 months retention)
- Add MinIO S3-compatible storage for assets

Assets Migration:
- Add MinIO client (lib/assets_client.py) for upload/delete
- Update upload endpoint to use MinIO (with local fallback)
- Add migration 043_asset_urls_to_text.sql for full HTTPS URLs
- Simplify normalizeAssetUrl for S3 URLs

Recovery:
- Add seed_full_city_reset.py for emergency city recovery
- Add DB_RESTORE.md with backup restore instructions
- Add SEED_RECOVERY.md with recovery procedures
- Add INFRA_ASSETS_MINIO.md with MinIO setup guide

Task: TASK_PHASE_DATABASE_HARDENING_AND_ASSETS_MIGRATION_v1
This commit is contained in:
Apple
2025-12-02 01:56:39 -08:00
parent 0e743e5629
commit 8e8f95e9ef
11 changed files with 1541 additions and 33 deletions

View File

@@ -14,6 +14,14 @@ import uuid
from PIL import Image
import shutil
# MinIO assets client
try:
from lib.assets_client import upload_asset as minio_upload_asset
MINIO_AVAILABLE = True
except ImportError:
MINIO_AVAILABLE = False
logger.warning("MinIO client not available, falling back to local storage")
from models_city import (
CityRoomRead,
CityRoomCreate,
@@ -339,24 +347,69 @@ async def upload_asset(
processed_bytes, thumb_bytes = process_image(content, max_size=max_size, force_square=force_square)
# Save to disk
# Map type to prefix
type_to_prefix = {
'microdao_logo': 'microdao/logo',
'microdao_banner': 'microdao/banner',
'room_logo': 'rooms/logo',
'room_banner': 'rooms/banner',
'agent_avatar': 'agents/avatar',
}
prefix = type_to_prefix.get(type, 'uploads')
# Upload to MinIO if available, otherwise fallback to local storage
if MINIO_AVAILABLE:
try:
# Upload processed image
processed_file = io.BytesIO(processed_bytes)
processed_url = minio_upload_asset(
processed_file,
content_type="image/png",
prefix=prefix,
filename=file.filename
)
# Upload thumbnail if exists
thumb_url = None
if thumb_bytes:
thumb_file = io.BytesIO(thumb_bytes)
thumb_url = minio_upload_asset(
thumb_file,
content_type="image/png",
prefix=f"{prefix}/thumb",
filename=file.filename
)
return {
"original_url": processed_url,
"processed_url": processed_url,
"thumb_url": thumb_url or processed_url
}
except Exception as e:
logger.warning(f"MinIO upload failed, falling back to local: {e}")
# Fall through to local storage
# Fallback: Save to local disk
filename = f"{uuid.uuid4()}.png"
filepath = f"static/uploads/{filename}"
thumb_filepath = f"static/uploads/thumb_{filename}"
os.makedirs("static/uploads", exist_ok=True)
with open(filepath, "wb") as f:
f.write(processed_bytes)
with open(thumb_filepath, "wb") as f:
f.write(thumb_bytes)
if thumb_bytes:
with open(thumb_filepath, "wb") as f:
f.write(thumb_bytes)
# Construct URLs
base_url = "/static/uploads"
base_url = "/api/static/uploads"
return {
"original_url": f"{base_url}/{filename}",
"processed_url": f"{base_url}/{filename}",
"thumb_url": f"{base_url}/thumb_{filename}"
"thumb_url": f"{base_url}/thumb_{filename}" if thumb_bytes else f"{base_url}/{filename}"
}
except Exception as e: