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:
egg
2025-11-16 19:13:22 +08:00
parent 3f41a33877
commit ad5c8be0a3
5 changed files with 121 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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 {