up version
This commit is contained in:
+1
-1
@@ -73,7 +73,7 @@ async def security_and_cache_middleware(request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
if path.startswith("/api/web/uploads/") and request.method == "GET" and response.status_code == 200:
|
||||
response.headers.setdefault("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
|
||||
response.headers.setdefault("Cache-Control", "public, max-age=31536000, immutable")
|
||||
return response
|
||||
|
||||
if request.method == "GET" and response.status_code == 200 and "application/json" in content_type:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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
|
||||
+16
-3
@@ -36,6 +36,8 @@ from database.models import (
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
from api.v2.routes._data_uri_migration import migrate_json_data_uris
|
||||
|
||||
|
||||
UPLOAD_DIR = Path("static/web_uploads")
|
||||
ALLOWED_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4", ".webm"})
|
||||
@@ -427,7 +429,10 @@ async def update_web_page_theme(
|
||||
if not slug or len(slug) > 64 or not _SLUG_RE.match(slug):
|
||||
raise HTTPException(400, "Некорректный slug страницы")
|
||||
current, _ = await _resolve_variant(session, slug, variant)
|
||||
current.theme_tokens = body.tokens
|
||||
cleaned_tokens, replaced = migrate_json_data_uris(body.tokens)
|
||||
if replaced:
|
||||
logger.info("[web] theme PUT slug={} replaced {} data: URI(s)", slug, replaced)
|
||||
current.theme_tokens = cleaned_tokens
|
||||
await session.flush()
|
||||
await bump_site_revision(session)
|
||||
return WebPageThemeResponse(
|
||||
@@ -449,18 +454,26 @@ async def update_web_page(
|
||||
current, _ = await _resolve_variant(session, slug, variant)
|
||||
await session.execute(delete(WebPageVariantBlock).where(WebPageVariantBlock.variant_id == current.id))
|
||||
|
||||
total_replaced = 0
|
||||
for block in body.blocks:
|
||||
cleaned_data, replaced = migrate_json_data_uris(block.data)
|
||||
total_replaced += replaced
|
||||
session.add(
|
||||
WebPageVariantBlock(
|
||||
variant_id=current.id,
|
||||
order=block.order,
|
||||
type=block.type,
|
||||
data=block.data,
|
||||
data=cleaned_data,
|
||||
)
|
||||
)
|
||||
|
||||
if body.theme is not None:
|
||||
current.theme_tokens = body.theme.tokens
|
||||
cleaned_theme, theme_replaced = migrate_json_data_uris(body.theme.tokens)
|
||||
total_replaced += theme_replaced
|
||||
current.theme_tokens = cleaned_theme
|
||||
|
||||
if total_replaced:
|
||||
logger.info("[web] page PUT slug={} replaced {} data: URI(s)", slug, total_replaced)
|
||||
|
||||
await session.flush()
|
||||
await bump_site_revision(session)
|
||||
|
||||
@@ -8,9 +8,9 @@ class PaymentLinkCreateRequest(BaseModel):
|
||||
identity_id: str | None = Field(None, description="ID идентичности; tg_id будет взят из привязки")
|
||||
amount: int | float = Field(..., gt=0, description="Сумма оплаты")
|
||||
currency: str = Field(default="RUB", description="Валюта (например RUB)")
|
||||
provider_id: str = Field(
|
||||
...,
|
||||
description="Идентификатор кассы: ROBOKASSA, FREEKASSA, YOOKASSA, YOOMONEY, KASSAI_CARDS, KASSAI_SBP, HELEKET и др.",
|
||||
provider_id: str | None = Field(
|
||||
default=None,
|
||||
description="Идентификатор кассы: ROBOKASSA, FREEKASSA, YOOKASSA, YOOMONEY, KASSAI_CARDS, KASSAI_SBP, HELEKET и др. Если не задан — берётся первый доступный.",
|
||||
)
|
||||
success_url: str | None = Field(None, description="URL перенаправления после успешной оплаты")
|
||||
failure_url: str | None = Field(None, description="URL перенаправления после неуспешной оплаты")
|
||||
|
||||
Reference in New Issue
Block a user