fix: add V2 file upload endpoint and update frontend to v2 API
Add missing file upload functionality to V2 API that was removed during V1 to V2 migration. Update frontend to use v2 API endpoints. Backend changes: - Add /api/v2/upload endpoint in main.py for file uploads - Import necessary dependencies (UploadFile, hashlib, TaskFile) - Upload endpoint creates task, saves file, and returns task info - Add UploadResponse schema to task.py schemas - Update tasks router imports for consistency Frontend changes: - Update API_VERSION from 'v1' to 'v2' in api.ts - Update UploadResponse type to match V2 API response format (task_id instead of batch_id, single file instead of array) This fixes the 404 error when uploading files from the frontend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -125,12 +125,103 @@ async def root():
|
|||||||
|
|
||||||
# Include V2 API routers
|
# Include V2 API routers
|
||||||
from app.routers import auth, tasks, admin
|
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(auth.router)
|
||||||
app.include_router(tasks.router)
|
app.include_router(tasks.router)
|
||||||
app.include_router(admin.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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ Handles OCR task operations with user isolation
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
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 fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.deps import get_db, get_current_user
|
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.user import User
|
||||||
from app.models.task import TaskStatus
|
from app.models.task import TaskStatus, TaskFile
|
||||||
from app.schemas.task import (
|
from app.schemas.task import (
|
||||||
TaskCreate,
|
TaskCreate,
|
||||||
TaskUpdate,
|
TaskUpdate,
|
||||||
@@ -21,6 +25,7 @@ from app.schemas.task import (
|
|||||||
TaskListResponse,
|
TaskListResponse,
|
||||||
TaskStatsResponse,
|
TaskStatsResponse,
|
||||||
TaskStatusEnum,
|
TaskStatusEnum,
|
||||||
|
UploadResponse,
|
||||||
)
|
)
|
||||||
from app.services.task_service import task_service
|
from app.services.task_service import task_service
|
||||||
from app.services.file_access_service import file_access_service
|
from app.services.file_access_service import file_access_service
|
||||||
|
|||||||
@@ -101,3 +101,19 @@ class TaskHistoryQuery(BaseModel):
|
|||||||
page_size: int = Field(default=50, ge=1, le=100)
|
page_size: int = Field(default=50, ge=1, le=100)
|
||||||
order_by: str = Field(default="created_at")
|
order_by: str = Field(default="created_at")
|
||||||
order_desc: bool = Field(default=True)
|
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")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type {
|
|||||||
*/
|
*/
|
||||||
const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL
|
const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
const API_BASE_URL = envApiBaseUrl !== undefined ? envApiBaseUrl : 'http://localhost:8000'
|
const API_BASE_URL = envApiBaseUrl !== undefined ? envApiBaseUrl : 'http://localhost:8000'
|
||||||
const API_VERSION = 'v1'
|
const API_VERSION = 'v2'
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private client: AxiosInstance
|
private client: AxiosInstance
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ export interface User {
|
|||||||
displayName?: string | null
|
displayName?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// File Upload
|
// File Upload (V2 API)
|
||||||
export interface UploadResponse {
|
export interface UploadResponse {
|
||||||
batch_id: number
|
task_id: string
|
||||||
files: FileInfo[]
|
filename: string
|
||||||
|
file_size: number
|
||||||
|
file_type: string
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileInfo {
|
export interface FileInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user