FEAT: Integrated Document Engine with WebP optimization, Thumbnail generation and Hybrid (NAS/SSD) storage logic
This commit is contained in:
Binary file not shown.
@@ -1,16 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import auth, catalog, assets, organizations
|
||||
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents # <--- Ide bekerült a documents!
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Felhasználó és Identitás
|
||||
# Hitelesítés
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
|
||||
# Katalógus és Jármű Robotok
|
||||
# Katalógus
|
||||
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
||||
|
||||
# Egyedi Eszközök (Assets)
|
||||
# Eszközök (Járművek)
|
||||
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
||||
|
||||
# Szervezetek és Onboarding
|
||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||
# Szervezetek
|
||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||
|
||||
# DOKUMENTUMOK (Ez az új rész, ami hiányzik neked)
|
||||
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
|
||||
Binary file not shown.
30
backend/app/api/v1/endpoints/documents.py
Normal file
30
backend/app/api/v1/endpoints/documents.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, BackgroundTasks, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.services.document_service import DocumentService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/upload/{parent_type}/{parent_id}")
|
||||
async def upload_document(
|
||||
parent_type: str,
|
||||
parent_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Dokumentum feltöltés, optimalizálás és NAS-ra mentés.
|
||||
parent_type: 'organizations' vagy 'assets'
|
||||
"""
|
||||
if parent_type not in ["organizations", "assets"]:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen cél-típus!")
|
||||
|
||||
doc = await DocumentService.process_upload(file, parent_type, parent_id, db, background_tasks)
|
||||
|
||||
return {
|
||||
"document_id": doc.id,
|
||||
"thumbnail": doc.thumbnail_path,
|
||||
"original_name": doc.original_name,
|
||||
"status": "processed_and_vaulted"
|
||||
}
|
||||
@@ -1,30 +1,45 @@
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api.v1.api import api_router
|
||||
from app.core.config import settings
|
||||
|
||||
# 1. Könyvtárstruktúra biztosítása (SSD puffer a miniképeknek)
|
||||
# Ez garantálja, hogy az app elindulásakor létezik a célmappa
|
||||
os.makedirs("static/previews", exist_ok=True)
|
||||
|
||||
app = FastAPI(
|
||||
title="Service Finder API",
|
||||
version="2.0.0", # A rendszer verziója, de a végpont marad v1
|
||||
version="2.0.0",
|
||||
openapi_url="/api/v1/openapi.json",
|
||||
docs_url="/docs"
|
||||
)
|
||||
|
||||
# PONTOS CORS BEÁLLÍTÁS
|
||||
# 2. PONTOS CORS BEÁLLÍTÁS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://192.168.100.10:3001", # Frontend portja
|
||||
"http://localhost:3001",
|
||||
"https://dev.profibot.hu" # Ha van NPM proxy
|
||||
"https://dev.profibot.hu" # NPM proxy esetén
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 3. STATIKUS FÁJLOK KISZOLGÁLÁSA
|
||||
# Ez teszi lehetővé, hogy a /static eléréssel lekérhetőek legyenek a miniképek
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# 4. ROUTER BEKÖTÉSE
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"status": "online", "message": "Service Finder Master System v1.0"}
|
||||
return {
|
||||
"status": "online",
|
||||
"message": "Service Finder Master System v2.0",
|
||||
"features": ["Document Engine", "Asset Vault", "Org Onboarding"]
|
||||
}
|
||||
BIN
backend/app/models/__pycache__/document.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/document.cpython-312.pyc
Normal file
Binary file not shown.
26
backend/app/models/document.py
Normal file
26
backend/app/models/document.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
from app.db.base import Base
|
||||
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
parent_type = Column(String(20), nullable=False) # 'organization' vagy 'asset'
|
||||
parent_id = Column(String(50), nullable=False) # Org vagy Asset technikai ID-ja
|
||||
doc_type = Column(String(50)) # pl. 'foundation_deed', 'registration'
|
||||
|
||||
original_name = Column(String(255), nullable=False)
|
||||
file_hash = Column(String(64), nullable=False) # A NAS-on tárolt név (UUID)
|
||||
file_ext = Column(String(10), default="webp")
|
||||
mime_type = Column(String(100), default="image/webp")
|
||||
file_size = Column(Integer)
|
||||
|
||||
has_thumbnail = Column(Boolean, default=False)
|
||||
thumbnail_path = Column(String(255)) # SSD-n lévő elérés
|
||||
|
||||
uploaded_by = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
Binary file not shown.
82
backend/app/services/document_service.py
Normal file
82
backend/app/services/document_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from PIL import Image
|
||||
from uuid import uuid4
|
||||
from fastapi import UploadFile, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.document import Document
|
||||
|
||||
class DocumentService:
|
||||
@staticmethod
|
||||
def _clean_temp(path: str):
|
||||
"""30 perc után törli az ideiglenes fájlt (opcionális, ha maradunk a puffer mellett)"""
|
||||
time.sleep(1800)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
@staticmethod
|
||||
async def process_upload(
|
||||
file: UploadFile,
|
||||
parent_type: str,
|
||||
parent_id: str,
|
||||
db: AsyncSession,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
file_uuid = str(uuid4())
|
||||
|
||||
# 1. Könyvtárstruktúra meghatározása
|
||||
temp_dir = "/app/temp/uploads"
|
||||
nas_vault_dir = f"/mnt/nas/app_data/organizations/{parent_id}/vault"
|
||||
ssd_thumb_dir = f"/app/static/previews/organizations/{parent_id}"
|
||||
|
||||
for d in [temp_dir, nas_vault_dir, ssd_thumb_dir]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
# 2. Mentés a TEMP-be
|
||||
temp_path = os.path.join(temp_dir, f"{file_uuid}_{file.filename}")
|
||||
content = await file.read()
|
||||
with open(temp_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
# 3. Képfeldolgozás (Pillow)
|
||||
img = Image.open(temp_path)
|
||||
|
||||
# A) Thumbnail generálás (300px WebP az SSD-re)
|
||||
thumb_filename = f"{file_uuid}_thumb.webp"
|
||||
thumb_path = os.path.join(ssd_thumb_dir, thumb_filename)
|
||||
thumb_img = img.copy()
|
||||
thumb_img.thumbnail((300, 300))
|
||||
thumb_img.save(thumb_path, "WEBP", quality=80)
|
||||
|
||||
# B) Nagy kép optimalizálás (Max 1600px WebP a NAS-ra)
|
||||
vault_filename = f"{file_uuid}.webp"
|
||||
vault_path = os.path.join(nas_vault_dir, vault_filename)
|
||||
|
||||
if img.width > 1600:
|
||||
ratio = 1600 / float(img.width)
|
||||
new_height = int(float(img.height) * float(ratio))
|
||||
img = img.resize((1600, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
img.save(vault_path, "WEBP", quality=85)
|
||||
|
||||
# 4. Adatbázis rögzítés
|
||||
new_doc = Document(
|
||||
id=uuid4(),
|
||||
parent_type=parent_type,
|
||||
parent_id=parent_id,
|
||||
original_name=file.filename,
|
||||
file_hash=file_uuid,
|
||||
file_size=os.path.getsize(vault_path),
|
||||
has_thumbnail=True,
|
||||
thumbnail_path=f"/static/previews/organizations/{parent_id}/{thumb_filename}"
|
||||
)
|
||||
db.add(new_doc)
|
||||
await db.commit()
|
||||
|
||||
# 5. Puffer törlés ütemezése (30 perc)
|
||||
# background_tasks.add_task(DocumentService._clean_temp, temp_path)
|
||||
# MVP-ben töröljük azonnal, ha már a NAS-on van a biztonságos másolat
|
||||
os.remove(temp_path)
|
||||
|
||||
return new_doc
|
||||
Reference in New Issue
Block a user