чанки в БД (таблица chunks), убран _chunk_store в памяти, v1.17

This commit is contained in:
2026-06-17 17:23:43 +04:00
parent 87026a1fd8
commit 0dac6bf37b
3 changed files with 81 additions and 47 deletions
+42 -30
View File
@@ -17,9 +17,6 @@ from api import api_bp
load_dotenv()
# Хранилище чанков (в памяти, теряется при рестарте)
_chunk_store = {}
def _save_file_to_db(filename, file_bytes, mime, contract_id):
"""Сохранить один файл в БД: documents + supplements. Возвращает doc_id или None."""
@@ -104,47 +101,56 @@ class ContractsApp:
# ── Чанковая загрузка ──────────────────────────────────────
def _chunk_upload(self):
"""Принять чанк файла. На последнем — собрать и сохранить."""
"""Принять чанк. Хранить в БД. На последнем — собрать и сохранить."""
upload_id = request.form.get("upload_id", "")
filename = request.form.get("filename", "")
chunk_index = int(request.form.get("chunk_index", 0))
total_chunks = int(request.form.get("total_chunks", 1))
cid = request.form.get("cid")
cid = request.form.get("cid") or None
chunk_file = request.files.get("chunk")
if not chunk_file:
return jsonify({"error": "Нет чанка"}), 400
chunk_data = chunk_file.read()
mime = mimeutil.guess_mime(filename) or "application/octet-stream"
if upload_id not in _chunk_store:
_chunk_store[upload_id] = {
"filename": filename,
"mime": mimeutil.guess_mime(filename) or "application/octet-stream",
"chunks": [None] * total_chunks,
"total": total_chunks,
"cid": cid,
"received": 0,
}
# Сохранить чанк в БД (ON CONFLICT — идемпотентно)
db.execute(
"INSERT INTO chunks (upload_id, chunk_index, chunk_data, filename, mime, total_chunks, cid) "
"VALUES (%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (upload_id, chunk_index) DO NOTHING",
(upload_id, chunk_index, chunk_data, filename, mime, total_chunks, cid),
)
store = _chunk_store[upload_id]
if store["chunks"][chunk_index] is None:
store["chunks"][chunk_index] = chunk_data
store["received"] += 1
# Сколько уже получено
count_res, _ = db.query(
"SELECT COUNT(*) FROM chunks WHERE upload_id=%s", (upload_id,)
)
received = count_res["rows"][0][0] if count_res else 0
if store["received"] == total_chunks:
# Собрать файл
file_bytes = b"".join(store["chunks"])
mime = store["mime"]
fname = store["filename"]
cid = store["cid"]
if received == total_chunks:
try:
# Собрать все чанки по порядку
rows_res, _ = db.query(
"SELECT chunk_data FROM chunks WHERE upload_id=%s ORDER BY chunk_index",
(upload_id,),
)
file_bytes = b"".join(r[0] for r in rows_res["rows"])
# Получить метаданные из первой записи
meta, _ = db.query_one(
"SELECT filename, mime, cid FROM chunks WHERE upload_id=%s LIMIT 1",
(upload_id,),
)
fname = meta["filename"] if meta else filename
fmime = meta["mime"] if meta else mime
fcid = meta["cid"] if meta else cid
# Создать контракт если нет
if cid:
contract_id = cid
if fcid:
contract_id = fcid
else:
conn, err = db.connect()
if err:
del _chunk_store[upload_id]
return jsonify({"error": f"БД: {err}"}), 500
cur = conn.cursor()
cur.execute(
@@ -156,11 +162,17 @@ class ContractsApp:
cur.close()
conn.close()
_save_file_to_db(fname, file_bytes, mime, str(contract_id))
del _chunk_store[upload_id]
doc_id = _save_file_to_db(fname, file_bytes, fmime, str(contract_id))
if not doc_id:
return jsonify({"error": "Не удалось сохранить файл в БД"}), 500
except Exception as e:
return jsonify({"error": f"Сборка: {e}"}), 500
finally:
db.execute("DELETE FROM chunks WHERE upload_id=%s", (upload_id,))
return jsonify({"contract_id": str(contract_id)})
return jsonify({"chunk": chunk_index, "received": store["received"], "total": total_chunks})
return jsonify({"chunk": chunk_index, "received": received, "total": total_chunks})
def _show_contract(self, cid):
"""Показать договор с кнопкой «Обработать»."""
+19
View File
@@ -98,6 +98,24 @@ CREATE TABLE IF NOT EXISTS spec_history (
);
"""
# ── Таблица chunks ─────────────────────────────────────────────
# Чанковая загрузка: части файла до сборки.
# upload_id связывает чанки одного файла.
DDL_CHUNKS = """
CREATE TABLE IF NOT EXISTS chunks (
upload_id TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
chunk_data BYTEA NOT NULL,
filename TEXT,
mime TEXT,
total_chunks INTEGER,
cid TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (upload_id, chunk_index)
);
"""
# ── Список всех DDL ────────────────────────────────────────────
ALL_DDL = [
@@ -106,6 +124,7 @@ ALL_DDL = [
("supplements", DDL_SUPPLEMENTS),
("spec_rows", DDL_SPEC_ROWS),
("spec_history", DDL_SPEC_HISTORY),
("chunks", DDL_CHUNKS),
]
+6 -3
View File
@@ -59,7 +59,7 @@
<body>
<div class="topbar">
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.15</span></span>
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.17</span></span>
</div>
<div class="content">
@@ -176,8 +176,11 @@
xhr.onload = function() {
if (xhr.status === 200) {
try { resolve(JSON.parse(xhr.responseText)); }
catch(e) { reject(new Error('Bad JSON')); }
try {
var resp = JSON.parse(xhr.responseText);
if (resp.error) { reject(new Error(resp.error)); return; }
resolve(resp);
} catch(e) { reject(new Error('Bad JSON')); }
} else {
reject(new Error('HTTP ' + xhr.status));
}