import os import shutil from pathlib import Path from fastapi import APIRouter, UploadFile, File, HTTPException from fastapi.responses import FileResponse from config import VOLUME_ROOT router = APIRouter() BASE = Path(VOLUME_ROOT) BLOCKED_DIRS = {"@appstore", "@docker", "@eaDir", "@S2S", "@sharesnap", "@tmp", "docker"} def _safe_path(path: str) -> Path: target = (BASE / path).resolve() if not str(target).startswith(str(BASE.resolve())): raise ValueError("forbidden") # Block access to sensitive directories rel = target.relative_to(BASE.resolve()) if rel.parts and rel.parts[0] in BLOCKED_DIRS: raise ValueError("forbidden") return target @router.get("/browse") def browse(path: str = ""): try: target = _safe_path(path) except ValueError: raise HTTPException(status_code=403, detail="Access denied") entries = [] for item in sorted(target.iterdir()): try: st = item.stat() entries.append({"name": item.name, "is_dir": item.is_dir(), "size": st.st_size if item.is_file() else 0}) except PermissionError: continue return {"path": str(target.relative_to(BASE)), "entries": entries} @router.get("/download") def download(path: str): try: return FileResponse(_safe_path(path)) except ValueError: raise HTTPException(status_code=403, detail="Access denied") @router.post("/upload") async def upload(path: str, file: UploadFile = File(...)): try: target = _safe_path(path) / file.filename except ValueError: raise HTTPException(status_code=403, detail="Access denied") with open(target, "wb") as f: shutil.copyfileobj(file.file, f) return {"ok": True} @router.delete("/delete") def delete(path: str): try: target = _safe_path(path) except ValueError: raise HTTPException(status_code=403, detail="Access denied") if target.is_dir(): shutil.rmtree(target) else: target.unlink() return {"ok": True}