"""Game-agnostic file operations.""" from __future__ import annotations import re from pathlib import Path def get_server_dir(server_id: int) -> Path: """Return the absolute directory path for a server's data.""" from config import settings base = Path(settings.servers_dir).resolve() return base / str(server_id) def ensure_server_dirs(server_id: int, layout: list[str] | None = None) -> None: """ Create servers/{id}/ and any subdirectories from adapter layout. layout example: ["server", "battleye", "mpmissions"] """ server_dir = get_server_dir(server_id) server_dir.mkdir(parents=True, exist_ok=True) if layout: for subdir in layout: (server_dir / subdir).mkdir(parents=True, exist_ok=True) def safe_delete_file(path: Path) -> bool: """Delete a file if it exists. Returns True if deleted.""" try: path.unlink(missing_ok=True) return True except OSError: return False def sanitize_filename(filename: str) -> str: """ Sanitize a filename for safe disk storage. Rules: - Strip path separators (/ \\ and ..) - Allow only alphanumeric, dots, hyphens, underscores, @ signs - Collapse consecutive dots (prevent ../ tricks) - Truncate to 255 characters - Raise ValueError if the result is empty """ # Take only the basename — strip any directory components filename = filename.replace("\\", "/").split("/")[-1] # Remove null bytes and control characters filename = re.sub(r"[\x00-\x1f\x7f]", "", filename) # Allow only safe characters: alphanum, dot, hyphen, underscore, @ filename = re.sub(r"[^\w.\-@]", "_", filename) # Collapse consecutive dots to prevent tricks like ".../.." filename = re.sub(r"\.{2,}", ".", filename) # Truncate filename = filename[:255] if not filename or filename in (".", ".."): raise ValueError(f"Filename '{filename}' is not safe for storage") return filename