From bb2811abb01bd4f81a27a1f86d40087ffe615d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 23 Jun 2026 08:24:35 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.123:=20fix=20ZIP=20=E2=80=94=20cgi.FieldSt?= =?UTF-8?q?orage=20for=20multipart,=20CORS=20on=20all=20responses,=20rever?= =?UTF-8?q?t=20Lucee=20unzip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/convert_server.py | 60 ++++++++++++-------- index.cfm | 20 ++----- unzip.cfm | 116 --------------------------------------- 3 files changed, 42 insertions(+), 154 deletions(-) delete mode 100644 unzip.cfm diff --git a/deploy/convert_server.py b/deploy/convert_server.py index 24d01c5..35dd2b5 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -279,37 +279,40 @@ class Handler(BaseHTTPRequestHandler): def _handle_unzip_upload(self): """Распаковать ZIP и загрузить каждый файл в Lucee. Только файлы из корня архива.""" - import zipfile, io + import zipfile, io, cgi, tempfile - # Читаем multipart form data + # CORS preflight уже обработан nginx, но на всякий случай + self._send_cors() + + # Парсим multipart через cgi.FieldStorage content_type = self.headers.get("Content-Type", "") length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) - # Извлекаем ZIP из multipart - boundary = content_type.split("boundary=")[1].strip() - parts = body.split(b"--" + boundary.encode()) - zip_data = None - for part in parts: - if b"filename=" in part: - # Отделяем заголовки от тела - header_end = part.find(b"\r\n\r\n") - if header_end > 0: - zip_data = part[header_end+4:] - # Убираем trailing \r\n и boundary - if zip_data.endswith(b"\r\n"): - zip_data = zip_data[:-2] - break + # Сохраняем тело во временный файл для cgi.FieldStorage + fp = tempfile.TemporaryFile() + fp.write(body) + fp.seek(0) - if not zip_data: - self.send_error(400, "no file in request") + env = os.environ.copy() + env["REQUEST_METHOD"] = "POST" + env["CONTENT_TYPE"] = content_type + env["CONTENT_LENGTH"] = str(length) + + form = cgi.FieldStorage(fp=fp, environ=env, keep_blank_values=True) + file_item = form.getfirst("file") or form.getfirst("files") + if not file_item or not file_item.file: + self._send_error_cors(400, "no file in request") + fp.close() return + zip_data = file_item.file.read() + fp.close() + results = [] try: with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: for name in zf.namelist(): - # Только файлы из корня (без папок), пропускаем директории if name.endswith('/') or '/' in name: continue content = zf.read(name) @@ -317,7 +320,6 @@ class Handler(BaseHTTPRequestHandler): if ext not in ('docx', 'doc', 'pdf'): continue - # Загрузить в Lucee (hex с \x префиксом для PostgreSQL bytea) content_hex = "\\x" + content.hex() resp = httpx.post( f"{LUCEE_URL}/upload.cfm", @@ -339,18 +341,30 @@ class Handler(BaseHTTPRequestHandler): results.append({"filename": name, "error": f"HTTP {resp.status_code}"}) except zipfile.BadZipFile: - self.send_error(400, "not a valid ZIP file") + self._send_error_cors(400, "not a valid ZIP file") return except Exception as e: - self.send_error(500, str(e)) + self._send_error_cors(500, str(e)) return self.send_response(200) + self._send_cors() self.send_header("Content-Type", "application/json; charset=utf-8") - self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"ok": True, "files": results}, ensure_ascii=False).encode()) + def _send_cors(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "*") + + def _send_error_cors(self, code, message): + self.send_response(code) + self._send_cors() + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + self.wfile.write(json.dumps({"ok": False, "error": message}, ensure_ascii=False).encode()) + def _handle_doc_convert(self): length = int(self.headers.get("Content-Length", 0)) data = self.rfile.read(length) diff --git a/index.cfm b/index.cfm index ef3e648..0992a9f 100644 --- a/index.cfm +++ b/index.cfm @@ -75,7 +75,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v1.0.122 — Lucee + Сверка договоров — LLM AI-driven Event Sourcing v1.0.123 — Lucee
@@ -176,6 +176,7 @@ lucide.createIcons(); var UPLOAD_URL = 'https://contracts.kube5s.ru/lucee/upload.cfm'; var CONVERT_URL = 'https://contracts.kube5s.ru/convert-doc'; +var UNZIP_URL = 'https://contracts.kube5s.ru/unzip-upload'; var SITE_URL = ''; // same origin for api calls var fileInput = document.getElementById('fileInput'); @@ -258,20 +259,9 @@ fileInput.addEventListener('change', async function() { renderTable(); try { - // Читаем ZIP через FileReader → base64 - var zipData = await new Promise(function(resolve, reject) { - var reader = new FileReader(); - reader.onload = function() { resolve(reader.result); }; - reader.onerror = function() { reject(new Error('FileReader failed')); }; - reader.readAsDataURL(f); - }); - var zipB64 = zipData.split(',')[1]; // убрать префикс data:application/zip;base64, - - var zipResp = await fetch('/unzip.cfm', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({filename: f.name, data: zipB64, contract_id: contractId || ''}) - }); + var fd = new FormData(); + fd.append('file', f); + var zipResp = await fetch(UNZIP_URL, { method: 'POST', body: fd }); var zipData = await zipResp.json(); if (!zipData.ok || !zipData.files) throw new Error('unzip failed'); diff --git a/unzip.cfm b/unzip.cfm deleted file mode 100644 index 7f9445c..0000000 --- a/unzip.cfm +++ /dev/null @@ -1,116 +0,0 @@ - - - - - {"ok":false,"error":"POST required"} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SELECT s.id as sid, s.document_id FROM supplements s - JOIN documents d ON d.id = s.document_id - WHERE s.contract_id = - AND d.filename = - - - DELETE FROM supplements WHERE id = - DELETE FROM documents WHERE id = - - - - INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES ( - , - , - decode(, 'hex'), - 'uploaded' - ) - - - SELECT id FROM documents WHERE filename= AND status='uploaded' ORDER BY created_at DESC LIMIT 1 - - - - INSERT INTO contracts (number, client) VALUES (,'') - - - SELECT id FROM contracts ORDER BY created_at DESC LIMIT 1 - - - - - INSERT INTO supplements (contract_id, document_id, type) VALUES ( - , - , - - ) - - - - - - - - - - - - - -#serializeJSON(result)#