mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat: Node file streaming, recursive listing, file type detection, and file delete
- Stream file GET/POST through controller without buffering in memory - Add recursive and subdirectory filtering to node file listing - Replace file extension with magic-based file type detection - Add DELETE endpoint for node and project files - Include directories in listing response - Add params and stream support to http_query - Fix lambda closures, streamer exception scope, and delete error codes
This commit is contained in:
8 files changed
+289
-136
No files matched your search
@@ -24,6 +24,7 @@ import ipaddress
|
||||
|
||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status, Query, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.routing import APIRoute
|
||||
from typing import List, Callable, Optional
|
||||
from uuid import UUID
|
||||
@@ -523,17 +524,30 @@ async def delete_disk_image(
|
||||
|
||||
|
||||
@router.get("/{node_id}/files", response_model=List[schemas.NodeFile], dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
async def list_node_files(node: Node = Depends(dep_node)) -> List[schemas.NodeFile]:
|
||||
async def list_node_files(
|
||||
node: Node = Depends(dep_node),
|
||||
path: str = Query("", description="Subdirectory path within node directory"),
|
||||
recursive: bool = Query(False, description="Recursively list all files")
|
||||
) -> List[schemas.NodeFile]:
|
||||
"""
|
||||
List files in a node directory with detailed metadata.
|
||||
|
||||
By default lists only the current directory level (non-recursive).
|
||||
Use recursive=true for a full recursive listing.
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
node_type = node.node_type
|
||||
url = f"/projects/{node.project.id}/nodes/{node_type}/{node.id}/files"
|
||||
params = {}
|
||||
if path:
|
||||
params["path"] = path
|
||||
if recursive:
|
||||
params["recursive"] = "true"
|
||||
res = await node.compute.http_query(
|
||||
"GET",
|
||||
f"/projects/{node.project.id}/nodes/{node_type}/{node.id}/files",
|
||||
"GET", url,
|
||||
params=params if params else None,
|
||||
timeout=None
|
||||
)
|
||||
return res.json
|
||||
@@ -550,14 +564,32 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
|
||||
path = force_unix_path(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
if path.startswith(".."):
|
||||
raise ControllerForbiddenError("It is forbidden to get a file outside the project directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||
|
||||
res = await node.compute.http_query("GET", f"/projects/{node.project.id}/files{path}", timeout=None, raw=True)
|
||||
return Response(res.body, media_type="application/octet-stream", status_code=res.status)
|
||||
compute_resp = await node.compute.http_query(
|
||||
"GET", f"/projects/{node.project.id}/files{path}",
|
||||
timeout=None, stream=True
|
||||
)
|
||||
|
||||
async def streamer():
|
||||
try:
|
||||
async for chunk in compute_resp.content.iter_chunked(65536):
|
||||
yield chunk
|
||||
except (IOError, OSError, asyncio.TimeoutError) as e:
|
||||
log.error(f"Error streaming file '{path}' from compute: {e}")
|
||||
raise
|
||||
finally:
|
||||
compute_resp.close()
|
||||
|
||||
return StreamingResponse(
|
||||
streamer(),
|
||||
media_type="application/octet-stream",
|
||||
status_code=compute_resp.status,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -575,15 +607,43 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
|
||||
path = force_unix_path(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
if path.startswith(".."):
|
||||
raise ControllerForbiddenError("Cannot write outside the node directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||
|
||||
data = await request.body() # FIXME: are we handling timeout or large files correctly?
|
||||
await node.compute.http_query("POST", f"/projects/{node.project.id}/files{path}", data=data, timeout=None, raw=True)
|
||||
# FIXME: response with correct status code (from compute)
|
||||
# Stream request body directly to compute node
|
||||
await node.compute.http_query(
|
||||
"POST", f"/projects/{node.project.id}/files{path}",
|
||||
data=request.stream(), timeout=None
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{node_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Modify"))]
|
||||
)
|
||||
async def delete_node_file(file_path: str, node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a file from the node directory.
|
||||
|
||||
Required privilege: Node.Modify
|
||||
"""
|
||||
|
||||
path = force_unix_path(file_path)
|
||||
|
||||
if path.startswith(".."):
|
||||
raise ControllerForbiddenError("It is forbidden to delete a file outside the project directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||
|
||||
await node.compute.http_query(
|
||||
"DELETE", f"/projects/{node.project.id}/files{path}",
|
||||
timeout=None
|
||||
)
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
|
||||
Reference in new issue
Block a user