backend/config_utils.py
Rama master · réplica de lectura del repositorio original.
"""Path mapping utility — extracted from config.py for size management.
config.py re-exports map_path for backwards compatibility.
"""
import os
import re
# "D:\..." or UNC "\\server\share\..." — what Sonarr/Radarr on Windows report.
_WINDOWS_PATH_RE = re.compile(r"^(?:[A-Za-z]:[\\/]|\\\\)")
def is_under_folder(path: str, folder: str) -> bool:
"""Whether ``path`` is ``folder`` or inside it.
A plain prefix test lets "/media/movies" match "/media/movies-4k/...": with
several pairs that maps a library onto another library's root.
"""
if not folder or not path.startswith(folder):
return False
rest = path[len(folder) :]
return not rest or folder.endswith(("/", "\\")) or rest.startswith(("/", "\\"))
def map_path(path: str) -> str:
"""Map a remote file path to a local path using configured path mappings.
Path mapping is configured via the SUBLARR_PATH_MAPPING setting:
Format: "remote_prefix=local_prefix" (multiple pairs separated by semicolons)
Example: "/data/media=/mnt/media;/anime=/share/anime"
A pair matches whole folders only ("/data/movies" never rewrites
"/data/movies-4k/..."), and the pair with the longest remote prefix wins,
so a specific pair beats a general one in any order.
On Windows, forward slashes in the mapped path are converted to backslashes.
A Windows remote path ("D:\\TV\\...", "\\\\nas\\tv\\...") matches its pair
with either separator, and elsewhere its backslashes become "/".
SECURITY NOTE: This function performs prefix replacement only.
Callers that serve or delete files MUST validate the mapped result with
``security_utils.is_safe_path(mapped, media_path)`` before using it,
to guard against path traversal after mapping.
"""
from config import get_settings # local import — prevents circular dep
s = get_settings()
mapping = s.path_mapping
if not mapping:
return path
windows = bool(_WINDOWS_PATH_RE.match(path))
probe = path.replace("\\", "/") if windows else path
best = None
for pair in mapping.split(";"):
pair = pair.strip()
if "=" not in pair:
continue
remote_prefix, local_prefix = pair.split("=", 1)
remote_prefix = remote_prefix.strip()
local_prefix = local_prefix.strip()
if not remote_prefix or not local_prefix:
continue
if windows:
remote_prefix = remote_prefix.replace("\\", "/")
# Windows paths ignore case: Sonarr may report d:\tv for D:\TV.
# Compared lowered only where lowering keeps the length, so the
# slice below still cuts the original at the same place.
if len(probe.lower()) == len(probe) and len(remote_prefix.lower()) == len(
remote_prefix
):
if not is_under_folder(probe.lower(), remote_prefix.lower()):
continue
elif not is_under_folder(probe, remote_prefix):
continue
elif not is_under_folder(probe, remote_prefix):
continue
if best is None or len(remote_prefix) > len(best[0]):
best = (remote_prefix, local_prefix)
if best is None:
return path
remote_prefix, local_prefix = best
# Join at the folder boundary whichever side spells the trailing separator.
mapped = local_prefix.rstrip("/\\") + probe[len(remote_prefix.rstrip("/\\")) :]
if os.name == "nt":
mapped = mapped.replace("/", "\\")
return mapped