70 lines
2.1 KiB
Python
70 lines
2.1 KiB
Python
"""Small filesystem helpers shared by core modules."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any
|
|
|
|
|
|
def atomic_write_bytes(path: Path, data: bytes, mode: int | None = None) -> None:
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
try:
|
|
with os.fdopen(fd, "wb") as fh:
|
|
fh.write(data)
|
|
fh.flush()
|
|
os.fsync(fh.fileno())
|
|
if mode is not None:
|
|
os.chmod(tmp, mode)
|
|
os.replace(tmp, path)
|
|
except BaseException:
|
|
try:
|
|
os.unlink(tmp)
|
|
except OSError:
|
|
pass
|
|
raise
|
|
|
|
|
|
def atomic_write_text(path: Path, text: str, mode: int | None = None) -> None:
|
|
atomic_write_bytes(path, text.encode("utf-8"), mode)
|
|
|
|
|
|
def atomic_write_json(path: Path, obj: Any, mode: int | None = None) -> None:
|
|
atomic_write_text(path, json.dumps(obj, indent=2, sort_keys=True) + "\n", mode)
|
|
|
|
|
|
def read_json(path: Path, default: Any) -> Any:
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return default
|
|
|
|
|
|
def slugify(text: str, fallback: str = "mod") -> str:
|
|
slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
return slug[:48] or fallback
|
|
|
|
|
|
def safe_relpath(rel: str) -> str | None:
|
|
"""Normalise an archive/dest path. Returns None if it is absolute or escapes."""
|
|
rel = rel.replace("\\", "/")
|
|
if not rel or "\0" in rel or rel.startswith("/") or re.match(r"^[A-Za-z]:", rel):
|
|
return None
|
|
parts = [p for p in PurePosixPath(rel).parts if p not in ("", ".")]
|
|
if not parts or any(p == ".." for p in parts):
|
|
return None
|
|
return "/".join(parts)
|
|
|
|
|
|
def is_within(child: Path, parent: Path) -> bool:
|
|
try:
|
|
Path(os.path.realpath(child)).relative_to(os.path.realpath(parent))
|
|
return True
|
|
except ValueError:
|
|
return False
|