功能特性: - ✅ 完整的后端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>
218 lines
6.4 KiB
Python
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()
|