Files
aiform_dev/backend/app/services/s3_service.py
AI Assistant 647abf6578 feat: Интеграция n8n + Redis Pub/Sub + SSE для real-time обработки заявок
🎯 Основные изменения:

Backend:
-  Добавлен SSE endpoint для real-time событий (/api/v1/events/{task_id})
-  Redis Pub/Sub для публикации/подписки на события OCR/Vision
-  Удален aioboto3 из requirements.txt (конфликт зависимостей)
-  Добавлен OCR worker (deprecated, логика перенесена в n8n)

Frontend (React):
-  Автогенерация claim_id и session_id
-  Клиентская конвертация файлов в PDF (JPG/PNG/HEIC/WEBP)
-  Сжатие изображений до 2MB перед конвертацией
-  SSE подписка на события OCR/Vision в Step1Policy
-  Валидация документов (полис vs неподходящий контент)
-  Real-time прогресс загрузки и обработки файлов
-  Интеграция с n8n webhooks для проверки полиса и загрузки файлов

n8n Workflows:
-  Проверка полиса в MySQL + запись в PostgreSQL
-  Загрузка файлов в S3 + OCR + Vision AI
-  Публикация событий в Redis через backend API
-  Валидация документов (распознавание полисов ERV)

Документация:
- 📝 N8N_INTEGRATION.md - интеграция с n8n
- 📝 N8N_SQL_QUERIES.md - SQL запросы для workflows
- 📝 N8N_PDF_COMPRESS.md - сжатие PDF
- 📝 N8N_STIRLING_COMPRESS.md - интеграция Stirling-PDF

Утилиты:
- 🔧 monitor_redis.py/sh - мониторинг Redis Pub/Sub
- 🔧 test_redis_events.sh - тестирование событий
- 🔧 pdfConverter.ts - клиентская конвертация в PDF

Архитектура:
React → n8n webhooks (sync) → MySQL/PostgreSQL/S3
      → n8n workflows (async) → OCR/Vision → Redis Pub/Sub → SSE → React
2025-10-27 08:33:16 +03:00

156 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()