Dish_Recognition/dish_server/media_store.py

159 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding=utf-8
"""本地对象存储(静态文件目录)。
云机默认落盘:/data/tpweb
对外访问:https://cpsb.ruimeida.cc/tpweb/...
"""
from __future__ import annotations
import os
import re
import time
from pathlib import Path
_FOLDER = os.path.dirname(os.path.abspath(__file__))
# 云微服务:上传走 :9100;落盘 /data/tpweb;对外 https://cpsb.ruimeida.cc/tpweb
CLOUD_MEDIA_HOST = "116.147.40.47"
CLOUD_MEDIA_PORT = 9100
# 部署目录参考:/home/cpsb/project/dish_server
# 磁盘目录(与 Nginx /tpweb 根一致);本机 Windows 开发回落到项目内目录
_DEFAULT_MEDIA_ROOT = (
os.environ.get("DISH_MEDIA_ROOT")
or ("/data/tpweb" if os.name != "nt" else os.path.join(_FOLDER, "data", "tpweb"))
)
# 浏览器访问前缀(HTTPS,避免管理系统 Mixed Content)
CLOUD_MEDIA_PUBLIC_BASE = "https://cpsb.ruimeida.cc/tpweb"
CLOUD_MEDIA_UPLOAD_URL = f"http://{CLOUD_MEDIA_HOST}:{CLOUD_MEDIA_PORT}/api/dish/media/upload"
MEDIA_ROOT = _DEFAULT_MEDIA_ROOT
MEDIA_PUBLIC_BASE = os.environ.get("DISH_MEDIA_PUBLIC_BASE", CLOUD_MEDIA_PUBLIC_BASE).rstrip("/")
_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
# template:学菜模板图,落盘 template/{菜名}/xxx.jpg
ALLOWED_TYPES = ("source", "labeled", "crop", "template")
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 _safe_dish_dirname(value, fallback="unknown"):
"""菜名目录:保留中文,去掉路径非法字符。"""
text = str(value or "").strip()
for ch in '<>:"/\\|?*\x00':
text = text.replace(ch, "_")
text = text.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,
dish_name=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"
if media_type == "template":
# 学菜模板:/data/tpweb/template/{菜名}/文件名
dish = _safe_dish_dirname(dish_name or request_id or "unknown")
orig = Path(filename or f"{dish}{ext}").name
for ch in '<>:"/\\|?*\x00':
orig = orig.replace(ch, "_")
if not Path(orig).suffix:
orig = f"{orig}{ext}"
rel = f"template/{dish}/{orig}"
else:
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,
dish_name=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,
dish_name=dish_name,
)
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