"""DMS Search / shared-with-me / Bulk Routen — extrahiert aus routes.py (BUG-018).""" from __future__ import annotations import uuid from fastapi import ( APIRouter, Depends, HTTPException, ) from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.visibility import apply_visibility_filter from app.deps import get_current_user, require_permission from app.plugins.builtins.dms.common import ( _parse_uuid, ) from app.plugins.builtins.dms.models import File as DmsFile from app.plugins.builtins.dms.models import Folder from app.plugins.builtins.dms.schemas import BulkDeleteRequest, BulkMoveRequest from app.plugins.builtins.permissions.contracts import get_contract as get_perms_contract _perms_contract = get_perms_contract() Permission = _perms_contract.Permission router = APIRouter(tags=["dms"]) @router.get("/search", dependencies=[Depends(require_permission("dms:read"))]) async def search_files( q: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC16: GET /api/v1/dms/search?q=text → 200 + matching files (ILIKE).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" query = select(DmsFile).where( DmsFile.tenant_id == tenant_id, DmsFile.deleted_at.is_(None), DmsFile.name.ilike(f"%{q}%"), ) query = await apply_visibility_filter( db, query, "dms_file", DmsFile, user_id, tenant_id, is_system_admin ) result = await db.execute(query) files = result.scalars().all() return [ { "id": str(f.id), "name": f.name, "folder_id": str(f.folder_id) if f.folder_id else None, "uploaded_by": str(f.uploaded_by), "mime_type": f.mime_type, "size_bytes": f.size_bytes, "deleted_at": None, "created_at": f.created_at.isoformat() if f.created_at else None, } for f in files ] @router.get("/shared-with-me", dependencies=[Depends(require_permission("dms:read"))]) async def shared_with_me( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC17: GET /api/v1/dms/shared-with-me → 200 + shared files list.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" # Query permissions for this user and join with files perm_result = await db.execute( select(Permission).where( Permission.tenant_id == tenant_id, Permission.user_id == user_id, ) ) perms = perm_result.scalars().all() file_ids = {p.file_id for p in perms} if not file_ids: return {"items": [], "total": 0} query = select(DmsFile).where( DmsFile.tenant_id == tenant_id, DmsFile.id.in_(file_ids), DmsFile.deleted_at.is_(None), ) query = await apply_visibility_filter( db, query, "dms_file", DmsFile, user_id, tenant_id, is_system_admin ) result = await db.execute(query) files = result.scalars().all() # Map permissions for access_level perm_map: dict[uuid.UUID, str] = {} for p in perms: if p.file_id in file_ids: perm_map[p.file_id] = p.access_level return [ { "id": str(f.id), "name": f.name, "folder_id": str(f.folder_id) if f.folder_id else None, "uploaded_by": str(f.uploaded_by), "mime_type": f.mime_type, "size_bytes": f.size_bytes, "access_level": perm_map.get(f.id, "read"), "created_at": f.created_at.isoformat() if f.created_at else None, } for f in files ] @router.post("/files/bulk-move", dependencies=[Depends(require_permission("dms:write"))]) async def bulk_move( body: BulkMoveRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC18: POST /api/v1/dms/files/bulk-move → 200, files moved.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" target_folder_id = ( _parse_uuid(body.target_folder_id, "target_folder_id") if body.target_folder_id else None ) # Validate target folder if specified if target_folder_id is not None: folder_result = await db.execute( select(Folder).where( Folder.id == target_folder_id, Folder.tenant_id == tenant_id, Folder.deleted_at.is_(None), ) ) if folder_result.scalar_one_or_none() is None: raise HTTPException( 404, detail={"detail": "Target folder not found", "code": "not_found"} ) file_ids = [_parse_uuid(fid, "file_id") for fid in body.file_ids] query = select(DmsFile).where( DmsFile.tenant_id == tenant_id, DmsFile.id.in_(file_ids), DmsFile.deleted_at.is_(None), ) query = await apply_visibility_filter( db, query, "dms_file", DmsFile, user_id, tenant_id, is_system_admin ) result = await db.execute(query) files = result.scalars().all() moved_count = 0 for f in files: f.folder_id = target_folder_id moved_count += 1 await db.flush() return { "moved": moved_count, "file_ids": [str(fid) for fid in file_ids], "target_folder_id": str(target_folder_id) if target_folder_id else None, } @router.post("/files/bulk-delete", dependencies=[Depends(require_permission("dms:delete"))]) async def bulk_delete( body: BulkDeleteRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC19: POST /api/v1/dms/files/bulk-delete → 200, files soft-deleted.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" file_ids = [_parse_uuid(fid, "file_id") for fid in body.file_ids] from datetime import UTC, datetime now = datetime.now(UTC) # Apply visibility filter to only delete files user has access to query = select(DmsFile).where( DmsFile.tenant_id == tenant_id, DmsFile.id.in_(file_ids), DmsFile.deleted_at.is_(None), ) query = await apply_visibility_filter( db, query, "dms_file", DmsFile, user_id, tenant_id, is_system_admin ) result = await db.execute(query) accessible_files = result.scalars().all() accessible_ids = [f.id for f in accessible_files] result = await db.execute( update(DmsFile) .where( DmsFile.tenant_id == tenant_id, DmsFile.id.in_(accessible_ids), DmsFile.deleted_at.is_(None), ) .values(deleted_at=now) ) deleted_count = result.rowcount await db.flush() return { "deleted": deleted_count, "file_ids": body.file_ids, }