add backend proxy to minio (upload)

This commit is contained in:
itqop 2025-12-30 18:02:50 +03:00
parent 54ac9a4f78
commit 0a41aad937
5 changed files with 120 additions and 5 deletions

View File

@ -1,6 +1,6 @@
"""Upload API routes."""
from fastapi import APIRouter, status
from fastapi import APIRouter, File, UploadFile, status
from app.api.dependencies import CurrentUser, DatabaseSession, S3ClientDep
from app.api.schemas import (
@ -49,6 +49,36 @@ async def create_upload(
)
@router.post("/{asset_id}/file")
async def upload_file(
asset_id: str,
file: UploadFile,
current_user: CurrentUser,
session: DatabaseSession,
s3_client: S3ClientDep,
):
"""
Upload file content through backend proxy to S3.
Args:
asset_id: Asset ID from create_upload
file: File to upload
current_user: Current authenticated user
session: Database session
s3_client: S3 client
Returns:
Success status
"""
asset_service = AssetService(session, s3_client)
await asset_service.upload_file_to_s3(
user_id=current_user.id,
asset_id=asset_id,
file=file,
)
return {"status": "success"}
@router.post("/{asset_id}/finalize", response_model=AssetResponse)
async def finalize_upload(
asset_id: str,

View File

@ -114,6 +114,22 @@ class S3Client:
ExtraArgs={"ContentType": content_type},
)
def put_object(self, storage_key: str, file_data: bytes, content_type: str) -> None:
"""
Upload file data (bytes) to S3.
Args:
storage_key: S3 object key
file_data: File content as bytes
content_type: File content type
"""
self.client.put_object(
Bucket=self.bucket,
Key=storage_key,
Body=file_data,
ContentType=content_type,
)
def delete_object(self, storage_key: str) -> None:
"""
Delete an object from S3.

View File

@ -3,7 +3,8 @@
import os
from typing import AsyncIterator, Optional, Tuple
from fastapi import HTTPException, status
from botocore.exceptions import ClientError
from fastapi import HTTPException, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Asset, AssetStatus, AssetType
@ -90,6 +91,51 @@ class AssetService:
return asset, presigned_post
async def upload_file_to_s3(
self,
user_id: str,
asset_id: str,
file: UploadFile,
) -> None:
"""
Upload file content to S3 through backend.
Args:
user_id: User ID
asset_id: Asset ID
file: File to upload
Raises:
HTTPException: If asset not found or not authorized
"""
# Get asset and verify ownership
asset = await self.asset_repo.get_by_id(asset_id)
if not asset or asset.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Asset not found",
)
if not asset.storage_key_original:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Asset has no storage key",
)
# Upload file to S3
try:
content = await file.read()
self.s3_client.put_object(
storage_key=asset.storage_key_original,
file_data=content,
content_type=asset.content_type,
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to upload file: {str(e)}",
)
async def finalize_upload(
self,
user_id: str,
@ -251,7 +297,19 @@ class AssetService:
content_type = asset.content_type
# Stream file from S3
file_stream, content_length = await self.s3_client.stream_object(storage_key)
try:
file_stream, content_length = await self.s3_client.stream_object(storage_key)
except ClientError as e:
error_code = e.response.get("Error", {}).get("Code", "")
if error_code == "NoSuchKey":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Media file not found in storage",
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve media from storage: {error_code}",
)
return file_stream, content_type, content_length

View File

@ -79,8 +79,8 @@ export default function UploadDialog({ open, onClose, onComplete }: UploadDialog
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
// Step 2: Upload to S3
await api.uploadToS3(uploadData.upload_url, file, uploadData.fields);
// Step 2: Upload file to backend
await api.uploadFileToBackend(uploadData.asset_id, file);
updateFileProgress(index, 66, 'uploading', undefined, uploadData.asset_id);

View File

@ -142,6 +142,17 @@ class ApiClient {
});
}
async uploadFileToBackend(assetId: string, file: File): Promise<void> {
const formData = new FormData();
formData.append('file', file);
await this.client.post(`/uploads/${assetId}/file`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
async finalizeUpload(assetId: string, etag?: string, sha256?: string): Promise<Asset> {
const { data } = await this.client.post(`/uploads/${assetId}/finalize`, { etag, sha256 });
return data;