"""DMS Bridge — integrates with the DMS plugin for file storage.""" from __future__ import annotations import logging import os import uuid from typing import Any from fastapi import UploadFile from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.plugins.builtins.dms.models import File as DmsFile, Folder logger = logging.getLogger(__name__) DMS_STORAGE_BASE = os.environ.get("DMS_STORAGE_BASE", "/tmp/dms") COMM_FOLDER_NAME = "_kommunikation" MAX_DIRECT_UPLOAD = 100 * 1024 * 1024 # 100 MB — larger files must be DMS references class DmsBridge: """Bridge to the DMS plugin for attachment storage.""" @staticmethod async def ensure_comm_folder( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, ) -> Folder: """Ensure the _kommunikation root folder exists in DMS.""" result = await db.execute( select(Folder).where( Folder.name == COMM_FOLDER_NAME, Folder.parent_id.is_(None), Folder.tenant_id == tenant_id, Folder.deleted_at.is_(None), ) ) folder = result.scalar_one_or_none() if folder is None: folder = Folder( tenant_id=tenant_id, name=COMM_FOLDER_NAME, parent_id=None, created_by=user_id, ) db.add(folder) await db.flush() return folder @staticmethod async def ensure_conversation_folder( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, conversation_id: uuid.UUID, ) -> Folder: """Ensure a sub-folder for a specific conversation exists.""" root_folder = await DmsBridge.ensure_comm_folder(db, tenant_id, user_id) conv_name = str(conversation_id) result = await db.execute( select(Folder).where( Folder.name == conv_name, Folder.parent_id == root_folder.id, Folder.tenant_id == tenant_id, Folder.deleted_at.is_(None), ) ) folder = result.scalar_one_or_none() if folder is None: folder = Folder( tenant_id=tenant_id, name=conv_name, parent_id=root_folder.id, created_by=user_id, ) db.add(folder) await db.flush() return folder @staticmethod async def store_attachment( db: AsyncSession, tenant_id: uuid.UUID, conversation_id: uuid.UUID, user_id: uuid.UUID, file: UploadFile, ) -> dict[str, Any]: """Store an uploaded file in the DMS under _kommunikation/{conversation_id}/. Returns dict with file_id, file_name, file_type, file_size. """ # Check size limit content = await file.read() file_size = len(content) if file_size > MAX_DIRECT_UPLOAD: raise ValueError( f"File size {file_size} exceeds direct upload limit ({MAX_DIRECT_UPLOAD} bytes). " f"Use DMS reference instead." ) # Ensure conversation folder folder = await DmsBridge.ensure_conversation_folder( db, tenant_id, user_id, conversation_id ) # Save file to disk file_id = uuid.uuid4() file_ext = os.path.splitext(file.filename or "")[1] or "" storage_path = os.path.join( DMS_STORAGE_BASE, str(tenant_id), str(folder.id), f"{file_id}{file_ext}", ) os.makedirs(os.path.dirname(storage_path), exist_ok=True) with open(storage_path, "wb") as f: f.write(content) # Create DMS file record dms_file = DmsFile( tenant_id=tenant_id, name=file.filename or f"{file_id}", folder_id=folder.id, uploaded_by=user_id, mime_type=file.content_type or "application/octet-stream", size_bytes=file_size, storage_path=storage_path, ) db.add(dms_file) await db.flush() return { "file_id": str(dms_file.id), "file_name": dms_file.name, "file_type": dms_file.mime_type, "file_size": dms_file.size_bytes, "file_source": "comm", } @staticmethod async def reference_external_file( db: AsyncSession, tenant_id: uuid.UUID, file_id: uuid.UUID, ) -> dict[str, Any] | None: """Reference an existing DMS file without copying it. Returns dict with file metadata or None if file not found. """ result = await db.execute( select(DmsFile).where( DmsFile.id == file_id, DmsFile.tenant_id == tenant_id, DmsFile.deleted_at.is_(None), ) ) dms_file = result.scalar_one_or_none() if dms_file is None: return None return { "file_id": str(dms_file.id), "file_name": dms_file.name, "file_type": dms_file.mime_type, "file_size": dms_file.size_bytes, "file_source": "dms", } @staticmethod async def get_file( db: AsyncSession, tenant_id: uuid.UUID, file_id: uuid.UUID, ) -> DmsFile | None: """Get a DMS file by ID.""" result = await db.execute( select(DmsFile).where( DmsFile.id == file_id, DmsFile.tenant_id == tenant_id, DmsFile.deleted_at.is_(None), ) ) return result.scalar_one_or_none()