Files
leocrm/app/plugins/builtins/dms/search_bulk_routes.py
T
Agent Zero f445aa69d5
Check Cross-Plugin Imports / check (push) Has been cancelled
refactor(i-g): BUG-018 God-Object Split 2 — dms/routes.py von 1492 auf 650 Zeilen (-56%)
- common.py neu: alle Safety-/Storage-Helper und Konstanten (exakte Original-Implementierung)
- folders_routes.py / sharing_routes.py / search_bulk_routes.py je eigener Router ohne Prefix
- routes.py: File-Lifecycle-Kern bleibt physisch (MAX_FILE_SIZE-Test-Patch-Semantik), Rest als Re-Export-Fassade + include_router x3
- Beweis: DMS-Suite 129 Tests = 125 passed + 4 identische Vorbestand-Failures (Baseline-Referenz 1:1), 20/20 Routen via Router-Introspection, ruff clean
2026-08-26 22:02:32 +02:00

224 lines
7.1 KiB
Python

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