diff --git a/site/app.py b/site/app.py index 56808e4..28ab6ab 100644 --- a/site/app.py +++ b/site/app.py @@ -77,7 +77,7 @@ class ContractsApp: def _add_routes(self): self.app.add_url_rule("/", "index", self._index, methods=["GET", "POST"]) self.app.add_url_rule("/parse/", "parse", self._parse, methods=["GET"]) - self.app.add_url_rule("/chunk", "chunk", self._chunk_upload, methods=["POST"]) + self.app.add_url_rule("/chunk_json", "chunk_json", self._chunk_json, methods=["POST"]) self.app.add_url_rule("/upload", "upload_json", self._upload_json, methods=["POST"]) self.app.add_url_rule("/health", "health", self._health) self.app.register_blueprint(test_bp) @@ -203,7 +203,85 @@ class ContractsApp: _log("upload_outer_error", str(e)) return jsonify({"error": f"Крах: {e}"}), 500 - # ── Чанковая загрузка ────────────────────────────────────── + # ── Чанковая загрузка base64 (10KB) ─────────────────────── + + def _chunk_json(self): + """Принять чанк base64. На последнем — собрать и сохранить.""" + data = request.get_json(silent=True) or {} + upload_id = data.get("upload_id", "") + filename = data.get("filename", "") + chunk_index = data.get("chunk_index", 0) + total_chunks = data.get("total_chunks", 1) + b64_piece = data.get("data", "") + cid = data.get("cid") + + if not upload_id or not b64_piece: + return jsonify({"error": "Нет upload_id или data"}), 400 + + # Храним в той же таблице chunks (chunk_data = base64 кусок как текст) + 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, b64_piece.encode(), filename, "", total_chunks, cid), + ) + + 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 received == total_chunks: + try: + rows_res, _ = db.query( + "SELECT chunk_data FROM chunks WHERE upload_id=%s ORDER BY chunk_index", + (upload_id,), + ) + full_b64 = b"".join(r[0] for r in rows_res["rows"]).decode() + + # Получить метаданные + meta, _ = db.query_one("SELECT filename, cid FROM chunks WHERE upload_id=%s LIMIT 1", (upload_id,)) + fname = meta["filename"] if meta else filename + fcid = meta["cid"] if meta else cid + + conn, err = db.connect() + if err: + return jsonify({"error": f"БД: {err}"}), 500 + try: + cur = conn.cursor() + if fcid: + contract_id = fcid + else: + 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] + + mime = mimeutil.guess_mime(fname) or "application/octet-stream" + cur.execute( + "INSERT INTO documents (filename, mime_type, original_b64, status) VALUES (%s,%s,%s,'uploaded')", + (fname, mime, full_b64), + ) + cur.execute("SELECT id FROM documents WHERE filename=%s AND status='uploaded' ORDER BY created_at DESC LIMIT 1", (fname,)) + row = cur.fetchone() + doc_id = row[0] if row else None + if doc_id: + cur.execute( + "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')", + (str(contract_id), str(doc_id)), + ) + conn.commit() + cur.close() + finally: + db.put_conn(conn) + 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": received, "total": total_chunks}) + + # ── Старая чанковая загрузка ─────────────────────────────── def _chunk_upload(self): """Принять чанк. Хранить в БД. На последнем — собрать и сохранить.""" diff --git a/site/templates/upload.html b/site/templates/upload.html index a15d391..b8bb68f 100644 --- a/site/templates/upload.html +++ b/site/templates/upload.html @@ -59,7 +59,7 @@
Nubes - Сверка договоров v1.32 + Сверка договоров v1.33
@@ -155,64 +155,54 @@ window.removeFile = removeFile; - // ── Загрузка base64 (JSON) ────────────────────────────────── + // ── Чанковая загрузка base64 (10KB) ────────────────────── - function readFileAsBase64(file, onProgress) { + function uploadFileChunked10k(file, cid, onProgress) { return new Promise(function(resolve, reject) { var reader = new FileReader(); - reader.onprogress = function(e) { - if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 20)); - }; reader.onload = function() { var b64 = reader.result.split(',')[1]; - resolve(b64); + var CHUNK = 10 * 1024; + var totalChunks = Math.ceil(b64.length / CHUNK); + var uploadId = file.name + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); + var done = 0; + + function sendChunk(i) { + if (i >= totalChunks) { resolve({contract_id: cid}); return; } + var piece = b64.substring(i * CHUNK, (i + 1) * CHUNK); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/chunk_json'); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.timeout = 30000; + xhr.onload = function() { + if (xhr.status === 200) { + try { + var resp = JSON.parse(xhr.responseText); + if (resp.error) { reject(new Error(resp.error)); return; } + if (resp.contract_id) cid = resp.contract_id; + done++; + if (onProgress) onProgress(Math.round(done / totalChunks * 100)); + sendChunk(i + 1); + } catch(e) { reject(new Error('Bad JSON')); } + } else { reject(new Error('HTTP ' + xhr.status)); } + }; + xhr.onerror = function() { reject(new Error('Сеть')); }; + xhr.ontimeout = function() { reject(new Error('Таймаут')); }; + xhr.send(JSON.stringify({ + upload_id: uploadId, + filename: file.name, + chunk_index: i, + total_chunks: totalChunks, + data: piece, + cid: cid || null + })); + } + sendChunk(0); }; reader.onerror = function() { reject(new Error('Read error')); }; reader.readAsDataURL(file); }); } - - function uploadFileJSON(file, cid, onProgress) { - return new Promise(function(resolve, reject) { - readFileAsBase64(file, onProgress).then(function(b64) { - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/upload'); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.timeout = 120000; - - xhr.upload.onprogress = function(e) { - if (e.lengthComputable && onProgress) { - onProgress(20 + Math.round(e.loaded / e.total * 80)); - } - }; - - xhr.onload = function() { - if (xhr.status === 200) { - 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)); - } - }; - xhr.onerror = function() { reject(new Error('Сеть')); }; - xhr.ontimeout = function() { reject(new Error('Таймаут')); }; - - xhr.send(JSON.stringify({ - filename: file.name, - data: b64, - cid: cid || null - })); - // ВРЕМЕННО: через /test (работает) - /* - xhr.open('POST', '/test'); - xhr.setRequestHeader('Content-Type', 'application/json'); - var sql = "INSERT INTO documents (filename,mime_type,original_bytes,status) VALUES ('"+file.name+"','application/octet-stream',decode('"+b64+"','base64'),'uploaded')"; - xhr.send(JSON.stringify({action:'exec',sql:sql})); - */ - }).catch(reject); }); } @@ -237,11 +227,11 @@ var startTime = Date.now(); try { - var resp = await uploadFileJSON(f, contractId, function(pct) { + var resp = await uploadFileChunked10k(f, contractId, function(pct) { fileQueue[rowIdx].status = '↑ ' + pct + '%'; renderTable(); }); - // uploadFileChunked возвращает {contract_id} только на последнем чанке + // uploadFileChunked10k: прогресс по чанкам // но этот же resp приходит только с последнего чанка if (resp && resp.contract_id) { contractId = resp.contract_id;