diff --git a/backend/app/main.py b/backend/app/main.py index abf6c0d..7f273da 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -125,12 +125,103 @@ async def root(): # Include V2 API routers from app.routers import auth, tasks, admin +from fastapi import UploadFile, File, Depends, HTTPException, status +from sqlalchemy.orm import Session +import hashlib + +from app.core.deps import get_db, get_current_user +from app.models.user import User +from app.models.task import TaskFile +from app.schemas.task import UploadResponse, TaskStatusEnum +from app.services.task_service import task_service app.include_router(auth.router) app.include_router(tasks.router) app.include_router(admin.router) +# File upload endpoint +@app.post("/api/v2/upload", response_model=UploadResponse, tags=["Upload"], summary="Upload file for OCR") +async def upload_file( + file: UploadFile = File(..., description="File to upload (PNG, JPG, PDF, etc.)"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Upload a file for OCR processing + + Creates a new task and uploads the file + + - **file**: File to upload + """ + try: + # Validate file extension + file_ext = Path(file.filename).suffix.lower().lstrip('.') + if file_ext not in settings.allowed_extensions_list: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File type .{file_ext} not allowed. Allowed types: {', '.join(settings.allowed_extensions_list)}" + ) + + # Read file content + file_content = await file.read() + file_size = len(file_content) + + # Calculate file hash + file_hash = hashlib.sha256(file_content).hexdigest() + + # Create task + task = task_service.create_task( + db=db, + user_id=current_user.id, + filename=file.filename, + file_type=file.content_type + ) + + # Save file to disk + upload_dir = Path(settings.upload_dir) + upload_dir.mkdir(parents=True, exist_ok=True) + + # Create unique filename using task_id + unique_filename = f"{task.task_id}_{file.filename}" + file_path = upload_dir / unique_filename + + # Write file + with open(file_path, "wb") as f: + f.write(file_content) + + # Create TaskFile record + task_file = TaskFile( + task_id=task.id, + original_name=file.filename, + stored_path=str(file_path), + file_size=file_size, + mime_type=file.content_type, + file_hash=file_hash + ) + db.add(task_file) + db.commit() + + logger.info(f"Uploaded file {file.filename} ({file_size} bytes) for task {task.task_id}, user {current_user.email}") + + return { + "task_id": task.task_id, + "filename": file.filename, + "file_size": file_size, + "file_type": file.content_type or "application/octet-stream", + "status": TaskStatusEnum.PENDING + } + + except HTTPException: + raise + except Exception as e: + logger.exception(f"Failed to upload file for user {current_user.id}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to upload file: {str(e)}" + ) + + if __name__ == "__main__": import uvicorn diff --git a/backend/app/routers/tasks.py b/backend/app/routers/tasks.py index 9fe237a..fc304ab 100644 --- a/backend/app/routers/tasks.py +++ b/backend/app/routers/tasks.py @@ -5,14 +5,18 @@ Handles OCR task operations with user isolation import logging from typing import Optional +from pathlib import Path +import shutil +import hashlib -from fastapi import APIRouter, Depends, HTTPException, status, Query +from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app.core.deps import get_db, get_current_user +from app.core.config import settings from app.models.user import User -from app.models.task import TaskStatus +from app.models.task import TaskStatus, TaskFile from app.schemas.task import ( TaskCreate, TaskUpdate, @@ -21,6 +25,7 @@ from app.schemas.task import ( TaskListResponse, TaskStatsResponse, TaskStatusEnum, + UploadResponse, ) from app.services.task_service import task_service from app.services.file_access_service import file_access_service diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py index fca498b..8dc45e7 100644 --- a/backend/app/schemas/task.py +++ b/backend/app/schemas/task.py @@ -101,3 +101,19 @@ class TaskHistoryQuery(BaseModel): page_size: int = Field(default=50, ge=1, le=100) order_by: str = Field(default="created_at") order_desc: bool = Field(default=True) + + +class UploadFileInfo(BaseModel): + """Uploaded file information""" + filename: str + file_size: int + file_type: str + + +class UploadResponse(BaseModel): + """File upload response""" + task_id: str = Field(..., description="Created task ID") + filename: str = Field(..., description="Original filename") + file_size: int = Field(..., description="File size in bytes") + file_type: str = Field(..., description="File MIME type") + status: TaskStatusEnum = Field(..., description="Initial task status") diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5b05088..5cc97a9 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -23,7 +23,7 @@ import type { */ const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL const API_BASE_URL = envApiBaseUrl !== undefined ? envApiBaseUrl : 'http://localhost:8000' -const API_VERSION = 'v1' +const API_VERSION = 'v2' class ApiClient { private client: AxiosInstance diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 6492c73..eca1a36 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -22,10 +22,13 @@ export interface User { displayName?: string | null } -// File Upload +// File Upload (V2 API) export interface UploadResponse { - batch_id: number - files: FileInfo[] + task_id: string + filename: string + file_size: number + file_type: string + status: 'pending' | 'processing' | 'completed' | 'failed' } export interface FileInfo {