mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-21 09:20:43 +03:00
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:
commit
ae2fa31c5e
@ -192,6 +192,20 @@ async def get_compute_project_files(project: Project = Depends(dep_project)) ->
|
|||||||
return await project.list_files()
|
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}")
|
@router.get("/projects/{project_id}/files/{file_path:path}")
|
||||||
async def get_compute_project_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
|
async def get_compute_project_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -522,6 +522,23 @@ async def delete_disk_image(
|
|||||||
await node.delete(f"/disk_image/{disk_name}")
|
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"))])
|
@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:
|
async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -18,8 +18,10 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import datetime
|
||||||
|
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError, ComputeForbiddenError
|
from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError, ComputeForbiddenError
|
||||||
from .port_manager import PortManager
|
from .port_manager import PortManager
|
||||||
@ -417,6 +419,67 @@ class Project:
|
|||||||
|
|
||||||
return files
|
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):
|
def _hash_file(self, path):
|
||||||
"""
|
"""
|
||||||
Compute and md5 hash for file
|
Compute and md5 hash for file
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from .controller.appliances import ApplianceVersion, ApplianceVersionV8, Applian
|
|||||||
from .controller.drawings import Drawing
|
from .controller.drawings import Drawing
|
||||||
from .controller.gns3vm import GNS3VM
|
from .controller.gns3vm import GNS3VM
|
||||||
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node
|
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
|
from .controller.users import UserCreate, UserUpdate, LoggedInUserUpdate, User, Credentials, UserGroupCreate, UserGroupUpdate, UserGroup
|
||||||
|
|
||||||
# Conditionally import AI-related schemas
|
# Conditionally import AI-related schemas
|
||||||
|
|||||||
@ -105,6 +105,18 @@ class ProjectFile(BaseModel):
|
|||||||
md5sum: str = Field(..., description="File checksum")
|
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):
|
class ProjectCompression(str, Enum):
|
||||||
"""
|
"""
|
||||||
Supported project compression.
|
Supported project compression.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user