diff --git a/site/routes/upload_bp.py b/site/routes/upload_bp.py index ca88aa3..fba2384 100644 --- a/site/routes/upload_bp.py +++ b/site/routes/upload_bp.py @@ -1,7 +1,7 @@ """Upload blueprint — загрузка, конвертация, распаковка.""" import io, os, base64, hashlib, zipfile import httpx -from flask import Blueprint, request, jsonify, send_file +from flask import Blueprint, request, jsonify from services.parse import parse_file from db import documents import config diff --git a/site/static/files.js b/site/static/files.js index da6e81f..d6c2a7e 100644 --- a/site/static/files.js +++ b/site/static/files.js @@ -12,8 +12,6 @@ * - statusToHTML() — рендер статуса (↑ N%, ✓) * - renderFiles() — рендер всей таблицы -var CONVERT_URL = '/convert-doc'; - /** * statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1). * @@ -206,49 +204,6 @@ window.toggleClassifyDetail = async function(i) { } }; -/** - * convertDoc(file, onProgress) — .doc → .docx через ВМ-буфер (паттерн drhider). - * Фаза 1: PUT .doc на ВМ. Фаза 2: /api/convert_refs → pull → конвертация → .docx. - */ -function convertDoc(file, onProgress) { - var startTime = Date.now(); - if (onProgress) onProgress({ kind: 'converting', elapsed: 0 }); - var token = crypto.randomUUID(); - var vmUrl = VM_UPLOAD_URL + token + '_0'; - - var timer = setInterval(function() { - var elapsed = Math.floor((Date.now() - startTime) / 1000); - if (onProgress) onProgress({ kind: 'converting', elapsed: elapsed }); - }, 1000); - - return fetch(vmUrl, { method: 'PUT', body: file, headers: { 'Content-Type': 'application/octet-stream' } }) - .then(function(r) { - if (!r.ok) throw new Error('Конвертация: VM upload HTTP ' + r.status); - return fetch('/api/convert_refs?_=' + Date.now(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ files: [{ name: file.name, size: file.size, url: vmUrl }] }) - }); - }) - .then(function(r) { - if (!r.ok) throw new Error('Конвертация: HTTP ' + r.status); - return r.blob(); - }) - .then(function(blob) { - clearInterval(timer); - if (blob.size === 0) throw new Error('Конвертация: пустой результат'); - return new File([blob], file.name.replace(/\.doc$/i, '.docx'), { - type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - lastModified: Date.now() - }); - }) - .catch(function(e) { - clearInterval(timer); - if (e.message === 'Failed to fetch' || e.name === 'TypeError') throw new Error('Конвертация: Сеть'); - throw e; - }); -} - /** * ⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔ uploadFile — загрузка через ВМ-буфер (паттерн drhider). * @@ -380,158 +335,6 @@ function applyParseResult(entry, parsed, elapsed) { } } -/** - * addZipFile(file) — Async-action: обработка ZIP-файла (Фаза 1, упрощена). - * - * 1. Отправляет ZIP на бэкенд (только распаковка, без БД/парсинга) - * 2. Для каждого файла из архива: base64 → Blob → File → addRegularFile() - * - * addRegularFile делает ВСЁ: upload, parse, дубликаты, статусы, render. - * Никакого дублирования логики — единый путь для обычных и ZIP-файлов. - */ -async function addZipFile(file) { - var zipEntry = { name: file.name, lastModified: file.lastModified, size: file.size, status: { kind: 'unzipping' } }; - state.files.push(zipEntry); - render(state); - - try { - // Шаг 1: распаковать ZIP через ВМ-буфер (паттерн drhider) - var token = crypto.randomUUID(); - var vmUrl = VM_UPLOAD_URL + token + '_0'; - var putResp = await fetch(vmUrl, { method: 'PUT', body: file, headers: { 'Content-Type': 'application/octet-stream' } }); - if (!putResp.ok) throw new Error('unzip failed: VM upload HTTP ' + putResp.status); - var refsResp = await fetch('/api/unzip_refs?_=' + Date.now(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ files: [{ name: file.name, size: file.size, url: vmUrl }] }) - }); - if (!refsResp.ok) throw new Error('unzip failed: HTTP ' + refsResp.status); - var zipResp = await refsResp.json(); - if (!zipResp.ok || !zipResp.files) throw new Error('unzip failed'); - - // Подтверждение: показать первые 10 файлов + итог - var preview = zipResp.files.slice(0, 10).map(function(f) { return ' • ' + f.filename + ' (' + (f.size/1024).toFixed(1) + ' KB)'; }).join('\n'); - if (total > 10) preview += '\n ... и ещё ' + (total - 10) + ' файлов'; - if (!confirm('Архив «' + file.name + '» — ' + total + ' файлов:\n\n' + preview + '\n\nЗагрузить эти файлы?')) { - // Отмена — убрать строку ZIP - var zipPos2 = state.files.indexOf(zipEntry); - if (zipPos2 >= 0) state.files.splice(zipPos2, 1); - render(state); - return; - } - - // Шаг 2: обработать каждый файл из архива - // Временная строка ZIP остаётся в таблице — обновляем её статус - var total = zipResp.files.length; - for (var zi = 0; zi < total; zi++) { - var zf = zipResp.files[zi]; - zipEntry.status = { kind: 'unzipping' }; // обновляем статус - render(state); - - if (zf.error) continue; - - // base64 → File → addRegularFile (единый путь) - var byteStr = atob(zf.data_b64); - var bytes = new Uint8Array(byteStr.length); - for (var b = 0; b < byteStr.length; b++) bytes[b] = byteStr.charCodeAt(b); - var mime = {docx:'application/vnd.openxmlformats-officedocument.wordprocessingml.document',doc:'application/msword',pdf:'application/pdf'}[zf.ext] || 'application/octet-stream'; - var extractedFile = new File([bytes], zf.filename, { type: mime, lastModified: Date.now() }); - - await addRegularFile(extractedFile, file.name); - } - - // Шаг 3: убрать временную строку ZIP (только после успешной обработки всех файлов) - var zipPos = state.files.indexOf(zipEntry); - if (zipPos >= 0) state.files.splice(zipPos, 1); - render(state); - - } catch(ze) { - // Ошибка — показать на строке ZIP (zipEntry всё ещё в state.files) - zipEntry.status = { kind: 'error', text: 'ZIP: ' + ze.message }; - render(state); - } -} - -/** - * addRegularFile(file) — Async-action: обработка обычного файла (Фаза 1). - * - * 1. Добавляет/заменяет файл в state.files - * 2. Загружает через XHR (uploadFile) с индикатором прогресса - * 3. После загрузки: проставляет doc_id, contractId - * 4. applyParseResult — применяет результат парсинга - * - * Мутирует state.files, вызывает render(state) после каждого изменения. - */ -async function addRegularFile(file, zipSource) { - // Создать запись с начальным статусом - var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'connecting', elapsed: 0 }, zip_source: zipSource || null }; - - // ДЕДУПЛИКАЦИЯ: ключ = (zip_source, name), чтобы одноимённые файлы из разных ZIP не затирались - var dupKey = (zipSource || '') + '/' + file.name; - var existingIdx = -1; - for (var j = 0; j < state.files.length; j++) { - var ejKey = (state.files[j].zip_source || '') + '/' + state.files[j].name; - if (ejKey === dupKey) { existingIdx = j; break; } - } - var rowIdx; - if (existingIdx >= 0) { - // Файл с таким именем уже есть — спросить пользователя - if (!confirm('Файл «' + file.name + '» уже есть в списке.\n\nOK — перезаписать\nОтмена — пропустить')) { - return; // пользователь выбрал «пропустить» - } - state.files[existingIdx] = entry; - rowIdx = existingIdx; - } else { - state.files.push(entry); - rowIdx = state.files.length - 1; - } - render(state); - - try { - // .doc → конвертация через libreoffice-сервис - var uploadTarget = file; - if (file.name.toLowerCase().endsWith('.doc')) { - uploadTarget = await convertDoc(file, function(st) { - state.files[rowIdx].status = st; - render(state); - }); - state.files[rowIdx].name = uploadTarget.name; - } - // fetch-загрузка с честным счётчиком времени - var resp = await uploadFile(uploadTarget, function(st) { - state.files[rowIdx].status = st; - render(state); - }, zipSource); - // Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys) - if (resp && resp.contract_id) state.contractId = resp.contract_id; - state.files[rowIdx].doc_id = resp.doc_id; - - // Дубликат? - if (resp && resp.duplicate_of) { - state.files[rowIdx].status = { kind: 'duplicate' }; - render(state); - return; - } - // Warning от парсинга? - if (resp && resp.warning) { - state.files[rowIdx].status = { kind: 'warning', text: resp.warning }; - render(state); - } else { - state.files[rowIdx].status = { kind: 'uploaded' }; - } - state.files[rowIdx].uploaded = true; - - // Авто-парсинг: бэкенд парсит всё (PDF + DOCX + DOC) - var t0 = Date.now(); - var pr = resp.parsed; - var elapsed = pr && pr.status === 'parsed' ? ((Date.now() - t0) / 1000).toFixed(1) : null; - applyParseResult(state.files[rowIdx], pr, elapsed); - } catch(err) { - state.files[rowIdx].status = { kind: 'error', text: err.message }; - } - render(state); -} - /** * finalizeUpload() — Завершение цикла загрузки (Фаза 1). * diff --git a/site/templates/index.html b/site/templates/index.html index e97dd66..e8be084 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -73,7 +73,7 @@