88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""File upload, listing, and removal for a session."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
|
from sqlalchemy.orm import Session as OrmSession
|
|
|
|
from ...config import ALLOWED_EXTENSIONS, MAX_UPLOAD_BYTES, UPLOAD_DIR
|
|
from ...core.xlsx_reader import TransactionReader, ParseError
|
|
from ...db import models
|
|
from ..deps import db_dep, file_dict, get_session_or_404, sanitize_filename
|
|
|
|
router = APIRouter(prefix="/api/sessions", tags=["files"])
|
|
|
|
|
|
@router.get("/{session_id}/files")
|
|
def list_files(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
|
|
get_session_or_404(session_id, db)
|
|
rows = db.query(models.SessionFile).filter(models.SessionFile.session_id == session_id).all()
|
|
return [file_dict(f) for f in rows]
|
|
|
|
|
|
@router.post("/{session_id}/files")
|
|
async def upload_files(session_id: int, files: list[UploadFile] = File(...),
|
|
db: OrmSession = Depends(db_dep)) -> list[dict]:
|
|
session = get_session_or_404(session_id, db)
|
|
dest_dir = UPLOAD_DIR / f"session_{session_id}"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
out = []
|
|
for uf in files:
|
|
safe = sanitize_filename(uf.filename or "upload.xlsx")
|
|
ext = os.path.splitext(safe)[1].lower()
|
|
if ext not in ALLOWED_EXTENSIONS:
|
|
raise HTTPException(400, f"Unsupported file type: {safe} ({ext})")
|
|
path = dest_dir / safe
|
|
h = hashlib.sha256()
|
|
size = 0
|
|
with open(path, "wb") as fh:
|
|
while True:
|
|
chunk = await uf.read(1 << 20)
|
|
if not chunk:
|
|
break
|
|
size += len(chunk)
|
|
if size > MAX_UPLOAD_BYTES:
|
|
fh.close()
|
|
os.remove(path)
|
|
raise HTTPException(413, f"File too large: {safe}")
|
|
h.update(chunk)
|
|
fh.write(chunk)
|
|
rec = models.SessionFile(
|
|
session_id=session_id, filename=safe, stored_path=str(path),
|
|
size_bytes=size, sha256=h.hexdigest(), status="uploaded",
|
|
)
|
|
# light validation: detect sheet + required columns (no full row scan)
|
|
try:
|
|
reader = TransactionReader(str(path))
|
|
reader.detect()
|
|
rec.data_sheet = reader.sheet_name
|
|
rec.status = "invalid" if reader.column_mapping.missing_required else "parsed"
|
|
if reader.column_mapping.missing_required:
|
|
rec.message = f"missing required columns: {reader.column_mapping.missing_required}"
|
|
reader.close()
|
|
except ParseError as e:
|
|
rec.status = "invalid"
|
|
rec.message = str(e)
|
|
db.add(rec)
|
|
out.append(rec)
|
|
session.status = "draft"
|
|
db.commit()
|
|
return [file_dict(f) for f in out]
|
|
|
|
|
|
@router.delete("/{session_id}/files/{file_id}")
|
|
def delete_file(session_id: int, file_id: int, db: OrmSession = Depends(db_dep)) -> dict:
|
|
f = db.get(models.SessionFile, file_id)
|
|
if not f or f.session_id != session_id:
|
|
raise HTTPException(404, "File not found")
|
|
try:
|
|
if f.stored_path and os.path.exists(f.stored_path):
|
|
os.remove(f.stored_path)
|
|
except OSError:
|
|
pass
|
|
db.delete(f)
|
|
db.commit()
|
|
return {"deleted": file_id}
|