147 lines
4.5 KiB
Python
147 lines
4.5 KiB
Python
# 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 central_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():
|
|
"""返回当前媒体前缀,如 http://192.168.1.8:9100/media"""
|
|
global _public_base
|
|
if _public_base:
|
|
return _public_base
|
|
env = (os.environ.get('DISH_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()
|
|
env_base = (os.environ.get('DISH_MEDIA_PUBLIC_BASE') or '').strip().rstrip('/')
|
|
if env_base and '127.0.0.1' not in env_base and 'localhost' not in env_base.lower():
|
|
_public_base = env_base
|
|
else:
|
|
_public_base = f'http://{lan}:{_port}/media'
|
|
os.environ['DISH_MEDIA_PUBLIC_BASE'] = _public_base
|
|
|
|
_thread = threading.Thread(target=_httpd.serve_forever, name='MediaHttpServer', daemon=True)
|
|
_thread.start()
|
|
print(f'[OK] media server: {_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)
|