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>