# coding=utf-8 """本地对象存储(静态文件目录)。 图片落到本目录,通过 GET /media/... 对外访问。 生产可把 data/media 挂到 Nginx / OSS,接口形态不变。 """ from __future__ import annotations import os import re import time from pathlib import Path _FOLDER = os.path.dirname(os.path.abspath(__file__)) MEDIA_ROOT = os.environ.get("DISH_MEDIA_ROOT") or os.path.join(_FOLDER, "data", "media") MEDIA_PUBLIC_BASE = os.environ.get("DISH_MEDIA_PUBLIC_BASE", "http://127.0.0.1:9000/media").rstrip("/") _SAFE = re.compile(r"[^A-Za-z0-9._-]+") ALLOWED_TYPES = ("source", "labeled", "crop") ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} def _safe_token(value, fallback="x"): text = _SAFE.sub("_", str(value or "").strip())[:80] return text or fallback def public_url(rel_path): rel = str(rel_path or "").replace("\\", "/").lstrip("/") return f"{MEDIA_PUBLIC_BASE}/{rel}" def local_path(rel_path): rel = str(rel_path or "").replace("\\", "/").lstrip("/") return os.path.normpath(os.path.join(MEDIA_ROOT, *rel.split("/"))) def is_http_url(value): text = (value or "").strip().lower() return text.startswith("http://") or text.startswith("https://") class MediaStore: """按 基地 / 请求号 归档图片。""" def __init__(self, root=None, public_base=None): self.root = os.path.abspath(root or MEDIA_ROOT) self.public_base = (public_base or MEDIA_PUBLIC_BASE).rstrip("/") os.makedirs(self.root, exist_ok=True) def save_bytes(self, data, filename, media_type="source", base_id="BASE", request_id=None): if not data: raise ValueError("empty file") media_type = (media_type or "source").strip().lower() if media_type not in ALLOWED_TYPES: media_type = "source" ext = Path(filename or "").suffix.lower() or ".jpg" if ext not in ALLOWED_EXT: ext = ".jpg" req = _safe_token(request_id or f"REQ_{int(time.time() * 1000)}") base = _safe_token(base_id or "BASE") stem = _safe_token(Path(filename or media_type).stem, media_type) rel = f"{base}/{req}/{media_type}_{stem}{ext}" dest = local_path_under(self.root, rel) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "wb") as fh: fh.write(data) rel_url = rel.replace("\\", "/") return { "rel_path": rel_url, "path": dest, "url": f"{self.public_base}/{rel_url}", "type": media_type, } def save_file(self, src_path, media_type="source", base_id="BASE", request_id=None): if not src_path or not os.path.isfile(src_path): return None with open(src_path, "rb") as fh: data = fh.read() return self.save_bytes( data, filename=os.path.basename(src_path), media_type=media_type, base_id=base_id, request_id=request_id, ) def local_path_under(root, rel_path): dest = os.path.normpath(os.path.join(root, *str(rel_path).replace("\\", "/").split("/"))) root_abs = os.path.abspath(root) dest_abs = os.path.abspath(dest) if dest_abs != root_abs and not dest_abs.startswith(root_abs + os.sep): raise ValueError("invalid media path") return dest_abs def copy_if_local(src_path, media_type, base_id, request_id, store=None): if not src_path: return src_path if is_http_url(src_path): return src_path store = store or MediaStore() saved = store.save_file(src_path, media_type=media_type, base_id=base_id, request_id=request_id) return saved["url"] if saved else src_path