mock server, file and folder management

This commit is contained in:
2026-06-12 02:18:30 +02:00
parent b67c8abdc5
commit f3db5fc5cf
28 changed files with 3049 additions and 6 deletions

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import re
from pathlib import PurePosixPath
from uuid import uuid4
_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9_.@ -]+")
class UnsafeFilePathError(ValueError):
pass
def normalize_logical_path(path: str | None, *, fallback_filename: str | None = None) -> str:
"""Return a safe tenant-relative logical path using POSIX separators.
The logical path is metadata, not a filesystem path. It never starts with a
slash and cannot contain path traversal components. It is used for browsing,
wildcard matching and attachment rules.
"""
raw = (path or "").replace("\\", "/").strip()
if not raw and fallback_filename:
raw = fallback_filename
if not raw:
raise UnsafeFilePathError("File path is empty")
if raw.startswith("/"):
raw = raw.lstrip("/")
parts: list[str] = []
for part in raw.split("/"):
clean = part.strip()
if not clean or clean == ".":
continue
if clean == "..":
raise UnsafeFilePathError("Path traversal is not allowed")
parts.append(clean)
if not parts:
raise UnsafeFilePathError("File path is empty")
return "/".join(parts)
def normalize_folder(path: str | None) -> str:
raw = (path or "").replace("\\", "/").strip().strip("/")
if not raw:
return ""
normalized = normalize_logical_path(raw)
return "" if normalized == "." else normalized
def filename_from_path(path: str) -> str:
name = PurePosixPath(path).name
if not name or name in {".", ".."}:
raise UnsafeFilePathError("Invalid filename")
return name
def join_folder_filename(folder: str | None, filename: str) -> str:
safe_name = sanitize_filename(filename)
safe_folder = normalize_folder(folder)
return f"{safe_folder}/{safe_name}" if safe_folder else safe_name
def sanitize_filename(filename: str | None) -> str:
raw = (filename or "file").replace("\\", "/").split("/")[-1].strip()
raw = raw.strip(".") or "file"
safe = _SAFE_NAME_RE.sub("_", raw)
safe = re.sub(r"\s+", " ", safe).strip()
return safe or f"file-{uuid4().hex}"
def safe_storage_component(value: str | None, fallback: str = "file") -> str:
safe = sanitize_filename(value or fallback)
return safe.replace(" ", "_")[:180]