Files
Solo_bot/api/v2/routes/_data_uri_migration.py
T
2026-04-27 15:42:38 +00:00

116 lines
3.3 KiB
Python

from __future__ import annotations
import base64
import re
import uuid
from pathlib import Path
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
UPLOAD_DIR = Path("static/web_uploads")
DATA_URI_THRESHOLD_BYTES = 2048
_DATA_URI_RE = re.compile(r"^data:([\w./+-]+);base64,(.+)$", re.DOTALL)
_MIME_TO_EXT = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"video/mp4": ".mp4",
"video/webm": ".webm",
}
def _save_data_uri_to_file(data_uri: str) -> str | None:
match = _DATA_URI_RE.match(data_uri)
if not match:
return None
mime = match.group(1).strip().lower()
payload = match.group(2)
ext = _MIME_TO_EXT.get(mime)
if not ext:
return None
try:
cleaned = "".join(payload.split())
decoded = base64.b64decode(cleaned, validate=False)
except Exception:
return None
if not decoded:
return None
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
name = f"{uuid.uuid4().hex}{ext}"
(UPLOAD_DIR / name).write_bytes(decoded)
return f"/api/web/uploads/{name}"
def migrate_json_data_uris(value: Any) -> tuple[Any, int]:
replaced = 0
def walk(node: Any) -> Any:
nonlocal replaced
if isinstance(node, str):
if not node.startswith("data:"):
return node
if len(node) < DATA_URI_THRESHOLD_BYTES:
return node
url = _save_data_uri_to_file(node)
if url is None:
return node
replaced += 1
return url
if isinstance(node, list):
return [walk(item) for item in node]
if isinstance(node, dict):
return {key: walk(item) for key, item in node.items()}
return node
return walk(value), replaced
async def run_startup_data_uri_migration(session: AsyncSession) -> tuple[int, int]:
from database.models import (
WebBlock,
WebPageVariant,
WebPageVariantBlock,
WebTheme,
)
rows_updated = 0
uris_replaced = 0
for theme in (await session.execute(select(WebTheme))).scalars().all():
cleaned, replaced = migrate_json_data_uris(theme.tokens or {})
if replaced:
theme.tokens = cleaned
rows_updated += 1
uris_replaced += replaced
for variant in (await session.execute(select(WebPageVariant))).scalars().all():
cleaned, replaced = migrate_json_data_uris(variant.theme_tokens or {})
if replaced:
variant.theme_tokens = cleaned
rows_updated += 1
uris_replaced += replaced
for block in (await session.execute(select(WebBlock))).scalars().all():
cleaned, replaced = migrate_json_data_uris(block.data or {})
if replaced:
block.data = cleaned
rows_updated += 1
uris_replaced += replaced
for block in (await session.execute(select(WebPageVariantBlock))).scalars().all():
cleaned, replaced = migrate_json_data_uris(block.data or {})
if replaced:
block.data = cleaned
rows_updated += 1
uris_replaced += replaced
return rows_updated, uris_replaced