"""宿主机路径 ↔ 容器路径映射 用于让用户在 Web 界面和 config.yaml 里填宿主机真实路径(如群晖的 /volume2/...), 程序在真正写盘/调用云盘 SDK 之前再转换成容器内挂载点路径(如 /app/downloads)。 保证文件落到 bind mount 卷而不是容器 overlay 文件系统。 匹配规则:最长前缀匹配,以路径分隔符为边界。无匹配则原样返回,不阻断流程。 """ import logging import os logger = logging.getLogger("media_downloader") # (host_prefix, container_prefix) 列表,加载时按 host_prefix 长度倒序 _mappings: list = [] def _normalize(p: str) -> str: """去掉尾部 /,统一用 / 做分隔符""" if not p: return "" return p.replace("\\", "/").rstrip("/") def load_mappings(items: list): """从 config 里的 path_mapping 列表加载映射 items 每项形如 {'host': '/volume2/save/tgdowload', 'container': '/app/downloads'} """ global _mappings pairs = [] for it in items or []: h = _normalize(str(it.get("host", "") or "")) c = _normalize(str(it.get("container", "") or "")) if not h or not c: continue pairs.append((h, c)) # 按 host 前缀长度倒序,保证最长前缀优先命中 pairs.sort(key=lambda x: len(x[0]), reverse=True) _mappings = pairs if _mappings: logger.info("path_mapper loaded %d mapping(s): %s", len(_mappings), _mappings) def _match_prefix(p: str, prefix: str) -> bool: """前缀匹配,必须落在路径分隔符边界上,避免 /a/b 匹配到 /a/bc""" return p == prefix or p.startswith(prefix + "/") def to_container(p: str) -> str: """宿主机路径 → 容器路径。无匹配原样返回。""" if not p: return p norm = _normalize(p) for host, container in _mappings: if _match_prefix(norm, host): return container + norm[len(host):] return p def to_host(p: str) -> str: """容器路径 → 宿主机路径。无匹配原样返回。""" if not p: return p norm = _normalize(p) for host, container in _mappings: if _match_prefix(norm, container): return host + norm[len(container):] return p