Spaces:
Running
Running
Commit ยท
7b588b2
1
Parent(s): f9ffb3e
Add file upload validation endpoint (#5)
Browse files- Create modular router structure (api/routes/upload.py)
- Implement /api/v1/upload/validate endpoint
- Add Pydantic schemas for request/response validation
- Include router in main FastAPI app
- Add comprehensive endpoint tests
Features:
- Async file handling for concurrent uploads
- In-memory processing (zero disk storage)
- Proper error handling and logging
- Auto-generated API documentation
Closes #5
- backend/api/routes/__init__.py +1 -0
- backend/api/routes/upload.py +73 -0
- backend/main.py +13 -21
- backend/models/schemas.py +23 -0
- backend/tests/test_upload.py +40 -0
backend/api/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API routes package."""
|
backend/api/routes/upload.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
File upload endpoints.
|
| 3 |
+
Why: Separate routing from main.py for scalability.
|
| 4 |
+
"""
|
| 5 |
+
from fastapi import APIRouter, UploadFile, File, HTTPException
|
| 6 |
+
from backend.utils.validators import validate_file, FileValidationError
|
| 7 |
+
from backend.models.schemas import FileValidationResponse, ErrorResponse
|
| 8 |
+
from backend.core.logger import setup_logger
|
| 9 |
+
|
| 10 |
+
logger = setup_logger(__name__)
|
| 11 |
+
|
| 12 |
+
# Create router (will be included in main.py)
|
| 13 |
+
router = APIRouter(
|
| 14 |
+
prefix="/api/v1/upload",
|
| 15 |
+
tags=["File Upload"]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@router.post(
|
| 20 |
+
"/validate",
|
| 21 |
+
response_model=FileValidationResponse,
|
| 22 |
+
responses={400: {"model": ErrorResponse}},
|
| 23 |
+
summary="Validate uploaded file",
|
| 24 |
+
description="""
|
| 25 |
+
Validates file type and size without processing.
|
| 26 |
+
|
| 27 |
+
**Privacy:** File is read into memory and immediately discarded.
|
| 28 |
+
No files are stored on disk.
|
| 29 |
+
|
| 30 |
+
**Supported types:**
|
| 31 |
+
- Images: JPEG, PNG, WebP
|
| 32 |
+
- Videos: MP4, MPEG
|
| 33 |
+
- Documents: PDF
|
| 34 |
+
|
| 35 |
+
**Max size:** 50MB
|
| 36 |
+
"""
|
| 37 |
+
)
|
| 38 |
+
async def validate_upload(
|
| 39 |
+
file: UploadFile = File(..., description="File to validate")
|
| 40 |
+
):
|
| 41 |
+
"""
|
| 42 |
+
Validate uploaded file.
|
| 43 |
+
|
| 44 |
+
Why async?
|
| 45 |
+
- Non-blocking file read
|
| 46 |
+
- Handles multiple concurrent uploads
|
| 47 |
+
- FastAPI best practice
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
# Read file into memory
|
| 51 |
+
file_bytes = await file.read()
|
| 52 |
+
logger.info(f"Received file: {file.filename}, size: {len(file_bytes)} bytes")
|
| 53 |
+
|
| 54 |
+
# Validate
|
| 55 |
+
result = validate_file(file_bytes, file.filename)
|
| 56 |
+
|
| 57 |
+
logger.info(f"Validation successful: {result}")
|
| 58 |
+
return FileValidationResponse(**result)
|
| 59 |
+
|
| 60 |
+
except FileValidationError as e:
|
| 61 |
+
logger.warning(f"Validation failed for {file.filename}: {str(e)}")
|
| 62 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Unexpected error validating {file.filename}: {str(e)}")
|
| 66 |
+
raise HTTPException(
|
| 67 |
+
status_code=500,
|
| 68 |
+
detail="Internal server error during file validation"
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
finally:
|
| 72 |
+
# Ensure file is closed (privacy - no disk storage)
|
| 73 |
+
await file.close()
|
backend/main.py
CHANGED
|
@@ -1,34 +1,26 @@
|
|
| 1 |
"""
|
| 2 |
VeriFile-X API - Privacy-preserving digital forensics platform.
|
| 3 |
-
|
| 4 |
-
Architecture:
|
| 5 |
-
- No file storage (all processing in-memory)
|
| 6 |
-
- Modular services for different analysis types
|
| 7 |
-
- Type-safe request/response models
|
| 8 |
"""
|
| 9 |
from fastapi import FastAPI
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from contextlib import asynccontextmanager
|
| 12 |
-
from
|
|
|
|
| 13 |
from backend.core.config import settings
|
|
|
|
|
|
|
| 14 |
|
| 15 |
logger = setup_logger(__name__)
|
| 16 |
|
| 17 |
|
| 18 |
@asynccontextmanager
|
| 19 |
async def lifespan(app: FastAPI):
|
| 20 |
-
"""
|
| 21 |
-
Startup and shutdown events.
|
| 22 |
-
Why: Resource initialization (models, connections) and cleanup.
|
| 23 |
-
"""
|
| 24 |
logger.info("๐ VeriFile-X API starting up...")
|
| 25 |
-
# TODO Phase 5: Load AI models here
|
| 26 |
yield
|
| 27 |
logger.info("๐ VeriFile-X API shutting down...")
|
| 28 |
-
# TODO: Cleanup resources
|
| 29 |
|
| 30 |
|
| 31 |
-
# Initialize FastAPI application
|
| 32 |
app = FastAPI(
|
| 33 |
title=settings.API_TITLE,
|
| 34 |
version=settings.API_VERSION,
|
|
@@ -36,7 +28,7 @@ app = FastAPI(
|
|
| 36 |
lifespan=lifespan,
|
| 37 |
)
|
| 38 |
|
| 39 |
-
#
|
| 40 |
app.add_middleware(
|
| 41 |
CORSMiddleware,
|
| 42 |
allow_origins=settings.CORS_ORIGINS,
|
|
@@ -45,26 +37,26 @@ app.add_middleware(
|
|
| 45 |
allow_headers=["*"],
|
| 46 |
)
|
| 47 |
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
@app.get("/")
|
| 50 |
async def root():
|
| 51 |
-
"""Root endpoint
|
| 52 |
return {
|
| 53 |
"name": settings.API_TITLE,
|
| 54 |
"version": settings.API_VERSION,
|
| 55 |
"status": "operational",
|
| 56 |
-
"docs": "/docs"
|
| 57 |
}
|
| 58 |
|
| 59 |
|
| 60 |
@app.get("/health")
|
| 61 |
async def health_check():
|
| 62 |
-
"""
|
| 63 |
-
Health check endpoint for monitoring.
|
| 64 |
-
Why: Deployment platforms (Render, AWS) ping this to verify service is alive.
|
| 65 |
-
"""
|
| 66 |
return {
|
| 67 |
"status": "healthy",
|
| 68 |
"debug_mode": settings.DEBUG,
|
| 69 |
-
"timestamp":
|
| 70 |
}
|
|
|
|
| 1 |
"""
|
| 2 |
VeriFile-X API - Privacy-preserving digital forensics platform.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
from fastapi import FastAPI
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
from contextlib import asynccontextmanager
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
from backend.core.config import settings
|
| 10 |
+
from backend.core.logger import setup_logger
|
| 11 |
+
from backend.api.routes import upload
|
| 12 |
|
| 13 |
logger = setup_logger(__name__)
|
| 14 |
|
| 15 |
|
| 16 |
@asynccontextmanager
|
| 17 |
async def lifespan(app: FastAPI):
|
| 18 |
+
"""Startup and shutdown events."""
|
|
|
|
|
|
|
|
|
|
| 19 |
logger.info("๐ VeriFile-X API starting up...")
|
|
|
|
| 20 |
yield
|
| 21 |
logger.info("๐ VeriFile-X API shutting down...")
|
|
|
|
| 22 |
|
| 23 |
|
|
|
|
| 24 |
app = FastAPI(
|
| 25 |
title=settings.API_TITLE,
|
| 26 |
version=settings.API_VERSION,
|
|
|
|
| 28 |
lifespan=lifespan,
|
| 29 |
)
|
| 30 |
|
| 31 |
+
# CORS
|
| 32 |
app.add_middleware(
|
| 33 |
CORSMiddleware,
|
| 34 |
allow_origins=settings.CORS_ORIGINS,
|
|
|
|
| 37 |
allow_headers=["*"],
|
| 38 |
)
|
| 39 |
|
| 40 |
+
# Include routers
|
| 41 |
+
app.include_router(upload.router)
|
| 42 |
+
|
| 43 |
|
| 44 |
@app.get("/")
|
| 45 |
async def root():
|
| 46 |
+
"""Root endpoint."""
|
| 47 |
return {
|
| 48 |
"name": settings.API_TITLE,
|
| 49 |
"version": settings.API_VERSION,
|
| 50 |
"status": "operational",
|
| 51 |
+
"docs": "/docs"
|
| 52 |
}
|
| 53 |
|
| 54 |
|
| 55 |
@app.get("/health")
|
| 56 |
async def health_check():
|
| 57 |
+
"""Health check endpoint."""
|
|
|
|
|
|
|
|
|
|
| 58 |
return {
|
| 59 |
"status": "healthy",
|
| 60 |
"debug_mode": settings.DEBUG,
|
| 61 |
+
"timestamp": datetime.now().isoformat()
|
| 62 |
}
|
backend/models/schemas.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Request/Response schemas for API endpoints.
|
| 3 |
+
Why: Type validation, auto-documentation, IDE support.
|
| 4 |
+
"""
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class FileValidationResponse(BaseModel):
|
| 10 |
+
"""Response model for file validation."""
|
| 11 |
+
valid: bool
|
| 12 |
+
mime_type: str
|
| 13 |
+
extension: str
|
| 14 |
+
size_bytes: int
|
| 15 |
+
size_mb: float
|
| 16 |
+
filename: str
|
| 17 |
+
message: Optional[str] = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ErrorResponse(BaseModel):
|
| 21 |
+
"""Standard error response."""
|
| 22 |
+
detail: str = Field(..., description="Error message")
|
| 23 |
+
error_type: Optional[str] = None
|
backend/tests/test_upload.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for file upload endpoint.
|
| 3 |
+
"""
|
| 4 |
+
import pytest
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_validate_upload_success(client, sample_image_bytes):
|
| 9 |
+
"""Test successful file validation."""
|
| 10 |
+
files = {"file": ("test.png", BytesIO(sample_image_bytes), "image/png")}
|
| 11 |
+
|
| 12 |
+
response = client.post("/api/v1/upload/validate", files=files)
|
| 13 |
+
|
| 14 |
+
assert response.status_code == 200
|
| 15 |
+
data = response.json()
|
| 16 |
+
assert data["valid"] is True
|
| 17 |
+
assert data["mime_type"] == "image/png"
|
| 18 |
+
assert data["filename"] == "test.png"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_validate_upload_invalid_type(client):
|
| 22 |
+
"""Test upload with invalid file type."""
|
| 23 |
+
files = {"file": ("test.txt", BytesIO(b"Plain text"), "text/plain")}
|
| 24 |
+
|
| 25 |
+
response = client.post("/api/v1/upload/validate", files=files)
|
| 26 |
+
|
| 27 |
+
assert response.status_code == 400
|
| 28 |
+
assert "not allowed" in response.json()["detail"]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_validate_upload_too_large(client):
|
| 32 |
+
"""Test upload exceeding size limit."""
|
| 33 |
+
# Create 60MB file
|
| 34 |
+
large_file = BytesIO(b"x" * (60 * 1024 * 1024))
|
| 35 |
+
files = {"file": ("huge.bin", large_file, "application/octet-stream")}
|
| 36 |
+
|
| 37 |
+
response = client.post("/api/v1/upload/validate", files=files)
|
| 38 |
+
|
| 39 |
+
assert response.status_code == 400
|
| 40 |
+
assert "exceeds limit" in response.json()["detail"]
|