Files
cvfs/apps/backend/fastapi/app/services/documents.py
Claude aa419cde0d Prepare repository for public deployment
- Replace ReportLab PDF export with LibreOffice headless for proper DOCX formatting preservation
- Add libreoffice-writer + fonts-liberation to backend Dockerfile
- Proxy public CV PDFs through frontend (/cv/[slug]) instead of redirecting to MinIO storage directly
- Fix docker-compose: route backend/worker to internal MinIO URL (http://cvfs-minio:9000), remove MinIO from public network, parameterize all domain/env vars
- Add storage cleanup (MinIO artifact deletion) when a document is deleted
- Add docker-compose.standalone.yml for self-deployment without Traefik/dokploy
- Update .env.example with comprehensive self-deployment documentation

https://claude.ai/code/session_017HGM9VPptZG52asT5pbL6Y
2026-04-04 10:06:20 +00:00

107 lines
3.2 KiB
Python

from __future__ import annotations
from fastapi import UploadFile
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from dlib.cv import parse_docx_bytes
from app.models import CvDocument, CvVersion, PublicAsset
from app.services.storage import persist_upload, storage_client
async def create_document(
session: AsyncSession,
*,
owner_id: str,
title: str,
description: str | None,
upload: UploadFile,
) -> CvDocument:
artifact_key, file_bytes = await persist_upload(upload, owner_id)
structured = parse_docx_bytes(file_bytes, version_label="root")
doc = CvDocument(owner_id=owner_id, title=title, description=description)
session.add(doc)
await session.flush() # persist doc so version FK is satisfied
version = CvVersion(
document_id=doc.id,
branch_name="root",
version_label="root",
artifact_docx_key=artifact_key,
structured_blocks=[block.model_dump() for block in structured.blocks],
metadata_json={"ingested": True},
)
session.add(version)
await session.flush() # persist version so root_version_id FK is satisfied
doc.root_version_id = version.id
await session.commit()
stmt = (
select(CvDocument)
.where(CvDocument.id == doc.id)
.options(
selectinload(CvDocument.versions).options(
selectinload(CvVersion.patches),
selectinload(CvVersion.public_assets),
)
)
)
result = await session.execute(stmt)
return result.scalars().unique().one()
async def list_documents(session: AsyncSession, owner_id: str) -> list[CvDocument]:
stmt = (
select(CvDocument)
.where(CvDocument.owner_id == owner_id)
.options(
selectinload(CvDocument.versions).options(
selectinload(CvVersion.patches),
selectinload(CvVersion.public_assets),
)
)
.order_by(CvDocument.created_at.desc())
)
result = await session.execute(stmt)
return result.scalars().unique().all()
async def get_document(
session: AsyncSession, owner_id: str, document_id: str
) -> CvDocument | None:
stmt = (
select(CvDocument)
.where(CvDocument.id == document_id, CvDocument.owner_id == owner_id)
.options(
selectinload(CvDocument.versions).options(
selectinload(CvVersion.patches),
selectinload(CvVersion.public_assets),
)
)
)
result = await session.execute(stmt)
return result.scalars().unique().one_or_none()
async def delete_document(
session: AsyncSession, owner_id: str, document_id: str
) -> bool:
doc = await get_document(session, owner_id, document_id)
if not doc:
return False
artifact_keys = {v.artifact_docx_key for v in doc.versions if v.artifact_docx_key}
version_ids = [v.id for v in doc.versions]
if version_ids:
await session.execute(
delete(PublicAsset).where(PublicAsset.version_id.in_(version_ids))
)
await session.delete(doc)
await session.commit()
for key in artifact_keys:
storage_client.delete_object(key=key)
return True