add backend proxy to minio (upload)
This commit is contained in:
parent
54ac9a4f78
commit
0a41aad937
|
|
@ -1,6 +1,6 @@
|
||||||
"""Upload API routes."""
|
"""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.dependencies import CurrentUser, DatabaseSession, S3ClientDep
|
||||||
from app.api.schemas import (
|
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)
|
@router.post("/{asset_id}/finalize", response_model=AssetResponse)
|
||||||
async def finalize_upload(
|
async def finalize_upload(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,22 @@ class S3Client:
|
||||||
ExtraArgs={"ContentType": content_type},
|
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:
|
def delete_object(self, storage_key: str) -> None:
|
||||||
"""
|
"""
|
||||||
Delete an object from S3.
|
Delete an object from S3.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
import os
|
import os
|
||||||
from typing import AsyncIterator, Optional, Tuple
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.domain.models import Asset, AssetStatus, AssetType
|
from app.domain.models import Asset, AssetStatus, AssetType
|
||||||
|
|
@ -90,6 +91,51 @@ class AssetService:
|
||||||
|
|
||||||
return asset, presigned_post
|
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(
|
async def finalize_upload(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|
@ -251,7 +297,19 @@ class AssetService:
|
||||||
content_type = asset.content_type
|
content_type = asset.content_type
|
||||||
|
|
||||||
# Stream file from S3
|
# 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
|
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);
|
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
|
||||||
|
|
||||||
// Step 2: Upload to S3
|
// Step 2: Upload file to backend
|
||||||
await api.uploadToS3(uploadData.upload_url, file, uploadData.fields);
|
await api.uploadFileToBackend(uploadData.asset_id, file);
|
||||||
|
|
||||||
updateFileProgress(index, 66, 'uploading', undefined, uploadData.asset_id);
|
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> {
|
async finalizeUpload(assetId: string, etag?: string, sha256?: string): Promise<Asset> {
|
||||||
const { data } = await this.client.post(`/uploads/${assetId}/finalize`, { etag, sha256 });
|
const { data } = await this.client.post(`/uploads/${assetId}/finalize`, { etag, sha256 });
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue