diff --git a/YakPanel-server/backend/app/api/files.py b/YakPanel-server/backend/app/api/files.py index ee84af65..e8bdeddb 100644 --- a/YakPanel-server/backend/app/api/files.py +++ b/YakPanel-server/backend/app/api/files.py @@ -13,28 +13,46 @@ router = APIRouter(prefix="/files", tags=["files"]) def _resolve_path(path: str) -> str: - """Resolve and validate path within allowed roots (cross-platform)""" - cfg = get_runtime_config() - www_root = os.path.abspath(cfg["www_root"]) - setup_path = os.path.abspath(cfg["setup_path"]) - allowed = [www_root, setup_path] - if os.name != "nt": - allowed.append(os.path.abspath("/www")) + """ + Resolve API path to an OS path. + + On Linux/macOS: path is an absolute POSIX path from filesystem root (/) so admins + can browse the whole server (same expectation as BT/aaPanel-style panels). + + On Windows (dev): paths stay sandboxed under www_root / setup_path. + """ if ".." in path: raise HTTPException(status_code=400, detail="Path traversal not allowed") - norm_path = path.strip().replace("\\", "/").strip("/") - # Root or www_root-style path - if not norm_path or norm_path in ("www", "www/wwwroot", "wwwroot"): - full = www_root - elif norm_path.startswith("www/wwwroot/"): - full = os.path.abspath(os.path.join(www_root, norm_path[12:])) - else: - full = os.path.abspath(os.path.join(www_root, norm_path)) - if not any( - full == r or (full + os.sep).startswith(r + os.sep) - for r in allowed - ): - raise HTTPException(status_code=403, detail="Path not allowed") + + raw = path.strip().replace("\\", "/") + + if os.name == "nt": + cfg = get_runtime_config() + www_root = os.path.abspath(cfg["www_root"]) + setup_path = os.path.abspath(cfg["setup_path"]) + allowed = [www_root, setup_path] + norm_path = raw.strip("/") + if not norm_path or norm_path in ("www", "www/wwwroot", "wwwroot"): + full = www_root + elif norm_path.startswith("www/wwwroot/"): + full = os.path.abspath(os.path.join(www_root, norm_path[12:])) + else: + full = os.path.abspath(os.path.join(www_root, norm_path)) + if not any( + full == r or (full + os.sep).startswith(r + os.sep) + for r in allowed + ): + raise HTTPException(status_code=403, detail="Path not allowed") + return full + + # POSIX: absolute paths from / + if not raw or raw == "/": + return "/" + if not raw.startswith("/"): + raw = "/" + raw + full = os.path.normpath(raw) + if not full.startswith("/"): + raise HTTPException(status_code=400, detail="Invalid path") return full @@ -51,7 +69,15 @@ async def files_list( if not os.path.isdir(full): raise HTTPException(status_code=404, detail="Not a directory") items = [] - for name in os.listdir(full): + try: + names = os.listdir(full) + except PermissionError: + raise HTTPException(status_code=403, detail="Permission denied") + except FileNotFoundError: + raise HTTPException(status_code=404, detail="Not found") + except OSError as e: + raise HTTPException(status_code=500, detail=str(e)) + for name in names: item_path = os.path.join(full, name) try: stat = os.stat(item_path) @@ -62,7 +88,8 @@ async def files_list( }) except OSError: pass - return {"path": path, "items": items} + display_path = full if full == "/" else full.rstrip("/") + return {"path": display_path, "items": items} @router.get("/read") diff --git a/YakPanel-server/frontend/src/pages/FilesPage.tsx b/YakPanel-server/frontend/src/pages/FilesPage.tsx index 592ca17d..b455ac68 100644 --- a/YakPanel-server/frontend/src/pages/FilesPage.tsx +++ b/YakPanel-server/frontend/src/pages/FilesPage.tsx @@ -58,7 +58,7 @@ export function FilesPage() { const handleBack = () => { const parts = path.replace(/\/$/, '').split('/').filter(Boolean) - if (parts.length <= 1) return + if (parts.length === 0) return parts.pop() const newPath = parts.length === 0 ? '/' : '/' + parts.join('/') loadDir(newPath) @@ -147,8 +147,8 @@ export function FilesPage() { .catch((err) => setError(err.message)) } - const breadcrumbs = path.split('/').filter(Boolean) - const canGoBack = breadcrumbs.length > 0 + const pathSegments = path.replace(/\/$/, '').split('/').filter(Boolean) + const canGoBack = pathSegments.length > 0 return ( <> @@ -172,7 +172,7 @@ export function FilesPage() { )} Upload - Path: {path} + Path: {path || '/'} { setShowMkdir(false); setMkdirName('') }} centered>