Merge pull request #2721 from yueguobin/feature/list-node-files-api

feat: Add API endpoint to list node files with metadata
This commit is contained in:
Jeremy Grossmann 2026-05-09 00:04:16 +08:00 committed by GitHub
commit ae2fa31c5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 107 additions and 1 deletions

View File

@ -192,6 +192,20 @@ async def get_compute_project_files(project: Project = Depends(dep_project)) ->
return await project.list_files()
@router.get("/projects/{project_id}/nodes/{node_type}/{node_id}/files", response_model=List[schemas.NodeFile])
async def get_compute_node_files(
node_type: str,
node_id: str,
project: Project = Depends(dep_project)
) -> List[schemas.NodeFile]:
"""
Return files belonging to a specific node with detailed metadata.
"""
node_path = f"project-files/{node_type}/{node_id}"
return await project.list_node_files(node_path)
@router.get("/projects/{project_id}/files/{file_path:path}")
async def get_compute_project_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
"""

View File

@ -522,6 +522,23 @@ async def delete_disk_image(
await node.delete(f"/disk_image/{disk_name}")
@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]:
"""
List files in a node directory with detailed metadata.
Required privilege: Node.Audit
"""
node_type = node.node_type
res = await node.compute.http_query(
"GET",
f"/projects/{node.project.id}/nodes/{node_type}/{node.id}/files",
timeout=None
)
return res.json
@router.get("/{node_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Node.Audit"))])
async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
"""

View File

@ -18,8 +18,10 @@ import os
import shutil
import asyncio
import hashlib
import datetime
from uuid import UUID, uuid4
from fastapi import HTTPException, status
from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError, ComputeForbiddenError
from .port_manager import PortManager
@ -417,6 +419,67 @@ class Project:
return files
async def list_node_files(self, node_path: str):
"""
List files in a specific node directory with detailed metadata.
:param node_path: Relative path to node directory (e.g., "project-files/qemu/node-id")
:returns: Array of files in the node directory with metadata
"""
node_full_path = os.path.normpath(os.path.join(self.path, node_path))
# Security check: ensure the path is within the project directory
if not os.path.commonpath([node_full_path, self.path]) == self.path:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Path is outside the project directory")
if not os.path.exists(node_full_path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Node directory not found")
files = []
try:
filenames = os.listdir(node_full_path)
except OSError as e:
log.error(f"Error listing node directory: {e}")
return files
for filename in filenames:
file_path = os.path.join(node_full_path, filename)
if not os.path.isfile(file_path) or filename.endswith(".ghost"):
continue
try:
# Get file stat information
stat_info = await wait_run_in_executor(os.stat, file_path)
# Get file extension
_, extension = os.path.splitext(filename)
extension = extension.lstrip('.')
# Format timestamps as ISO 8601
try:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError) as e:
log.warning(f"Invalid timestamp for '{filename}': {e}")
created_at = modified_at = ""
file_info = {
"path": filename,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"extension": extension
}
files.append(file_info)
except OSError as e:
log.warning(f"Error getting metadata for file '{filename}': {e}")
continue
return files
def _hash_file(self, path):
"""
Compute and md5 hash for file

View File

@ -28,7 +28,7 @@ from .controller.appliances import ApplianceVersion, ApplianceVersionV8, Applian
from .controller.drawings import Drawing
from .controller.gns3vm import GNS3VM
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node
from .controller.projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile, ProjectCompression
from .controller.projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile, ProjectCompression, NodeFile
from .controller.users import UserCreate, UserUpdate, LoggedInUserUpdate, User, Credentials, UserGroupCreate, UserGroupUpdate, UserGroup
# Conditionally import AI-related schemas

View File

@ -105,6 +105,18 @@ class ProjectFile(BaseModel):
md5sum: str = Field(..., description="File checksum")
class NodeFile(BaseModel):
"""
Detailed file information for node files.
"""
path: str = Field(..., description="File name")
size: int = Field(..., description="File size in bytes")
created_at: str = Field(..., description="File creation time (ISO 8601)")
modified_at: str = Field(..., description="File modification time (ISO 8601)")
extension: str = Field(..., description="File extension")
class ProjectCompression(str, Enum):
"""
Supported project compression.