返回 JoyAI-Echo
media_decode.py
1 """Shared helpers for decoding ``data:...;base64,...`` URLs to disk.
2
3 Historically lived in ``nanobot.api.server``; now shared by the WebSocket
4 channel so the ``api`` + ``websocket`` ingress paths apply the same parsing,
5 size guard, and filesystem layout.
6 """
7
8 from __future__ import annotations
9
10 import base64
11 import mimetypes
12 import re
13 import uuid
14 from pathlib import Path
15
16 from nanobot.utils.helpers import safe_filename
17
18 DEFAULT_MAX_BYTES = 10 * 1024 * 1024
19 MAX_FILE_SIZE = DEFAULT_MAX_BYTES
20
21 _DATA_URL_RE = re.compile(r"^data:([^;]+);base64,(.+)$", re.DOTALL)
22
23
24 class FileSizeExceeded(Exception):
25 """Raised when a decoded payload exceeds the caller's size limit."""
26
27
28 def save_base64_data_url(
29 data_url: str,
30 media_dir: Path,
31 *,
32 max_bytes: int | None = None,
33 ) -> str | None:
34 """Decode a ``data:<mime>;base64,<payload>`` URL and persist it.
35
36 Returns the absolute path on success, ``None`` when the URL shape or the
37 base64 payload itself is malformed. Raises :class:`FileSizeExceeded`
38 when the decoded payload is larger than ``max_bytes`` (default 10 MB).
39 """
40 m = _DATA_URL_RE.match(data_url)
41 if not m:
42 return None
43 mime_type, b64_payload = m.group(1), m.group(2)
44 try:
45 raw = base64.b64decode(b64_payload)
46 except Exception:
47 return None
48 limit = DEFAULT_MAX_BYTES if max_bytes is None else max_bytes
49 if len(raw) > limit:
50 raise FileSizeExceeded(f"File exceeds {limit // (1024 * 1024)}MB limit")
51 ext = mimetypes.guess_extension(mime_type) or ".bin"
52 filename = f"{uuid.uuid4().hex[:12]}{ext}"
53 dest = media_dir / safe_filename(filename)
54 dest.write_bytes(raw)
55 return str(dest)
56
56 lines PYTHON