abinazebinoy commited on
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 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 backend.core.logger import setup_logger
 
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
- # Configure CORS (Cross-Origin Resource Sharing)
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 - API information."""
52
  return {
53
  "name": settings.API_TITLE,
54
  "version": settings.API_VERSION,
55
  "status": "operational",
56
- "docs": "/docs" # Swagger UI
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": "2025-02-12T21:30:00Z"
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"]