🎯 Основные изменения: 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
156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
"""
|
||
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()
|
||
|
||
|
||
|