Dish_Recognition/service/media_server.py

144 lines
4.4 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
"""本机媒体静态服务:把 YOLO 裁剪图等通过 HTTP 暴露,供管理系统访问。
访问形式:
http://<本机局域网IP>:9100/media/<base>/<request_id>/crop_xxx.jpg
"""
import os
import socket
import threading
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote
from dish_server.media_store import MEDIA_ROOT, MediaStore
_DEFAULT_PORT = int(os.environ.get('DISH_MEDIA_SERVER_PORT', '9100'))
_ENABLED = os.environ.get('DISH_MEDIA_SERVER_ENABLED', '1') == '1'
_lock = threading.Lock()
_httpd = None
_thread = None
_public_base = None
_port = _DEFAULT_PORT
_media_root = os.path.abspath(MEDIA_ROOT)
def _lan_ip():
env = (os.environ.get('DISH_MEDIA_HOST') or '').strip()
if env:
return env
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('8.8.8.8', 80))
return s.getsockname()[0]
finally:
s.close()
except Exception:
return '127.0.0.1'
class _MediaHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
pass
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Cache-Control', 'public, max-age=60')
super().end_headers()
def do_GET(self):
raw = unquote(self.path.split('?', 1)[0])
if raw == '/health':
body = b'ok'
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if not raw.startswith('/media/') and raw != '/media':
self.send_error(404, 'Not Found')
return
super().do_GET()
def translate_path(self, path):
"""把 /media/xxx 映射到 MEDIA_ROOT/xxx。"""
path = unquote(path.split('?', 1)[0])
if path.startswith('/media/'):
rel = path[len('/media/'):]
elif path == '/media':
rel = ''
else:
rel = ''
rel = rel.replace('\\', '/').lstrip('/')
# 防目录穿越
parts = [p for p in rel.split('/') if p and p not in ('.', '..')]
dest = os.path.normpath(os.path.join(_media_root, *parts))
root = os.path.abspath(_media_root)
if dest != root and not dest.startswith(root + os.sep):
return os.path.join(root, '__invalid__')
return dest
def get_public_base():
"""本机兜底媒体前缀(局域网),不是管理系统用的 HTTPS tpweb。"""
global _public_base
if _public_base:
return _public_base
env = (os.environ.get('DISH_LOCAL_MEDIA_PUBLIC_BASE') or '').strip().rstrip('/')
if env:
return env
return f'http://{_lan_ip()}:{_port}/media'
def ensure_media_server(port=None, enabled=None):
"""启动本机 /media 静态服务(幂等)。"""
global _httpd, _thread, _public_base, _port, _media_root
if enabled is None:
enabled = _ENABLED
if not enabled:
return get_public_base()
with _lock:
if _httpd is not None:
return _public_base
_port = int(port or _DEFAULT_PORT)
_media_root = os.path.abspath(MEDIA_ROOT)
os.makedirs(_media_root, exist_ok=True)
try:
_httpd = ThreadingHTTPServer(('0.0.0.0', _port), _MediaHandler)
except OSError as exc:
print(f'[WARN] media server bind :{_port} failed: {exc}')
return get_public_base()
lan = _lan_ip()
# 本机兜底地址只用局域网;不要覆盖 DISH_MEDIA_PUBLIC_BASE(可能是 https://cpsb.../tpweb)
_public_base = f'http://{lan}:{_port}/media'
os.environ['DISH_LOCAL_MEDIA_PUBLIC_BASE'] = _public_base
_thread = threading.Thread(target=_httpd.serve_forever, name='MediaHttpServer', daemon=True)
_thread.start()
print(f'[OK] local media fallback: {_public_base} (root={_media_root})')
return _public_base
def stop_media_server():
global _httpd, _thread
with _lock:
if _httpd is None:
return
try:
_httpd.shutdown()
except Exception:
pass
_httpd = None
_thread = None
def media_store_with_public():
base = ensure_media_server()
return MediaStore(public_base=base)