""" 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