add backend proxy to minio (upload)
This commit is contained in:
parent
54ac9a4f78
commit
0a41aad937
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue