Files
Atomizer/atomizer-dashboard/backend/api/routes/files.py
Anto01 1c7c7aff05 feat(canvas): Add file browser, introspection, and improve node flow
Phase 1-7 of Canvas V4 Ralph Loop implementation:

Backend:
- Add /api/files routes for browsing model files
- Add /api/nx routes for NX model introspection
- Add NXIntrospector service to discover expressions and extractors
- Add health check with database status

Frontend:
- Add FileBrowser component for selecting .sim/.prt/.fem files
- Add IntrospectionPanel to discover expressions and extractors
- Update NodeConfigPanel with browse and introspect buttons
- Update schema with NODE_HANDLES for proper flow direction
- Update validation for correct DesignVar -> Model -> Solver flow
- Update useCanvasStore.addNode() to accept custom data

Flow correction: Design Variables now connect TO Model (as source),
not FROM Model. This matches the actual data flow in optimization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:47:10 -05:00

156 lines
4.5 KiB
Python

"""
Files API Routes
Provides file browsing capabilities for the Canvas Builder.
"""
from fastapi import APIRouter, Query
from pathlib import Path
from typing import List, Optional
import os
router = APIRouter()
# Path to studies root (go up 5 levels from this file)
_file_path = os.path.abspath(__file__)
ATOMIZER_ROOT = Path(os.path.normpath(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.dirname(_file_path))
)))))
STUDIES_ROOT = ATOMIZER_ROOT / "studies"
@router.get("/list")
async def list_files(
path: str = "",
types: str = ".sim,.prt,.fem,.afem"
):
"""
List files in a directory, filtered by type.
Args:
path: Relative path from studies root (empty for root)
types: Comma-separated list of file extensions to include
Returns:
List of files and directories with their paths
"""
allowed_types = [t.strip().lower() for t in types.split(',') if t.strip()]
base_path = STUDIES_ROOT / path if path else STUDIES_ROOT
if not base_path.exists():
return {"files": [], "path": path, "error": "Directory not found"}
files = []
try:
for entry in sorted(base_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
# Skip hidden files and directories
if entry.name.startswith('.'):
continue
if entry.is_dir():
# Include directories
files.append({
"name": entry.name,
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
"isDirectory": True,
})
else:
# Include files matching type filter
suffix = entry.suffix.lower()
if suffix in allowed_types:
files.append({
"name": entry.name,
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
"isDirectory": False,
"size": entry.stat().st_size,
})
except PermissionError:
return {"files": [], "path": path, "error": "Permission denied"}
except Exception as e:
return {"files": [], "path": path, "error": str(e)}
return {"files": files, "path": path}
@router.get("/search")
async def search_files(
query: str,
types: str = ".sim,.prt,.fem,.afem",
max_results: int = 50
):
"""
Search for files by name pattern.
Args:
query: Search pattern (partial name match)
types: Comma-separated list of file extensions to include
max_results: Maximum number of results to return
Returns:
List of matching files with their paths
"""
allowed_types = [t.strip().lower() for t in types.split(',') if t.strip()]
query_lower = query.lower()
files = []
def search_recursive(directory: Path, depth: int = 0):
"""Recursively search for matching files"""
if depth > 10 or len(files) >= max_results: # Limit depth and results
return
try:
for entry in directory.iterdir():
if len(files) >= max_results:
return
if entry.name.startswith('.'):
continue
if entry.is_dir():
search_recursive(entry, depth + 1)
elif entry.suffix.lower() in allowed_types:
if query_lower in entry.name.lower():
files.append({
"name": entry.name,
"path": str(entry.relative_to(STUDIES_ROOT)).replace("\\", "/"),
"isDirectory": False,
"size": entry.stat().st_size,
})
except (PermissionError, OSError):
pass
search_recursive(STUDIES_ROOT)
return {"files": files, "query": query, "total": len(files)}
@router.get("/exists")
async def check_file_exists(path: str):
"""
Check if a file exists.
Args:
path: Relative path from studies root
Returns:
Boolean indicating if file exists and file info
"""
file_path = STUDIES_ROOT / path
exists = file_path.exists()
result = {
"exists": exists,
"path": path,
}
if exists:
result["isDirectory"] = file_path.is_dir()
if file_path.is_file():
result["size"] = file_path.stat().st_size
result["name"] = file_path.name
return result