init
This commit is contained in:
6
app/storage/__init__.py
Normal file
6
app/storage/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .base import StorageBackend
|
||||
from .s3_backend import S3Backend
|
||||
from .minio_backend import MinIOBackend
|
||||
from .factory import get_storage_backend
|
||||
|
||||
__all__ = ["StorageBackend", "S3Backend", "MinIOBackend", "get_storage_backend"]
|
||||
73
app/storage/base.py
Normal file
73
app/storage/base.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import BinaryIO
|
||||
|
||||
|
||||
class StorageBackend(ABC):
|
||||
"""Abstract base class for storage backends"""
|
||||
|
||||
@abstractmethod
|
||||
async def upload_file(self, file_data: BinaryIO, object_name: str) -> str:
|
||||
"""
|
||||
Upload a file to storage
|
||||
|
||||
Args:
|
||||
file_data: Binary file data
|
||||
object_name: Name/path of the object in storage
|
||||
|
||||
Returns:
|
||||
Storage path/URL of uploaded file
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def download_file(self, object_name: str) -> bytes:
|
||||
"""
|
||||
Download a file from storage
|
||||
|
||||
Args:
|
||||
object_name: Name/path of the object in storage
|
||||
|
||||
Returns:
|
||||
Binary file data
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_file(self, object_name: str) -> bool:
|
||||
"""
|
||||
Delete a file from storage
|
||||
|
||||
Args:
|
||||
object_name: Name/path of the object in storage
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def file_exists(self, object_name: str) -> bool:
|
||||
"""
|
||||
Check if a file exists in storage
|
||||
|
||||
Args:
|
||||
object_name: Name/path of the object in storage
|
||||
|
||||
Returns:
|
||||
True if file exists
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_file_url(self, object_name: str, expiration: int = 3600) -> str:
|
||||
"""
|
||||
Get a presigned URL for downloading a file
|
||||
|
||||
Args:
|
||||
object_name: Name/path of the object in storage
|
||||
expiration: URL expiration time in seconds
|
||||
|
||||
Returns:
|
||||
Presigned URL
|
||||
"""
|
||||
pass
|
||||
17
app/storage/factory.py
Normal file
17
app/storage/factory.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from app.storage.base import StorageBackend
|
||||
from app.storage.s3_backend import S3Backend
|
||||
from app.storage.minio_backend import MinIOBackend
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def get_storage_backend() -> StorageBackend:
|
||||
"""
|
||||
Factory function to get the appropriate storage backend
|
||||
based on configuration
|
||||
"""
|
||||
if settings.storage_backend == "s3":
|
||||
return S3Backend()
|
||||
elif settings.storage_backend == "minio":
|
||||
return MinIOBackend()
|
||||
else:
|
||||
raise ValueError(f"Unsupported storage backend: {settings.storage_backend}")
|
||||
88
app/storage/minio_backend.py
Normal file
88
app/storage/minio_backend.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from botocore.client import Config
|
||||
from typing import BinaryIO
|
||||
from app.storage.base import StorageBackend
|
||||
from app.config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MinIOBackend(StorageBackend):
|
||||
"""MinIO storage backend implementation (S3-compatible)"""
|
||||
|
||||
def __init__(self):
|
||||
# MinIO uses S3-compatible API
|
||||
self.s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=f"{'https' if settings.minio_secure else 'http'}://{settings.minio_endpoint}",
|
||||
aws_access_key_id=settings.minio_access_key,
|
||||
aws_secret_access_key=settings.minio_secret_key,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='us-east-1'
|
||||
)
|
||||
self.bucket_name = settings.minio_bucket_name
|
||||
self._ensure_bucket_exists()
|
||||
|
||||
def _ensure_bucket_exists(self):
|
||||
"""Create bucket if it doesn't exist"""
|
||||
try:
|
||||
self.s3_client.head_bucket(Bucket=self.bucket_name)
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == '404':
|
||||
try:
|
||||
self.s3_client.create_bucket(Bucket=self.bucket_name)
|
||||
logger.info(f"Created MinIO bucket: {self.bucket_name}")
|
||||
except ClientError as create_error:
|
||||
logger.error(f"Failed to create bucket: {create_error}")
|
||||
raise
|
||||
|
||||
async def upload_file(self, file_data: BinaryIO, object_name: str) -> str:
|
||||
"""Upload file to MinIO"""
|
||||
try:
|
||||
self.s3_client.upload_fileobj(file_data, self.bucket_name, object_name)
|
||||
return f"minio://{self.bucket_name}/{object_name}"
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to upload file to MinIO: {e}")
|
||||
raise
|
||||
|
||||
async def download_file(self, object_name: str) -> bytes:
|
||||
"""Download file from MinIO"""
|
||||
try:
|
||||
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return response['Body'].read()
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to download file from MinIO: {e}")
|
||||
raise
|
||||
|
||||
async def delete_file(self, object_name: str) -> bool:
|
||||
"""Delete file from MinIO"""
|
||||
try:
|
||||
self.s3_client.delete_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return True
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to delete file from MinIO: {e}")
|
||||
return False
|
||||
|
||||
async def file_exists(self, object_name: str) -> bool:
|
||||
"""Check if file exists in MinIO"""
|
||||
try:
|
||||
self.s3_client.head_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return True
|
||||
except ClientError:
|
||||
return False
|
||||
|
||||
async def get_file_url(self, object_name: str, expiration: int = 3600) -> str:
|
||||
"""Generate presigned URL for MinIO object"""
|
||||
try:
|
||||
url = self.s3_client.generate_presigned_url(
|
||||
'get_object',
|
||||
Params={'Bucket': self.bucket_name, 'Key': object_name},
|
||||
ExpiresIn=expiration
|
||||
)
|
||||
return url
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to generate presigned URL: {e}")
|
||||
raise
|
||||
87
app/storage/s3_backend.py
Normal file
87
app/storage/s3_backend.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from typing import BinaryIO
|
||||
from app.storage.base import StorageBackend
|
||||
from app.config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class S3Backend(StorageBackend):
|
||||
"""AWS S3 storage backend implementation"""
|
||||
|
||||
def __init__(self):
|
||||
self.s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=settings.aws_access_key_id,
|
||||
aws_secret_access_key=settings.aws_secret_access_key,
|
||||
region_name=settings.aws_region
|
||||
)
|
||||
self.bucket_name = settings.s3_bucket_name
|
||||
self._ensure_bucket_exists()
|
||||
|
||||
def _ensure_bucket_exists(self):
|
||||
"""Create bucket if it doesn't exist"""
|
||||
try:
|
||||
self.s3_client.head_bucket(Bucket=self.bucket_name)
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == '404':
|
||||
try:
|
||||
self.s3_client.create_bucket(
|
||||
Bucket=self.bucket_name,
|
||||
CreateBucketConfiguration={'LocationConstraint': settings.aws_region}
|
||||
)
|
||||
logger.info(f"Created S3 bucket: {self.bucket_name}")
|
||||
except ClientError as create_error:
|
||||
logger.error(f"Failed to create bucket: {create_error}")
|
||||
raise
|
||||
|
||||
async def upload_file(self, file_data: BinaryIO, object_name: str) -> str:
|
||||
"""Upload file to S3"""
|
||||
try:
|
||||
self.s3_client.upload_fileobj(file_data, self.bucket_name, object_name)
|
||||
return f"s3://{self.bucket_name}/{object_name}"
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to upload file to S3: {e}")
|
||||
raise
|
||||
|
||||
async def download_file(self, object_name: str) -> bytes:
|
||||
"""Download file from S3"""
|
||||
try:
|
||||
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return response['Body'].read()
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to download file from S3: {e}")
|
||||
raise
|
||||
|
||||
async def delete_file(self, object_name: str) -> bool:
|
||||
"""Delete file from S3"""
|
||||
try:
|
||||
self.s3_client.delete_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return True
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to delete file from S3: {e}")
|
||||
return False
|
||||
|
||||
async def file_exists(self, object_name: str) -> bool:
|
||||
"""Check if file exists in S3"""
|
||||
try:
|
||||
self.s3_client.head_object(Bucket=self.bucket_name, Key=object_name)
|
||||
return True
|
||||
except ClientError:
|
||||
return False
|
||||
|
||||
async def get_file_url(self, object_name: str, expiration: int = 3600) -> str:
|
||||
"""Generate presigned URL for S3 object"""
|
||||
try:
|
||||
url = self.s3_client.generate_presigned_url(
|
||||
'get_object',
|
||||
Params={'Bucket': self.bucket_name, 'Key': object_name},
|
||||
ExpiresIn=expiration
|
||||
)
|
||||
return url
|
||||
except ClientError as e:
|
||||
logger.error(f"Failed to generate presigned URL: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user