229 lines
11 KiB
Python
229 lines
11 KiB
Python
import re
|
|
import uuid
|
|
|
|
from fastapi import HTTPException, UploadFile, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.knowledge import KnowledgeDevice, KnowledgeDocument, KnowledgeManufacturer, KnowledgeNote
|
|
from app.repositories.knowledge_repository import KnowledgeRepository
|
|
from app.schemas.knowledge import (
|
|
KnowledgeDeviceCreate,
|
|
KnowledgeDeviceUpdate,
|
|
KnowledgeDocumentCreate,
|
|
KnowledgeDocumentUpdate,
|
|
KnowledgeManufacturerCreate,
|
|
KnowledgeManufacturerUpdate,
|
|
KnowledgeNoteCreate,
|
|
KnowledgeNoteUpdate,
|
|
)
|
|
from app.storage import get_storage_service
|
|
from app.storage.exceptions import StorageFileNotFoundError, StorageValidationError
|
|
|
|
|
|
def slugify(value: str) -> str:
|
|
normalized = value.strip().lower()
|
|
normalized = normalized.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
|
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
|
|
normalized = normalized.strip("-")
|
|
return normalized or uuid.uuid4().hex[:10]
|
|
|
|
|
|
def unique_slug(db: Session, base: str, exists) -> str:
|
|
slug = slugify(base)
|
|
candidate = slug
|
|
index = 2
|
|
while exists(db, candidate):
|
|
candidate = f"{slug}-{index}"
|
|
index += 1
|
|
return candidate
|
|
|
|
|
|
def parse_tags(value: str) -> list[str]:
|
|
seen: set[str] = set()
|
|
tags: list[str] = []
|
|
for item in value.split(","):
|
|
tag = item.strip()
|
|
key = tag.lower()
|
|
if tag and key not in seen:
|
|
seen.add(key)
|
|
tags.append(tag)
|
|
return tags
|
|
|
|
|
|
def storage_validation_error(exc: StorageValidationError) -> HTTPException:
|
|
detail = str(exc) or "Ungültige Datei"
|
|
status_code = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE if "groß" in detail else status.HTTP_400_BAD_REQUEST
|
|
return HTTPException(status_code=status_code, detail=detail)
|
|
|
|
|
|
class KnowledgeService:
|
|
@staticmethod
|
|
def create_manufacturer(db: Session, payload: KnowledgeManufacturerCreate) -> KnowledgeManufacturer:
|
|
if KnowledgeRepository.get_manufacturer_by_name(db, payload.name):
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Herstellername ist bereits vergeben")
|
|
slug = unique_slug(db, payload.name, lambda session, value: KnowledgeRepository.get_manufacturer_by_slug(session, value) is not None)
|
|
manufacturer = KnowledgeManufacturer(name=payload.name, slug=slug, website=str(payload.website or ""), notes=payload.notes)
|
|
db.add(manufacturer)
|
|
db.commit()
|
|
db.refresh(manufacturer)
|
|
return manufacturer
|
|
|
|
@staticmethod
|
|
def update_manufacturer(db: Session, manufacturer: KnowledgeManufacturer, payload: KnowledgeManufacturerUpdate) -> KnowledgeManufacturer:
|
|
if KnowledgeRepository.get_manufacturer_by_name(db, payload.name, manufacturer.id):
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Herstellername ist bereits vergeben")
|
|
manufacturer.name = payload.name
|
|
manufacturer.slug = unique_slug(
|
|
db,
|
|
payload.name,
|
|
lambda session, value: KnowledgeRepository.get_manufacturer_by_slug(session, value, manufacturer.id) is not None,
|
|
)
|
|
manufacturer.website = str(payload.website or "")
|
|
manufacturer.notes = payload.notes
|
|
db.commit()
|
|
db.refresh(manufacturer)
|
|
return manufacturer
|
|
|
|
@staticmethod
|
|
def create_device(db: Session, payload: KnowledgeDeviceCreate) -> KnowledgeDevice:
|
|
if KnowledgeRepository.get_manufacturer(db, payload.manufacturer_id) is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
|
|
slug = slugify(payload.model_number or payload.name)
|
|
if KnowledgeRepository.get_device_by_slug(db, manufacturer_id=payload.manufacturer_id, slug=slug):
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Geräte-Slug ist für diesen Hersteller bereits vergeben")
|
|
device = KnowledgeDevice(slug=slug, **payload.model_dump())
|
|
db.add(device)
|
|
db.commit()
|
|
return KnowledgeRepository.get_device(db, device.id) or device
|
|
|
|
@staticmethod
|
|
def update_device(db: Session, device: KnowledgeDevice, payload: KnowledgeDeviceUpdate) -> KnowledgeDevice:
|
|
if KnowledgeRepository.get_manufacturer(db, payload.manufacturer_id) is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
|
|
slug = slugify(payload.model_number or payload.name)
|
|
if KnowledgeRepository.get_device_by_slug(db, manufacturer_id=payload.manufacturer_id, slug=slug, exclude_id=device.id):
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Geräte-Slug ist für diesen Hersteller bereits vergeben")
|
|
for key, value in payload.model_dump().items():
|
|
setattr(device, key, value)
|
|
device.slug = slug
|
|
db.commit()
|
|
return KnowledgeRepository.get_device(db, device.id) or device
|
|
|
|
@staticmethod
|
|
def create_document(db: Session, payload: KnowledgeDocumentCreate) -> KnowledgeDocument:
|
|
KnowledgeService._validate_document_links(db, payload.manufacturer_id, payload.device_id)
|
|
slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value) is not None)
|
|
document = KnowledgeDocument(slug=slug, external_url=str(payload.external_url or ""), **payload.model_dump(exclude={"external_url"}))
|
|
db.add(document)
|
|
db.commit()
|
|
return KnowledgeRepository.get_document(db, document.id) or document
|
|
|
|
@staticmethod
|
|
async def upload_document(
|
|
db: Session,
|
|
*,
|
|
file: UploadFile,
|
|
payload: KnowledgeDocumentCreate,
|
|
) -> KnowledgeDocument:
|
|
KnowledgeService._validate_document_links(db, payload.manufacturer_id, payload.device_id)
|
|
storage_service = get_storage_service()
|
|
max_bytes = storage_service.max_upload_mb * 1024 * 1024
|
|
content = await file.read(max_bytes + 1)
|
|
original_name = file.filename or ""
|
|
mime_type = file.content_type
|
|
try:
|
|
metadata = storage_service.save_file(
|
|
namespace=f"knowledge/documents/{payload.manufacturer_id}",
|
|
content=content,
|
|
original_filename=original_name,
|
|
mime_type=mime_type,
|
|
)
|
|
except StorageValidationError as exc:
|
|
raise storage_validation_error(exc) from exc
|
|
slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value) is not None)
|
|
document = KnowledgeDocument(
|
|
slug=slug,
|
|
external_url=str(payload.external_url or ""),
|
|
file_name=metadata.original_filename,
|
|
file_path=metadata.storage_key,
|
|
mime_type=metadata.mime_type,
|
|
file_size=metadata.size,
|
|
checksum_sha256=metadata.checksum_sha256,
|
|
**payload.model_dump(exclude={"external_url"}),
|
|
)
|
|
db.add(document)
|
|
db.commit()
|
|
return KnowledgeRepository.get_document(db, document.id) or document
|
|
|
|
@staticmethod
|
|
def update_document(db: Session, document: KnowledgeDocument, payload: KnowledgeDocumentUpdate) -> KnowledgeDocument:
|
|
KnowledgeService._validate_document_links(db, payload.manufacturer_id, payload.device_id)
|
|
slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value, document.id) is not None)
|
|
for key, value in payload.model_dump(exclude={"external_url"}).items():
|
|
setattr(document, key, value)
|
|
document.external_url = str(payload.external_url or "")
|
|
document.slug = slug
|
|
db.commit()
|
|
return KnowledgeRepository.get_document(db, document.id) or document
|
|
|
|
@staticmethod
|
|
def create_note(db: Session, payload: KnowledgeNoteCreate) -> KnowledgeNote:
|
|
KnowledgeService._validate_optional_links(db, payload.manufacturer_id, payload.device_id)
|
|
note = KnowledgeNote(**payload.model_dump())
|
|
db.add(note)
|
|
db.commit()
|
|
return KnowledgeRepository.get_note(db, note.id) or note
|
|
|
|
@staticmethod
|
|
def open_document_file(document: KnowledgeDocument):
|
|
if not document.file_path:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument hat keine lokale Datei")
|
|
try:
|
|
return get_storage_service().open_file(document.file_path)
|
|
except StorageFileNotFoundError as exc:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datei nicht gefunden") from exc
|
|
except StorageValidationError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Ungültiger Dateipfad") from exc
|
|
|
|
@staticmethod
|
|
def delete_document_file(document: KnowledgeDocument) -> None:
|
|
if not document.file_path:
|
|
return
|
|
KnowledgeService.delete_storage_key(document.file_path)
|
|
|
|
@staticmethod
|
|
def delete_storage_key(storage_key: str) -> None:
|
|
try:
|
|
get_storage_service().delete_file(storage_key)
|
|
except StorageValidationError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Ungültiger Dateipfad") from exc
|
|
|
|
@staticmethod
|
|
def update_note(db: Session, note: KnowledgeNote, payload: KnowledgeNoteUpdate) -> KnowledgeNote:
|
|
KnowledgeService._validate_optional_links(db, payload.manufacturer_id, payload.device_id)
|
|
for key, value in payload.model_dump().items():
|
|
setattr(note, key, value)
|
|
db.commit()
|
|
return KnowledgeRepository.get_note(db, note.id) or note
|
|
|
|
@staticmethod
|
|
def _validate_document_links(db: Session, manufacturer_id: int, device_id: int | None) -> None:
|
|
if KnowledgeRepository.get_manufacturer(db, manufacturer_id) is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
|
|
if device_id is not None:
|
|
device = KnowledgeRepository.get_device(db, device_id)
|
|
if device is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
|
|
if device.manufacturer_id != manufacturer_id:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Gerät gehört nicht zum Hersteller")
|
|
|
|
@staticmethod
|
|
def _validate_optional_links(db: Session, manufacturer_id: int | None, device_id: int | None) -> None:
|
|
if manufacturer_id is not None and KnowledgeRepository.get_manufacturer(db, manufacturer_id) is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
|
|
if device_id is not None:
|
|
device = KnowledgeRepository.get_device(db, device_id)
|
|
if device is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
|
|
if manufacturer_id is not None and device.manufacturer_id != manufacturer_id:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Gerät gehört nicht zum Hersteller")
|