e0255412ac
Check Cross-Plugin Imports / check (push) Has been cancelled
1. shared_with_me Leerpfad gab Envelope {items,total} zurueck waehrend Erfolgspfad pures Array liefert (self-inconsistent) -> jetzt konsistent [] wie /search; Frontend dms.ts vertraegt beide Shapes
2+3. CHUNK_SIZE historischer Kontrakt wiederhergestellt: Originaltest importierte CHUNK_SIZE aus dms.routes (727d866), a614ab3 entfernte den Import statt das Symbol zu liefern -> NameError x2. Jetzt: oeffentliche Konstante in common.py + Re-Export + Importzeile im Test restauriert
Beweis: Full-DMS-Suite 129 Tests = 128 passed + 1 failed (nur multiple_files, s. Follow-up) vs Baseline 125+4
224 lines
7.1 KiB
Python
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 []
|
|
|
|
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,
|
|
}
|