2025-10-24 21:24:00 +03:00
|
|
|
|
"""
|
|
|
|
|
|
S3 Service - Загрузка файлов в S3 (Timeweb Cloud Storage)
|
|
|
|
|
|
"""
|
|
|
|
|
|
import boto3
|
|
|
|
|
|
from botocore.client import Config
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
from ..config import settings
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class S3Service:
|
|
|
|
|
|
"""Сервис для работы с S3 хранилищем"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.client = None
|
|
|
|
|
|
self.bucket = settings.s3_bucket
|
|
|
|
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
|
|
"""Подключение к S3"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.client = boto3.client(
|
|
|
|
|
|
's3',
|
|
|
|
|
|
endpoint_url=settings.s3_endpoint,
|
|
|
|
|
|
aws_access_key_id=settings.s3_access_key,
|
|
|
|
|
|
aws_secret_access_key=settings.s3_secret_key,
|
|
|
|
|
|
config=Config(signature_version='s3v4'),
|
|
|
|
|
|
region_name=settings.s3_region
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"✅ S3 connected: {settings.s3_endpoint}/{settings.s3_bucket}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ S3 connection error: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
async def upload_file(
|
|
|
|
|
|
self,
|
|
|
|
|
|
file_content: bytes,
|
|
|
|
|
|
filename: str,
|
|
|
|
|
|
content_type: str = 'application/octet-stream',
|
|
|
|
|
|
folder: str = 'uploads'
|
|
|
|
|
|
) -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Загрузить файл в S3
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
file_content: Содержимое файла в bytes
|
|
|
|
|
|
filename: Имя файла
|
|
|
|
|
|
content_type: MIME тип
|
|
|
|
|
|
folder: Папка в bucket
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
URL файла в S3 или None при ошибке
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.client:
|
|
|
|
|
|
self.connect()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Генерируем уникальное имя файла
|
|
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
|
|
unique_id = str(uuid.uuid4())[:8]
|
|
|
|
|
|
safe_filename = f"{folder}/{timestamp}_{unique_id}_{filename}"
|
|
|
|
|
|
|
|
|
|
|
|
# Загружаем файл
|
|
|
|
|
|
self.client.put_object(
|
|
|
|
|
|
Bucket=self.bucket,
|
|
|
|
|
|
Key=safe_filename,
|
|
|
|
|
|
Body=file_content,
|
|
|
|
|
|
ContentType=content_type
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Генерируем URL
|
|
|
|
|
|
file_url = f"{settings.s3_endpoint}/{self.bucket}/{safe_filename}"
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"✅ File uploaded to S3: {safe_filename}")
|
|
|
|
|
|
return file_url
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ S3 upload error: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async def delete_file(self, file_key: str) -> bool:
|
|
|
|
|
|
"""Удалить файл из S3"""
|
|
|
|
|
|
if not self.client:
|
|
|
|
|
|
self.connect()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.client.delete_object(
|
|
|
|
|
|
Bucket=self.bucket,
|
|
|
|
|
|
Key=file_key
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"✅ File deleted from S3: {file_key}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ S3 delete error: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Глобальный экземпляр
|
|
|
|
|
|
s3_service = S3Service()
|
|
|
|
|
|
|
2025-10-25 09:27:56 +03:00
|
|
|
|
|
2025-10-25 09:56:01 +03:00
|
|
|
|
|