From 51a4c73e211ebce2f378785dc1269c61d352208f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 8 May 2026 22:57:19 +0800 Subject: [PATCH] feat: Add detailed metadata to node files listing Enhance the node files API to include comprehensive file metadata: - File size in bytes - File creation time (ISO 8601 format) - File modification time (ISO 8601 format) - File extension Create new NodeFile schema to support these additional fields while keeping the existing ProjectFile schema for backward compatibility. This provides users with better information to manage and identify files in the Web UI. Co-Authored-By: Claude Sonnet 4.6 --- gns3server/api/routes/compute/projects.py | 6 ++-- gns3server/api/routes/controller/nodes.py | 6 ++-- gns3server/compute/project.py | 38 ++++++++++++++++++----- gns3server/schemas/controller/projects.py | 13 ++++++++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/gns3server/api/routes/compute/projects.py b/gns3server/api/routes/compute/projects.py index 25de86740..711e78416 100644 --- a/gns3server/api/routes/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -192,14 +192,14 @@ 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.ProjectFile]) +@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.ProjectFile]: +) -> List[schemas.NodeFile]: """ - Return files belonging to a specific node. + Return files belonging to a specific node with detailed metadata. """ node_path = f"project-files/{node_type}/{node_id}" diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 68b009275..0ee624ce4 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -522,10 +522,10 @@ async def delete_disk_image( await node.delete(f"/disk_image/{disk_name}") -@router.get("/{node_id}/files", response_model=List[schemas.ProjectFile], dependencies=[Depends(has_privilege("Node.Audit"))]) -async def list_node_files(node: Node = Depends(dep_node)) -> List[schemas.ProjectFile]: +@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. + List files in a node directory with detailed metadata. Required privilege: Node.Audit """ diff --git a/gns3server/compute/project.py b/gns3server/compute/project.py index ca189a4a2..1539a7cd0 100644 --- a/gns3server/compute/project.py +++ b/gns3server/compute/project.py @@ -18,6 +18,7 @@ import os import shutil import asyncio import hashlib +import datetime from uuid import UUID, uuid4 from fastapi import HTTPException @@ -420,10 +421,10 @@ class Project: async def list_node_files(self, node_path: str): """ - List files in a specific node directory. + 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 + :returns: Array of files in the node directory with metadata """ node_full_path = os.path.normpath(os.path.join(self.path, node_path)) @@ -442,14 +443,37 @@ class Project: for filename in os.listdir(node_full_path): file_path = os.path.join(node_full_path, filename) if os.path.isfile(file_path) and not filename.endswith(".ghost"): - file_info = {"path": filename} try: - file_info["md5sum"] = await wait_run_in_executor( - self._hash_file, file_path + # 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 + created_at = await wait_run_in_executor( + lambda: datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat() ) - except OSError: + modified_at = await wait_run_in_executor( + lambda: datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat() + ) + + # Get MD5 checksum + md5sum = await wait_run_in_executor(self._hash_file, file_path) + + file_info = { + "path": filename, + "md5sum": md5sum, + "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 - files.append(file_info) except OSError as e: log.error(f"Error listing node files: {e}") diff --git a/gns3server/schemas/controller/projects.py b/gns3server/schemas/controller/projects.py index 711eabb6b..bbb6164db 100644 --- a/gns3server/schemas/controller/projects.py +++ b/gns3server/schemas/controller/projects.py @@ -105,6 +105,19 @@ class ProjectFile(BaseModel): md5sum: str = Field(..., description="File checksum") +class NodeFile(BaseModel): + """ + Detailed file information for node files. + """ + + path: str = Field(..., description="File name") + md5sum: str = Field(..., description="File checksum") + 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.