config.yaml 新增 path_mapping 段声明宿主机前缀与容器挂载点前缀的对应关系。 用户在 Web 界面和 config 里填宿主机真实路径(如群晖 /volume2/...), 程序在真正落盘、调用云盘 SDK 前透明转成容器内路径,保证文件写到 bind mount 而不是容器 overlay 文件系统;前端显示/返回的路径再反向转回宿主机形式。 无映射配置时所有 to_container/to_host 均原样返回,向后兼容老配置。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -15,6 +15,7 @@ from ruamel import yaml
|
|||||||
from module.cloud_drive import CloudDrive, CloudDriveConfig
|
from module.cloud_drive import CloudDrive, CloudDriveConfig
|
||||||
from module.filter import Filter
|
from module.filter import Filter
|
||||||
from module.language import Language, set_language
|
from module.language import Language, set_language
|
||||||
|
from module import path_mapper
|
||||||
from utils.format import replace_date_time, validate_title
|
from utils.format import replace_date_time, validate_title
|
||||||
from utils.meta_data import MetaData
|
from utils.meta_data import MetaData
|
||||||
|
|
||||||
@@ -433,6 +434,9 @@ class Application:
|
|||||||
"""
|
"""
|
||||||
# pylint: disable = R0912
|
# pylint: disable = R0912
|
||||||
# TODO: judge the storage if enough,and provide more path
|
# TODO: judge the storage if enough,and provide more path
|
||||||
|
# 先加载 path_mapping,后续 save_path / cloud_drive 路径都按此映射表解析
|
||||||
|
path_mapper.load_mappings(_config.get("path_mapping", []))
|
||||||
|
|
||||||
if _config.get("save_path") is not None:
|
if _config.get("save_path") is not None:
|
||||||
self.save_path = _config["save_path"]
|
self.save_path = _config["save_path"]
|
||||||
|
|
||||||
@@ -676,10 +680,12 @@ class Application:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
ret: bool = False
|
ret: bool = False
|
||||||
|
# 云盘上传也走容器路径,避免 SDK 在容器内找不到宿主机路径
|
||||||
|
save_path_container = path_mapper.to_container(self.save_path)
|
||||||
if self.cloud_drive_config.upload_adapter == "rclone":
|
if self.cloud_drive_config.upload_adapter == "rclone":
|
||||||
ret = await CloudDrive.rclone_upload_file(
|
ret = await CloudDrive.rclone_upload_file(
|
||||||
self.cloud_drive_config,
|
self.cloud_drive_config,
|
||||||
self.save_path,
|
save_path_container,
|
||||||
local_file_path,
|
local_file_path,
|
||||||
progress_callback,
|
progress_callback,
|
||||||
progress_args,
|
progress_args,
|
||||||
@@ -688,7 +694,7 @@ class Application:
|
|||||||
ret = await self.loop.run_in_executor(
|
ret = await self.loop.run_in_executor(
|
||||||
self.executor,
|
self.executor,
|
||||||
CloudDrive.aligo_upload_file(
|
CloudDrive.aligo_upload_file(
|
||||||
self.cloud_drive_config, self.save_path, local_file_path
|
self.cloud_drive_config, save_path_container, local_file_path
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -724,7 +730,8 @@ class Application:
|
|||||||
res = os.path.join(res, media_datetime)
|
res = os.path.join(res, media_datetime)
|
||||||
elif prefix == "media_type":
|
elif prefix == "media_type":
|
||||||
res = os.path.join(res, media_type)
|
res = os.path.join(res, media_type)
|
||||||
return res
|
# 真正落盘前把宿主机路径转成容器内挂载点路径,保证写到 bind mount 卷
|
||||||
|
return path_mapper.to_container(res)
|
||||||
|
|
||||||
def get_file_name(
|
def get_file_name(
|
||||||
self, message_id: int, file_name: Optional[str], caption: Optional[str]
|
self, message_id: int, file_name: Optional[str], caption: Optional[str]
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""宿主机路径 ↔ 容器路径映射
|
||||||
|
|
||||||
|
用于让用户在 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
|
||||||
+3
-2
@@ -16,6 +16,7 @@ from ruamel.yaml import YAML
|
|||||||
from werkzeug.serving import make_server
|
from werkzeug.serving import make_server
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
|
from module import path_mapper
|
||||||
from module.app import Application, ChatDownloadConfig, TaskNode
|
from module.app import Application, ChatDownloadConfig, TaskNode
|
||||||
from module.download_stat import (
|
from module.download_stat import (
|
||||||
DownloadState,
|
DownloadState,
|
||||||
@@ -337,7 +338,7 @@ def get_download_list():
|
|||||||
"total_size": format_byte(r.get("file_size") or 0),
|
"total_size": format_byte(r.get("file_size") or 0),
|
||||||
"download_progress": "100",
|
"download_progress": "100",
|
||||||
"download_speed": r.get("download_time", ""),
|
"download_speed": r.get("download_time", ""),
|
||||||
"save_path": (r.get("file_path") or "").replace("\\", "/"),
|
"save_path": path_mapper.to_host((r.get("file_path") or "").replace("\\", "/")),
|
||||||
"status": r.get("status", "success"),
|
"status": r.get("status", "success"),
|
||||||
})
|
})
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
@@ -363,7 +364,7 @@ def get_download_list():
|
|||||||
"total_size": format_byte(value["total_size"]),
|
"total_size": format_byte(value["total_size"]),
|
||||||
"download_progress": str(round(value["down_byte"] / value["total_size"] * 100, 1)),
|
"download_progress": str(round(value["down_byte"] / value["total_size"] * 100, 1)),
|
||||||
"download_speed": download_speed,
|
"download_speed": download_speed,
|
||||||
"save_path": value["file_name"].replace("\\", "/"),
|
"save_path": path_mapper.to_host(value["file_name"].replace("\\", "/")),
|
||||||
"paused": is_message_paused(str(chat_id), idx),
|
"paused": is_message_paused(str(chat_id), idx),
|
||||||
})
|
})
|
||||||
return json.dumps(items, ensure_ascii=False)
|
return json.dumps(items, ensure_ascii=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user