1
0
Fork 0
mirror of https://github.com/EDeev/api_processor.git synced 2026-10-07 20:49:34 +03:00

Настройки из окружения, токен доступа, лимит размера, без утечки ошибок

- SECRET_KEY, DEBUG (по умолчанию выключен), ALLOWED_HOSTS — из DJANGO_*;
- необязательный API_TOKEN (заголовок Authorization: Bearer), лимит файла
  MAX_UPLOAD_SIZE_MB (по умолчанию 50);
- в ответ на ошибку больше не отдаётся текст исключения — он уходит в лог;
- Django 5.2 LTS: 4.2 больше не поддерживается.
This commit is contained in:
Деев Егор Викторович 2026-10-06 00:38:57 +00:00
parent 7465fce2b7
commit b211b10c39
3 changed files with 125 additions and 66 deletions

16
api_app/permissions.py Normal file
View file

@ -0,0 +1,16 @@
import hmac
from django.conf import settings
from rest_framework.permissions import BasePermission
class ApiTokenPermission(BasePermission):
"""Если задан API_TOKEN, запросы должны нести заголовок Authorization: Bearer <токен>"""
message = "Нужен заголовок Authorization: Bearer <API_TOKEN>"
def has_permission(self, request, view):
if not settings.API_TOKEN:
return True
header = request.headers.get("Authorization", "")
return hmac.compare_digest(header, f"Bearer {settings.API_TOKEN}")

View file

@ -1,77 +1,90 @@
# api_project/api_app/views.py
import logging
import os
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
from django.conf import settings
from rest_framework import status
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import AudioFile, DocumentFile
from .services.vosk_recognizer import recognize_speech
from .services.scan import extract_text_tables
from .grpc_client.client import send_to_grpc_server
from .models import AudioFile, DocumentFile
from .services.scan import extract_text_tables
from .services.vosk_recognizer import RecognitionError, recognize_speech
class AudioToTextView(APIView):
logger = logging.getLogger(__name__)
def too_large(uploaded):
return uploaded.size > settings.MAX_UPLOAD_SIZE
def size_error():
return Response({'error': f'Файл больше {settings.MAX_UPLOAD_SIZE // 1024 // 1024} МБ'},
status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
class ProcessView(APIView):
parser_classes = (MultiPartParser, FormParser)
field = ""
model = None
def process(self, path):
raise NotImplementedError
def handle(self, uploaded):
record = self.model(file=uploaded)
record.save()
try:
text = self.process(os.path.join(settings.MEDIA_ROOT, record.file.name))
except RecognitionError as e:
return Response({'error': str(e)}, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
except Exception:
# подробности — в лог, а не в ответ клиенту
logger.exception("Ошибка обработки %s", record.file.name)
return Response({'error': 'Не удалось обработать файл'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
record.processed_text = text
record.save()
return Response({'text': text, 'grpc_response': send_to_grpc_server(text)}, status=status.HTTP_200_OK)
class AudioToTextView(ProcessView):
model = AudioFile
def process(self, path):
return recognize_speech(path)
def post(self, request, *args, **kwargs):
audio_file = request.FILES.get('audio')
if not audio_file:
return Response({'error': 'Нет аудио файла'}, status=status.HTTP_400_BAD_REQUEST)
audio_model = AudioFile(file=audio_file)
audio_model.save()
try:
file_path = os.path.join(settings.MEDIA_ROOT, audio_model.file.name)
text = recognize_speech(file_path)
audio_model.processed_text = text
audio_model.save()
grpc_response = send_to_grpc_server(text)
return Response({
'text': text,
'grpc_response': grpc_response
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if too_large(audio_file):
return size_error()
return self.handle(audio_file)
class DocumentToTextView(ProcessView):
model = DocumentFile
def process(self, path):
return extract_text_tables(path)
class DocumentToTextView(APIView):
parser_classes = (MultiPartParser, FormParser)
def post(self, request, *args, **kwargs):
document_file = request.FILES.get('document')
if not document_file:
return Response({'error': 'Нет документа'}, status=status.HTTP_400_BAD_REQUEST)
file_ext = os.path.splitext(document_file.name)[1].lower()
if file_ext not in ['.pdf', '.docx']:
return Response({'error': 'Поддерживаются только PDF и DOCX файлы'},
return Response({'error': 'Поддерживаются только PDF и DOCX файлы'},
status=status.HTTP_400_BAD_REQUEST)
doc_model = DocumentFile(file=document_file)
doc_model.save()
try:
file_path = os.path.join(settings.MEDIA_ROOT, doc_model.file.name)
text = extract_text_tables(file_path)
doc_model.processed_text = text
doc_model.save()
grpc_response = send_to_grpc_server(text)
return Response({
'text': text,
'grpc_response': grpc_response
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if too_large(document_file):
return size_error()
return self.handle(document_file)

View file

@ -3,11 +3,20 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-)+yykzv8cr7dbc38g2#x(8*ifs@+-f_fyan9!c%mmxg1$ekztq'
DEBUG = True
def env_bool(name, default=False):
return os.environ.get(name, str(default)).lower() in ("1", "true", "yes")
ALLOWED_HOSTS = []
DEBUG = env_bool("DJANGO_DEBUG")
# Ключ по умолчанию — только для разработки (DJANGO_DEBUG=True)
DEV_SECRET_KEY = "django-insecure-dev-only-change-me"
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", DEV_SECRET_KEY if DEBUG else "")
if not SECRET_KEY:
raise RuntimeError("Задайте DJANGO_SECRET_KEY (или DJANGO_DEBUG=True для разработки)")
ALLOWED_HOSTS = [h for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") if h]
INSTALLED_APPS = [
'django.contrib.admin',
@ -15,7 +24,7 @@ INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
# 'django.contrib.staticfiles',
'django.contrib.staticfiles',
'rest_framework', # Добавляем DRF
'api_app', # Наше API приложение
]
@ -53,17 +62,38 @@ WSGI_APPLICATION = 'api_project.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'NAME': os.environ.get("DJANGO_DB_PATH", BASE_DIR / 'db.sqlite3'),
}
}
# Путь для загрузки файлов
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", os.path.join(BASE_DIR, 'media'))
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Распознавание и обработка
VOSK_MODEL_PATH = os.environ.get("VOSK_MODEL_PATH", BASE_DIR / "models" / "vosk-model-small-ru-0.22")
FFMPEG_BINARY = os.environ.get("FFMPEG_BINARY", "ffmpeg") # из PATH; раньше — Windows-путь к ffmpeg.exe
GRPC_SERVER = os.environ.get("GRPC_SERVER", "localhost:50051") # пусто — не отправлять на gRPC
GRPC_TIMEOUT = float(os.environ.get("GRPC_TIMEOUT", "10"))
API_TOKEN = os.environ.get("API_TOKEN", "") # если задан — нужен заголовок Authorization: Bearer
MAX_UPLOAD_SIZE = int(os.environ.get("MAX_UPLOAD_SIZE_MB", "50")) * 1024 * 1024
FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # крупные файлы — во временный файл, не в память
# Настройки для REST Framework
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny', # Для тестирования, в продакшне лучше ограничить
'api_app.permissions.ApiTokenPermission',
]
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
# как в существующей миграции — без новой миграции
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'