import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.folder import Folder from app.models.project import Project from app.schemas.folder import FolderCreate, FolderListResponse, FolderResponse, FolderUpdate from app.routers.auth import get_current_agent router = APIRouter(tags=["folders"]) def build_folder_path(project_id: str, folder_id: str, parent_id: str | None, parent_path: str | None) -> str: if parent_id and parent_path: return f"{parent_path}/{folder_id}" return f"/{project_id}/{folder_id}" @router.get("/api/v1/projects/{project_id}/folders", response_model=FolderListResponse) async def list_folders( request: Request, project_id: str, parent_id: str | None = Query(None), db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) proj_result = await db.execute( select(Project).where( Project.id == project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Project not found") query = select(Folder).where( Folder.project_id == project_id, Folder.parent_id == parent_id, Folder.is_deleted == False, ).order_by(Folder.name) result = await db.execute(query) folders = result.scalars().all() return FolderListResponse(folders=[FolderResponse.model_validate(f) for f in folders]) @router.post("/api/v1/projects/{project_id}/folders", response_model=FolderResponse, status_code=201) async def create_folder( request: Request, project_id: str, payload: FolderCreate, db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) proj_result = await db.execute( select(Project).where( Project.id == project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Project not found") parent_path = None if payload.parent_id: parent_result = await db.execute( select(Folder).where( Folder.id == payload.parent_id, Folder.project_id == project_id, Folder.is_deleted == False, ) ) parent = parent_result.scalar_one_or_none() if not parent: raise HTTPException(status_code=400, detail="Parent folder not found") parent_path = parent.path folder_id = str(uuid.uuid4()) path = build_folder_path(project_id, folder_id, payload.parent_id, parent_path) folder = Folder( id=folder_id, name=payload.name, project_id=project_id, parent_id=payload.parent_id, path=path, ) db.add(folder) await db.flush() return FolderResponse.model_validate(folder) @router.get("/api/v1/folders/{folder_id}", response_model=FolderResponse) async def get_folder( request: Request, folder_id: str, db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) result = await db.execute( select(Folder).where( Folder.id == folder_id, Folder.is_deleted == False, ) ) folder = result.scalar_one_or_none() if not folder: raise HTTPException(status_code=404, detail="Folder not found") proj_result = await db.execute( select(Project).where( Project.id == folder.project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Folder not found") return FolderResponse.model_validate(folder) @router.put("/api/v1/folders/{folder_id}", response_model=FolderResponse) async def update_folder( request: Request, folder_id: str, payload: FolderUpdate, db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) result = await db.execute( select(Folder).where( Folder.id == folder_id, Folder.is_deleted == False, ) ) folder = result.scalar_one_or_none() if not folder: raise HTTPException(status_code=404, detail="Folder not found") proj_result = await db.execute( select(Project).where( Project.id == folder.project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=403, detail="Forbidden") if payload.name is not None: folder.name = payload.name if payload.parent_id is not None: if payload.parent_id == folder_id: raise HTTPException(status_code=400, detail="Cannot set folder as its own parent") parent_result = await db.execute( select(Folder).where( Folder.id == payload.parent_id, Folder.project_id == folder.project_id, Folder.is_deleted == False, ) ) parent = parent_result.scalar_one_or_none() if not parent: raise HTTPException(status_code=400, detail="Parent folder not found") folder.parent_id = payload.parent_id folder.path = f"{parent.path}/{folder.id}" folder.updated_at = datetime.utcnow() await db.flush() return FolderResponse.model_validate(folder) @router.delete("/api/v1/folders/{folder_id}", status_code=204) async def delete_folder( request: Request, folder_id: str, db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) result = await db.execute( select(Folder).where( Folder.id == folder_id, Folder.is_deleted == False, ) ) folder = result.scalar_one_or_none() if not folder: raise HTTPException(status_code=404, detail="Folder not found") proj_result = await db.execute( select(Project).where( Project.id == folder.project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=403, detail="Forbidden") folder.is_deleted = True folder.deleted_at = datetime.utcnow() folder.deleted_by = agent.id await db.flush() return None @router.post("/api/v1/folders/{folder_id}/restore", response_model=FolderResponse) async def restore_folder( request: Request, folder_id: str, db: AsyncSession = Depends(get_db), ): agent = await get_current_agent(request, db) result = await db.execute( select(Folder).where( Folder.id == folder_id, Folder.is_deleted == True, ) ) folder = result.scalar_one_or_none() if not folder: raise HTTPException(status_code=404, detail="Folder not found") proj_result = await db.execute( select(Project).where( Project.id == folder.project_id, Project.agent_id == agent.id, Project.is_deleted == False, ) ) if not proj_result.scalar_one_or_none(): raise HTTPException(status_code=403, detail="Forbidden") folder.is_deleted = False folder.deleted_at = None folder.deleted_by = None await db.flush() return FolderResponse.model_validate(folder)