new changes
This commit is contained in:
@@ -13,28 +13,46 @@ router = APIRouter(prefix="/files", tags=["files"])
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_path(path: str) -> str:
|
def _resolve_path(path: str) -> str:
|
||||||
"""Resolve and validate path within allowed roots (cross-platform)"""
|
"""
|
||||||
cfg = get_runtime_config()
|
Resolve API path to an OS path.
|
||||||
www_root = os.path.abspath(cfg["www_root"])
|
|
||||||
setup_path = os.path.abspath(cfg["setup_path"])
|
On Linux/macOS: path is an absolute POSIX path from filesystem root (/) so admins
|
||||||
allowed = [www_root, setup_path]
|
can browse the whole server (same expectation as BT/aaPanel-style panels).
|
||||||
if os.name != "nt":
|
|
||||||
allowed.append(os.path.abspath("/www"))
|
On Windows (dev): paths stay sandboxed under www_root / setup_path.
|
||||||
|
"""
|
||||||
if ".." in path:
|
if ".." in path:
|
||||||
raise HTTPException(status_code=400, detail="Path traversal not allowed")
|
raise HTTPException(status_code=400, detail="Path traversal not allowed")
|
||||||
norm_path = path.strip().replace("\\", "/").strip("/")
|
|
||||||
# Root or www_root-style path
|
raw = path.strip().replace("\\", "/")
|
||||||
if not norm_path or norm_path in ("www", "www/wwwroot", "wwwroot"):
|
|
||||||
full = www_root
|
if os.name == "nt":
|
||||||
elif norm_path.startswith("www/wwwroot/"):
|
cfg = get_runtime_config()
|
||||||
full = os.path.abspath(os.path.join(www_root, norm_path[12:]))
|
www_root = os.path.abspath(cfg["www_root"])
|
||||||
else:
|
setup_path = os.path.abspath(cfg["setup_path"])
|
||||||
full = os.path.abspath(os.path.join(www_root, norm_path))
|
allowed = [www_root, setup_path]
|
||||||
if not any(
|
norm_path = raw.strip("/")
|
||||||
full == r or (full + os.sep).startswith(r + os.sep)
|
if not norm_path or norm_path in ("www", "www/wwwroot", "wwwroot"):
|
||||||
for r in allowed
|
full = www_root
|
||||||
):
|
elif norm_path.startswith("www/wwwroot/"):
|
||||||
raise HTTPException(status_code=403, detail="Path not allowed")
|
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
|
return full
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +69,15 @@ async def files_list(
|
|||||||
if not os.path.isdir(full):
|
if not os.path.isdir(full):
|
||||||
raise HTTPException(status_code=404, detail="Not a directory")
|
raise HTTPException(status_code=404, detail="Not a directory")
|
||||||
items = []
|
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)
|
item_path = os.path.join(full, name)
|
||||||
try:
|
try:
|
||||||
stat = os.stat(item_path)
|
stat = os.stat(item_path)
|
||||||
@@ -62,7 +88,8 @@ async def files_list(
|
|||||||
})
|
})
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
return {"path": path, "items": items}
|
display_path = full if full == "/" else full.rstrip("/")
|
||||||
|
return {"path": display_path, "items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/read")
|
@router.get("/read")
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function FilesPage() {
|
|||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
const parts = path.replace(/\/$/, '').split('/').filter(Boolean)
|
const parts = path.replace(/\/$/, '').split('/').filter(Boolean)
|
||||||
if (parts.length <= 1) return
|
if (parts.length === 0) return
|
||||||
parts.pop()
|
parts.pop()
|
||||||
const newPath = parts.length === 0 ? '/' : '/' + parts.join('/')
|
const newPath = parts.length === 0 ? '/' : '/' + parts.join('/')
|
||||||
loadDir(newPath)
|
loadDir(newPath)
|
||||||
@@ -147,8 +147,8 @@ export function FilesPage() {
|
|||||||
.catch((err) => setError(err.message))
|
.catch((err) => setError(err.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = path.split('/').filter(Boolean)
|
const pathSegments = path.replace(/\/$/, '').split('/').filter(Boolean)
|
||||||
const canGoBack = breadcrumbs.length > 0
|
const canGoBack = pathSegments.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -172,7 +172,7 @@ export function FilesPage() {
|
|||||||
)}
|
)}
|
||||||
Upload
|
Upload
|
||||||
</AdminButton>
|
</AdminButton>
|
||||||
<code className="small bg-body-secondary px-2 py-1 rounded ms-auto text-break">Path: {path}</code>
|
<code className="small bg-body-secondary px-2 py-1 rounded ms-auto text-break">Path: {path || '/'}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal show={showMkdir} onHide={() => { setShowMkdir(false); setMkdirName('') }} centered>
|
<Modal show={showMkdir} onHide={() => { setShowMkdir(false); setMkdirName('') }} centered>
|
||||||
|
|||||||
Reference in New Issue
Block a user