ifixkart-backend/Backend/app/api/v1/routers/FileRouter.py

211 lines
7.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status
from sqlalchemy.orm import Session
from typing import List
import ulid
import os
from app.core.database.db_session import get_db
from app.repositories.file_repository import file_repository
from app.models.FileUploadModel import FileUpload
from app.schemas.File import FileUploadResponse
from app.storage.local_provider import LocalStorageProvider
from app.models.UserModel import User
from app.core.permissions.RoleChecker import get_current_user
import blurhash
router = APIRouter(prefix="/api/v1/files", tags=["File Ingestion Services"])
# Initialize Local Storage driver
storage_driver = LocalStorageProvider(base_upload_dir="uploads")
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf", ".doc", ".docx", ".zip"}
from app.core.media.media_garbage_collector import get_media_settings, attach_file
from starlette.concurrency import run_in_threadpool
from PIL import Image as PILImage
import io
from pathlib import Path
BACKEND_ROOT = Path(__file__).resolve().parents[4]
UPLOADS_DIR = BACKEND_ROOT / "uploads"
RAW_UPLOADS_DIR = UPLOADS_DIR / "raw"
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf", ".doc", ".docx", ".zip"}
@router.post("/upload", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED)
async def upload_file(
file: UploadFile = File(...),
entity_type: str = Form(...),
entity_id: str = Form(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# 1. Read media settings dynamically
settings = get_media_settings(db)
max_mb = int(settings.get("media_max_size_mb", 10))
max_bytes = max_mb * 1024 * 1024
store_raw = bool(settings.get("media_store_original", True))
webp_quality = int(settings.get("media_webp_quality", 88))
# 2. Validate file extension
_, ext = os.path.splitext(file.filename or "")
ext = ext.lower()
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(status_code=400, detail=f"File extension '{ext}' is not allowed.")
# 3. Read bytes and enforce size limit
file_bytes = await file.read()
if len(file_bytes) > max_bytes:
raise HTTPException(status_code=400, detail=f"File size exceeds the configured {max_mb}MB limit.")
# 4. Generate 26-char canonical ULID
file_id = str(ulid.ULID())
folder_path = UPLOADS_DIR / entity_type / entity_id
folder_path.mkdir(parents=True, exist_ok=True)
RAW_UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
raw_path_rel = None
webp_path_rel = None
thumb_path_rel = None
med_path_rel = None
large_path_rel = None
blur_hash_val = None
# 5. Process Image Optimization
if ext in (".jpg", ".jpeg", ".png", ".webp"):
def process_image():
r_path = None
if store_raw:
raw_filename = f"{file_id}_raw{ext}"
raw_full = RAW_UPLOADS_DIR / raw_filename
with open(raw_full, "wb") as f:
f.write(file_bytes)
r_path = f"/uploads/raw/{raw_filename}"
img = PILImage.open(io.BytesIO(file_bytes))
if img.mode in ("RGBA", "P"):
img = img.convert("RGBA")
elif img.mode != "RGB":
img = img.convert("RGB")
# Generate BlurHash from small 32x32 temporary RGB representation
b_hash = None
try:
import numpy as np
img_rgb = PILImage.open(io.BytesIO(file_bytes)).convert("RGB")
temp_thumb = img_rgb.resize((32, 32))
b_hash = blurhash.encode(np.asarray(temp_thumb), 4, 3)
except Exception:
b_hash = None
max_dim = 2560
if img.size[0] > max_dim or img.size[1] > max_dim:
img.thumbnail((max_dim, max_dim), PILImage.Resampling.BILINEAR)
# Main WebP
webp_full = folder_path / f"{file_id}.webp"
bio = io.BytesIO()
img.save(bio, format="WEBP", quality=webp_quality)
with open(webp_full, "wb") as f:
f.write(bio.getvalue())
w_path = f"/uploads/{entity_type}/{entity_id}/{file_id}.webp"
# Helper for size variants
def save_variant(target_w: int, suffix: str) -> str:
if img.size[0] > target_w:
w_pct = target_w / float(img.size[0])
h_sz = int(float(img.size[1]) * float(w_pct))
r_img = img.resize((target_w, h_sz), PILImage.Resampling.BILINEAR)
else:
r_img = img
var_full = folder_path / f"{file_id}_{suffix}.webp"
v_bio = io.BytesIO()
r_img.save(v_bio, format="WEBP", quality=80)
with open(var_full, "wb") as f:
f.write(v_bio.getvalue())
return f"/uploads/{entity_type}/{entity_id}/{file_id}_{suffix}.webp"
t_path = save_variant(300, "thumbnail")
m_path = save_variant(800, "medium")
l_path = save_variant(1500, "large")
return {
"raw_path": r_path,
"webp_path": w_path,
"thumb_path": t_path,
"med_path": m_path,
"large_path": l_path,
"blur_hash": b_hash,
}
try:
res_dict = await run_in_threadpool(process_image)
raw_path_rel = res_dict["raw_path"]
webp_path_rel = res_dict["webp_path"]
thumb_path_rel = res_dict["thumb_path"]
med_path_rel = res_dict["med_path"]
large_path_rel = res_dict["large_path"]
blur_hash_val = res_dict["blur_hash"]
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to process and compress image: {str(exc)}")
else:
# Non-image files
stored_name = f"{file_id}{ext}"
stored_full = folder_path / stored_name
with open(stored_full, "wb") as f:
f.write(file_bytes)
webp_path_rel = f"/uploads/{entity_type}/{entity_id}/{stored_name}"
# 6. Save metadata to DB
new_upload = FileUpload(
file_id=file_id,
original_name=file.filename or "uploaded_file",
stored_name=f"{file_id}.webp",
mime_type=file.content_type or "application/octet-stream",
extension=ext.replace(".", ""),
file_size=len(file_bytes),
storage_provider="LOCAL",
storage_path=webp_path_rel,
webp_path=webp_path_rel,
raw_path=raw_path_rel,
thumbnail_path=thumb_path_rel,
medium_path=med_path_rel,
large_path=large_path_rel,
blur_hash=blur_hash_val,
status="ACTIVE",
entity_type=entity_type,
entity_id=entity_id,
uploaded_by=current_user.user_id
)
db.add(new_upload)
db.commit()
db.refresh(new_upload)
return new_upload
@router.get("/entity/{entity_type}/{entity_id}", response_model=List[FileUploadResponse])
def get_files_by_entity(
entity_type: str,
entity_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
return file_repository.get_by_entity(db, entity_type, entity_id)
@router.delete("/delete/{file_id}")
def delete_file(
file_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
upload_record = file_repository.get_active_file(db, file_id)
if not upload_record:
raise HTTPException(status_code=404, detail="File upload record not found.")
# Delete physical file from storage provider
storage_driver.delete_file(upload_record.storage_path)
# Mark deleted in DB (soft delete)
file_repository.mark_deleted(db, file_id)
return {"detail": "File deleted successfully"}