mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-06 15:21:13 +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
+121
-44
@@ -16,6 +16,7 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import magic
|
||||
import asyncio
|
||||
import hashlib
|
||||
import datetime
|
||||
@@ -424,65 +425,141 @@ class Project:
|
||||
|
||||
return files
|
||||
|
||||
async def list_node_files(self, node_path: str):
|
||||
async def list_node_files(self, node_path: str, subpath: str = "", recursive: bool = False):
|
||||
"""
|
||||
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")
|
||||
:param subpath: Optional subdirectory path. Defaults to root of node directory.
|
||||
:param recursive: If True, recursively list all files (use with caution on large directories).
|
||||
:returns: Array of files in the node directory with metadata
|
||||
"""
|
||||
|
||||
node_full_path = os.path.normpath(os.path.join(self.path, node_path))
|
||||
subpath = subpath.lstrip("/")
|
||||
|
||||
# Security check: ensure the path is within the project directory
|
||||
if not os.path.commonpath([node_full_path, self.path]) == self.path:
|
||||
if subpath:
|
||||
target_path = os.path.normpath(os.path.join(node_full_path, subpath))
|
||||
else:
|
||||
target_path = node_full_path
|
||||
|
||||
# Security check: ensure the path is within the node directory
|
||||
if not os.path.commonpath([target_path, node_full_path]) == node_full_path:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Path is outside the project directory")
|
||||
|
||||
if not os.path.exists(node_full_path):
|
||||
detail="Path is outside the node directory")
|
||||
if not os.path.exists(target_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Node directory not found")
|
||||
detail="Path not found")
|
||||
|
||||
if recursive:
|
||||
return await self._list_node_files_recursive(node_full_path, target_path,
|
||||
node_full_path if subpath else None)
|
||||
|
||||
# Non-recursive: list only the current directory level
|
||||
files = []
|
||||
for entry in os.scandir(target_path):
|
||||
name = entry.name
|
||||
rel_path = name if not subpath else os.path.join(subpath, name)
|
||||
try:
|
||||
stat_info = await wait_run_in_executor(lambda e=entry: e.stat())
|
||||
is_dir = await wait_run_in_executor(lambda e=entry: e.is_dir())
|
||||
if is_dir:
|
||||
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):
|
||||
created_at = modified_at = ""
|
||||
files.append({
|
||||
"path": rel_path,
|
||||
"size": stat_info.st_size,
|
||||
"created_at": created_at,
|
||||
"modified_at": modified_at,
|
||||
"file_type": "directory"
|
||||
})
|
||||
else:
|
||||
if name.endswith(".ghost"):
|
||||
continue
|
||||
try:
|
||||
file_type = await wait_run_in_executor(
|
||||
lambda e=entry: magic.from_file(e.path, mime=False)
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"Error getting file type for '{rel_path}': {e}")
|
||||
file_type = ""
|
||||
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):
|
||||
created_at = modified_at = ""
|
||||
files.append({
|
||||
"path": rel_path,
|
||||
"size": stat_info.st_size,
|
||||
"created_at": created_at,
|
||||
"modified_at": modified_at,
|
||||
"file_type": file_type
|
||||
})
|
||||
except OSError:
|
||||
continue
|
||||
return files
|
||||
|
||||
async def _list_node_files_recursive(self, node_full_path, start_path, base_path=None):
|
||||
"""
|
||||
Recursively list all files and directories under start_path.
|
||||
"""
|
||||
if base_path is None:
|
||||
base_path = node_full_path
|
||||
|
||||
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
|
||||
for dirpath, dirnames, filenames in os.walk(start_path, followlinks=False):
|
||||
for dirname in dirnames:
|
||||
dir_full_path = os.path.join(dirpath, dirname)
|
||||
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
|
||||
stat_info = await wait_run_in_executor(os.stat, dir_full_path)
|
||||
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):
|
||||
created_at = modified_at = ""
|
||||
files.append({
|
||||
"path": os.path.relpath(dir_full_path, base_path),
|
||||
"size": stat_info.st_size,
|
||||
"created_at": created_at,
|
||||
"modified_at": modified_at,
|
||||
"file_type": "directory"
|
||||
})
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
for filename in filenames:
|
||||
if filename.endswith(".ghost"):
|
||||
continue
|
||||
file_path = os.path.join(dirpath, filename)
|
||||
rel_path = os.path.relpath(file_path, base_path)
|
||||
try:
|
||||
stat_info = await wait_run_in_executor(os.stat, file_path)
|
||||
try:
|
||||
file_type = await wait_run_in_executor(
|
||||
lambda fp=file_path: magic.from_file(fp, mime=False)
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"Error getting file type for '{rel_path}': {e}")
|
||||
file_type = ""
|
||||
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 '{rel_path}': {e}")
|
||||
created_at = modified_at = ""
|
||||
files.append({
|
||||
"path": rel_path,
|
||||
"size": stat_info.st_size,
|
||||
"created_at": created_at,
|
||||
"modified_at": modified_at,
|
||||
"file_type": file_type
|
||||
})
|
||||
except OSError as e:
|
||||
log.warning(f"Error getting metadata for file '{rel_path}': {e}")
|
||||
continue
|
||||
return files
|
||||
|
||||
def _hash_file(self, path):
|
||||
|
||||
Reference in new issue
Block a user