POST /upload (multipart/form-data: file):
1. Сохраняет original_bytes в documents (status='uploaded')
2. parser.parse() → elements
3. textify.to_text() → parsed_text
4. UPDATE documents SET parsed_text, status='parsed'
5. Возвращает {document_id, filename, status, elements_count}
При ошибке парсинга: status='error', error_message, 422
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
"""
|
||
upload.py — Слой загрузки документов.
|
||
|
||
Blueprint /upload: принимает файлы, сохраняет в БД, запускает парсинг.
|
||
|
||
Поток:
|
||
POST /upload (multipart/form-data: file)
|
||
→ сохраняем original_bytes в documents (status='uploaded')
|
||
→ parser.parse() → elements
|
||
→ textify.to_text() → parsed_text
|
||
→ UPDATE documents SET parsed_text, status='parsed'
|
||
→ возвращаем document_id
|
||
|
||
Не зависит от LLM-слоёв. Только от parser.py, textify.py, db.py.
|
||
"""
|
||
|
||
from flask import Blueprint, request, jsonify
|
||
import db
|
||
import parser
|
||
import textify
|
||
|
||
upload_bp = Blueprint("upload", __name__)
|
||
|
||
|
||
@upload_bp.route("/upload", methods=["POST"])
|
||
def upload():
|
||
"""
|
||
Загрузить файл договора/допника.
|
||
|
||
Ожидает multipart/form-data с полем 'file'.
|
||
Возвращает {document_id, filename, status}.
|
||
"""
|
||
# 1. Проверить что файл передан
|
||
if "file" not in request.files:
|
||
return jsonify({"error": "no file in request"}), 400
|
||
|
||
file = request.files["file"]
|
||
if not file.filename:
|
||
return jsonify({"error": "empty filename"}), 400
|
||
|
||
# 2. Определить MIME-тип
|
||
filename = file.filename
|
||
mime_type = file.content_type or _guess_mime(filename)
|
||
file_bytes = file.read()
|
||
|
||
if not file_bytes:
|
||
return jsonify({"error": "empty file"}), 400
|
||
|
||
# 3. Сохранить исходный файл в БД
|
||
rowcount, err = db.execute(
|
||
"INSERT INTO documents (filename, mime_type, original_bytes, status) "
|
||
"VALUES (%s, %s, %s, 'uploaded')",
|
||
(filename, mime_type, file_bytes),
|
||
)
|
||
if err:
|
||
return jsonify({"error": f"db insert: {err}"}), 500
|
||
|
||
# 4. Получить ID созданного документа
|
||
doc, err = db.query_one(
|
||
"SELECT id FROM documents "
|
||
"WHERE filename = %s AND status = 'uploaded' "
|
||
"ORDER BY created_at DESC LIMIT 1",
|
||
(filename,),
|
||
)
|
||
if err or not doc:
|
||
return jsonify({"error": f"document not found after insert: {err}"}), 500
|
||
|
||
document_id = doc["id"]
|
||
|
||
# 5. Парсинг: bytes → elements → текст
|
||
parse_result = parser.parse(file_bytes, mime_type)
|
||
|
||
if parse_result.get("errors"):
|
||
# Парсинг частично не удался — сохраняем что смогли, помечаем ошибку
|
||
db.execute(
|
||
"UPDATE documents SET status = 'error', error_message = %s WHERE id = %s",
|
||
("; ".join(parse_result["errors"]), str(document_id)),
|
||
)
|
||
return jsonify({
|
||
"document_id": str(document_id),
|
||
"filename": filename,
|
||
"status": "error",
|
||
"errors": parse_result["errors"],
|
||
}), 422
|
||
|
||
# 6. JSON elements → линейный текст для LLM
|
||
elements = parse_result.get("elements", [])
|
||
parsed_text = textify.to_text(elements)
|
||
|
||
# 7. Обновить документ: текст + статус
|
||
db.execute(
|
||
"UPDATE documents SET parsed_text = %s, status = 'parsed' WHERE id = %s",
|
||
(parsed_text, str(document_id)),
|
||
)
|
||
|
||
return jsonify({
|
||
"document_id": str(document_id),
|
||
"filename": filename,
|
||
"status": "parsed",
|
||
"elements_count": len(elements),
|
||
})
|
||
|
||
|
||
def _guess_mime(filename: str) -> str:
|
||
"""
|
||
Угадать MIME-тип по расширению файла.
|
||
Используется когда браузер не передал content_type.
|
||
"""
|
||
ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
|
||
mime_map = {
|
||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
"doc": "application/msword",
|
||
"pdf": "application/pdf",
|
||
"zip": "application/zip",
|
||
}
|
||
return mime_map.get(ext, "application/octet-stream")
|