Files
ljwx-pages/backend/app/services/storage_service.py
T
brunoandClaude Sonnet 4.5 b2cc88aa6a 初始提交: 错题集应用核心功能实现
功能特性:
- ✅ 完整的后端API (FastAPI)
- ✅ 图片处理管线 (PaddleOCR + OpenCV)
- ✅ 手写去除功能
- ✅ Elasticsearch全文搜索
- ✅ MinIO对象存储集成
- ✅ Flutter跨平台前端架构
- ✅ Riverpod状态管理
- ✅ Docker容器化部署

技术栈:
- 后端: Python 3.9+, FastAPI, PostgreSQL, Elasticsearch, Redis
- 前端: Flutter 3.0+, Dart, Riverpod
- 图像处理: PaddleOCR, OpenCV
- 基础设施: Docker, MinIO

完成度:
- 后端核心功能: 85%
- 图片处理: 100%
- 前端数据层: 100%
- 前端UI: 20%

文档:
- 需求规格说明书
- 技术设计文档
- API接口文档
- 实施进度文档
- 快速启动脚本

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 19:53:45 +08:00

218 lines
6.4 KiB
Python

from minio import Minio
from minio.error import S3Error
from app.config import settings
import logging
from io import BytesIO
from typing import Optional
import uuid
logger = logging.getLogger(__name__)
class StorageService:
"""
Service for handling file storage using MinIO
"""
def __init__(self):
try:
# Initialize MinIO client
# Remove http:// or https:// from endpoint if present
endpoint = settings.storage_endpoint.replace("http://", "").replace("https://", "")
self.client = Minio(
endpoint,
access_key=settings.storage_access_key,
secret_key=settings.storage_secret_key,
secure=False # Set to True for HTTPS
)
self.bucket_name = settings.storage_bucket
self.enabled = True
# Ensure bucket exists
self._ensure_bucket_exists()
logger.info(f"MinIO storage service initialized successfully, bucket: {self.bucket_name}")
except Exception as e:
logger.warning(f"MinIO initialization failed: {e}. File storage will be limited.")
self.client = None
self.enabled = False
def _ensure_bucket_exists(self):
"""
Create bucket if it doesn't exist
"""
try:
if not self.client.bucket_exists(self.bucket_name):
self.client.make_bucket(self.bucket_name)
logger.info(f"Created bucket: {self.bucket_name}")
except S3Error as e:
logger.error(f"Failed to create bucket: {e}")
async def upload_file(
self,
file_path: str,
object_name: Optional[str] = None,
content_type: str = "image/jpeg"
) -> str:
"""
Upload a file to MinIO storage
Args:
file_path: Local path to the file
object_name: Name of the object in storage (if None, generate UUID)
content_type: MIME type of the file
Returns:
URL of the uploaded file
"""
if not self.enabled:
# Fallback: return local file path
return f"file://{file_path}"
try:
# Generate object name if not provided
if object_name is None:
extension = file_path.split('.')[-1] if '.' in file_path else 'jpg'
object_name = f"{uuid.uuid4()}.{extension}"
# Upload file
self.client.fput_object(
self.bucket_name,
object_name,
file_path,
content_type=content_type
)
# Generate URL
url = f"{settings.storage_endpoint}/{self.bucket_name}/{object_name}"
logger.info(f"Uploaded file to {url}")
return url
except S3Error as e:
logger.error(f"Failed to upload file {file_path}: {e}")
return f"file://{file_path}" # Fallback to local path
async def upload_bytes(
self,
data: bytes,
object_name: Optional[str] = None,
content_type: str = "image/jpeg"
) -> str:
"""
Upload bytes data to MinIO storage
Args:
data: Bytes data to upload
object_name: Name of the object in storage (if None, generate UUID)
content_type: MIME type of the file
Returns:
URL of the uploaded file
"""
if not self.enabled:
return "data://unavailable"
try:
# Generate object name if not provided
if object_name is None:
extension = 'jpg' if 'image' in content_type else 'bin'
object_name = f"{uuid.uuid4()}.{extension}"
# Upload bytes
data_stream = BytesIO(data)
self.client.put_object(
self.bucket_name,
object_name,
data_stream,
length=len(data),
content_type=content_type
)
# Generate URL
url = f"{settings.storage_endpoint}/{self.bucket_name}/{object_name}"
logger.info(f"Uploaded bytes to {url}")
return url
except S3Error as e:
logger.error(f"Failed to upload bytes: {e}")
return "data://unavailable"
async def delete_file(self, object_name: str) -> bool:
"""
Delete a file from MinIO storage
Args:
object_name: Name of the object to delete
Returns:
True if successful, False otherwise
"""
if not self.enabled:
return False
try:
self.client.remove_object(self.bucket_name, object_name)
logger.info(f"Deleted object {object_name}")
return True
except S3Error as e:
logger.error(f"Failed to delete object {object_name}: {e}")
return False
async def get_file_url(self, object_name: str, expires: int = 3600) -> str:
"""
Get a presigned URL for accessing a file
Args:
object_name: Name of the object
expires: Expiration time in seconds (default: 1 hour)
Returns:
Presigned URL
"""
if not self.enabled:
return ""
try:
url = self.client.presigned_get_object(
self.bucket_name,
object_name,
expires=expires
)
return url
except S3Error as e:
logger.error(f"Failed to generate presigned URL for {object_name}: {e}")
return ""
def extract_object_name_from_url(self, url: str) -> Optional[str]:
"""
Extract object name from storage URL
Args:
url: Full storage URL
Returns:
Object name or None
"""
try:
# URL format: http://endpoint/bucket/object_name
parts = url.split('/')
if len(parts) >= 2 and self.bucket_name in url:
# Find the bucket name and get everything after it
bucket_index = parts.index(self.bucket_name)
if bucket_index + 1 < len(parts):
return '/'.join(parts[bucket_index + 1:])
except Exception as e:
logger.error(f"Failed to extract object name from URL {url}: {e}")
return None
# Global storage service instance
storage_service = StorageService()