Initial YakPanel commit
This commit is contained in:
240
YakPanel-server/backend/app/api/files.py
Normal file
240
YakPanel-server/backend/app/api/files.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""YakPanel - File manager API"""
|
||||
import os
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.config import get_runtime_config
|
||||
from app.core.utils import read_file, write_file, path_safe_check
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
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"))
|
||||
if ".." in path:
|
||||
raise HTTPException(status_code=401, 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")
|
||||
return full
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def files_list(
|
||||
path: str = "/",
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List directory contents"""
|
||||
try:
|
||||
full = _resolve_path(path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not os.path.isdir(full):
|
||||
raise HTTPException(status_code=401, detail="Not a directory")
|
||||
items = []
|
||||
for name in os.listdir(full):
|
||||
item_path = os.path.join(full, name)
|
||||
try:
|
||||
stat = os.stat(item_path)
|
||||
items.append({
|
||||
"name": name,
|
||||
"is_dir": os.path.isdir(item_path),
|
||||
"size": stat.st_size if os.path.isfile(item_path) else 0,
|
||||
})
|
||||
except OSError:
|
||||
pass
|
||||
return {"path": path, "items": items}
|
||||
|
||||
|
||||
@router.get("/read")
|
||||
async def files_read(
|
||||
path: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Read file content"""
|
||||
try:
|
||||
full = _resolve_path(path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not os.path.isfile(full):
|
||||
raise HTTPException(status_code=404, detail="Not a file")
|
||||
content = read_file(full)
|
||||
if content is None:
|
||||
raise HTTPException(status_code=500, detail="Failed to read file")
|
||||
return {"path": path, "content": content}
|
||||
|
||||
|
||||
@router.get("/download")
|
||||
async def files_download(
|
||||
path: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Download file"""
|
||||
try:
|
||||
full = _resolve_path(path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not os.path.isfile(full):
|
||||
raise HTTPException(status_code=404, detail="Not a file")
|
||||
return FileResponse(full, filename=os.path.basename(full))
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def files_upload(
|
||||
path: str = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Upload file to directory"""
|
||||
try:
|
||||
full = _resolve_path(path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not os.path.isdir(full):
|
||||
raise HTTPException(status_code=400, detail="Path must be a directory")
|
||||
filename = file.filename or "upload"
|
||||
if ".." in filename or "/" in filename or "\\" in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
dest = os.path.join(full, filename)
|
||||
content = await file.read()
|
||||
if not write_file(dest, content, "wb"):
|
||||
raise HTTPException(status_code=500, detail="Failed to write file")
|
||||
return {"status": True, "msg": "Uploaded", "path": path + "/" + filename}
|
||||
|
||||
|
||||
class MkdirRequest(BaseModel):
|
||||
path: str
|
||||
name: str
|
||||
|
||||
|
||||
class RenameRequest(BaseModel):
|
||||
path: str
|
||||
old_name: str
|
||||
new_name: str
|
||||
|
||||
|
||||
class DeleteRequest(BaseModel):
|
||||
path: str
|
||||
name: str
|
||||
is_dir: bool
|
||||
|
||||
|
||||
@router.post("/mkdir")
|
||||
async def files_mkdir(
|
||||
body: MkdirRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create directory"""
|
||||
try:
|
||||
parent = _resolve_path(body.path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not os.path.isdir(parent):
|
||||
raise HTTPException(status_code=400, detail="Parent must be a directory")
|
||||
if not body.name or ".." in body.name or "/" in body.name or "\\" in body.name:
|
||||
raise HTTPException(status_code=400, detail="Invalid directory name")
|
||||
if not path_safe_check(body.name):
|
||||
raise HTTPException(status_code=400, detail="Invalid directory name")
|
||||
full = os.path.join(parent, body.name)
|
||||
if os.path.exists(full):
|
||||
raise HTTPException(status_code=400, detail="Already exists")
|
||||
try:
|
||||
os.makedirs(full, 0o755)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {"status": True, "msg": "Created", "path": body.path.rstrip("/") + "/" + body.name}
|
||||
|
||||
|
||||
@router.post("/rename")
|
||||
async def files_rename(
|
||||
body: RenameRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Rename file or directory"""
|
||||
try:
|
||||
parent = _resolve_path(body.path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not body.old_name or not body.new_name:
|
||||
raise HTTPException(status_code=400, detail="Names required")
|
||||
for n in (body.old_name, body.new_name):
|
||||
if ".." in n or "/" in n or "\\" in n or not path_safe_check(n):
|
||||
raise HTTPException(status_code=400, detail="Invalid name")
|
||||
old_full = os.path.join(parent, body.old_name)
|
||||
new_full = os.path.join(parent, body.new_name)
|
||||
if not os.path.exists(old_full):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if os.path.exists(new_full):
|
||||
raise HTTPException(status_code=400, detail="Target already exists")
|
||||
try:
|
||||
os.rename(old_full, new_full)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {"status": True, "msg": "Renamed"}
|
||||
|
||||
|
||||
@router.post("/delete")
|
||||
async def files_delete(
|
||||
body: DeleteRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete file or directory"""
|
||||
try:
|
||||
parent = _resolve_path(body.path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if not body.name or ".." in body.name or "/" in body.name or "\\" in body.name:
|
||||
raise HTTPException(status_code=400, detail="Invalid name")
|
||||
full = os.path.join(parent, body.name)
|
||||
if not os.path.exists(full):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
try:
|
||||
if body.is_dir:
|
||||
import shutil
|
||||
shutil.rmtree(full)
|
||||
else:
|
||||
os.remove(full)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {"status": True, "msg": "Deleted"}
|
||||
|
||||
|
||||
class WriteFileRequest(BaseModel):
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/write")
|
||||
async def files_write(
|
||||
body: WriteFileRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Write text file content"""
|
||||
try:
|
||||
full = _resolve_path(body.path)
|
||||
except HTTPException:
|
||||
raise
|
||||
if os.path.isdir(full):
|
||||
raise HTTPException(status_code=400, detail="Cannot write to directory")
|
||||
if not write_file(full, body.content):
|
||||
raise HTTPException(status_code=500, detail="Failed to write file")
|
||||
return {"status": True, "msg": "Saved"}
|
||||
Reference in New Issue
Block a user