""" 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}" # Загружаем файл с публичным доступом (для OCR) # ВРЕМЕННОЕ РЕШЕНИЕ: делаем файлы публичными пока presigned URL не работает acl = 'public-read' if folder == 'ocr_temp' else 'private' self.client.put_object( Bucket=self.bucket, Key=safe_filename, Body=file_content, ContentType=content_type, ACL=acl # Делаем ocr_temp файлы публичными ) # Генерируем URL file_url = f"{settings.s3_endpoint}/{self.bucket}/{safe_filename}" logger.info(f"✅ File uploaded to S3: {safe_filename} (ACL: {acl})") 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 def generate_presigned_url(self, file_key: str, expiration: int = 3600) -> Optional[str]: """ Генерация временного публичного URL для файла Args: file_key: Ключ файла в S3 (путь) expiration: Время жизни URL в секундах (по умолчанию 1 час) Returns: Presigned URL или None при ошибке """ if not self.client: self.connect() try: # Для Timeweb Cloud Storage нужно использовать ClientMethod вместо обычного метода # И добавить HttpMethod явно url = self.client.generate_presigned_url( ClientMethod='get_object', Params={ 'Bucket': self.bucket, 'Key': file_key }, ExpiresIn=expiration, HttpMethod='GET' ) logger.info(f"✅ Presigned URL generated for: {file_key} (expires in {expiration}s)") return url except Exception as e: logger.error(f"❌ Presigned URL generation error: {e}") return None def get_public_url(self, file_key: str) -> str: """ Простой публичный URL (без подписи) ВНИМАНИЕ: Работает только если bucket публичный! Args: file_key: Ключ файла в S3 Returns: Публичный URL """ return f"{settings.s3_endpoint}/{self.bucket}/{file_key}" # Глобальный экземпляр s3_service = S3Service()