чанки в БД (таблица 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
+56 -44
View File
@@ -17,9 +17,6 @@ from api import api_bp
load_dotenv() load_dotenv()
# Хранилище чанков (в памяти, теряется при рестарте)
_chunk_store = {}
def _save_file_to_db(filename, file_bytes, mime, contract_id): def _save_file_to_db(filename, file_bytes, mime, contract_id):
"""Сохранить один файл в БД: documents + supplements. Возвращает doc_id или None.""" """Сохранить один файл в БД: documents + supplements. Возвращает doc_id или None."""
@@ -104,63 +101,78 @@ class ContractsApp:
# ── Чанковая загрузка ────────────────────────────────────── # ── Чанковая загрузка ──────────────────────────────────────
def _chunk_upload(self): def _chunk_upload(self):
"""Принять чанк файла. На последнем — собрать и сохранить.""" """Принять чанк. Хранить в БД. На последнем — собрать и сохранить."""
upload_id = request.form.get("upload_id", "") upload_id = request.form.get("upload_id", "")
filename = request.form.get("filename", "") filename = request.form.get("filename", "")
chunk_index = int(request.form.get("chunk_index", 0)) chunk_index = int(request.form.get("chunk_index", 0))
total_chunks = int(request.form.get("total_chunks", 1)) 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") chunk_file = request.files.get("chunk")
if not chunk_file: if not chunk_file:
return jsonify({"error": "Нет чанка"}), 400 return jsonify({"error": "Нет чанка"}), 400
chunk_data = chunk_file.read() chunk_data = chunk_file.read()
mime = mimeutil.guess_mime(filename) or "application/octet-stream"
if upload_id not in _chunk_store: # Сохранить чанк в БД (ON CONFLICT — идемпотентно)
_chunk_store[upload_id] = { db.execute(
"filename": filename, "INSERT INTO chunks (upload_id, chunk_index, chunk_data, filename, mime, total_chunks, cid) "
"mime": mimeutil.guess_mime(filename) or "application/octet-stream", "VALUES (%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (upload_id, chunk_index) DO NOTHING",
"chunks": [None] * total_chunks, (upload_id, chunk_index, chunk_data, filename, mime, total_chunks, cid),
"total": total_chunks, )
"cid": cid,
"received": 0,
}
store = _chunk_store[upload_id] # Сколько уже получено
if store["chunks"][chunk_index] is None: count_res, _ = db.query(
store["chunks"][chunk_index] = chunk_data "SELECT COUNT(*) FROM chunks WHERE upload_id=%s", (upload_id,)
store["received"] += 1 )
received = count_res["rows"][0][0] if count_res else 0
if store["received"] == total_chunks: if received == total_chunks:
# Собрать файл try:
file_bytes = b"".join(store["chunks"]) # Собрать все чанки по порядку
mime = store["mime"] rows_res, _ = db.query(
fname = store["filename"] "SELECT chunk_data FROM chunks WHERE upload_id=%s ORDER BY chunk_index",
cid = store["cid"] (upload_id,),
# Создать контракт если нет
if cid:
contract_id = cid
else:
conn, err = db.connect()
if err:
del _chunk_store[upload_id]
return jsonify({"error": f"БД: {err}"}), 500
cur = conn.cursor()
cur.execute(
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
("б" + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""),
) )
contract_id = cur.fetchone()[0] file_bytes = b"".join(r[0] for r in rows_res["rows"])
conn.commit()
cur.close() # Получить метаданные из первой записи
conn.close() 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 fcid:
contract_id = fcid
else:
conn, err = db.connect()
if err:
return jsonify({"error": f"БД: {err}"}), 500
cur = conn.cursor()
cur.execute(
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
("б" + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""),
)
contract_id = cur.fetchone()[0]
conn.commit()
cur.close()
conn.close()
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,))
_save_file_to_db(fname, file_bytes, mime, str(contract_id))
del _chunk_store[upload_id]
return jsonify({"contract_id": str(contract_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): 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 ──────────────────────────────────────────── # ── Список всех DDL ────────────────────────────────────────────
ALL_DDL = [ ALL_DDL = [
@@ -106,6 +124,7 @@ ALL_DDL = [
("supplements", DDL_SUPPLEMENTS), ("supplements", DDL_SUPPLEMENTS),
("spec_rows", DDL_SPEC_ROWS), ("spec_rows", DDL_SPEC_ROWS),
("spec_history", DDL_SPEC_HISTORY), ("spec_history", DDL_SPEC_HISTORY),
("chunks", DDL_CHUNKS),
] ]
+6 -3
View File
@@ -59,7 +59,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes"> <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>
<div class="content"> <div class="content">
@@ -176,8 +176,11 @@
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
try { resolve(JSON.parse(xhr.responseText)); } try {
catch(e) { reject(new Error('Bad JSON')); } 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 { } else {
reject(new Error('HTTP ' + xhr.status)); reject(new Error('HTTP ' + xhr.status));
} }