Files
contracts-app/v2/chunk_upload.js
T

214 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* v2/chunk_upload.js — Клиентская загрузка файла чанками по 32KB.
*
* Использование:
* <input type="file" id="file-input">
* <div id="upload-ui"></div>
* <script src="/static/v2/chunk_upload.js"></script>
*
* После успешной загрузки появляется кнопка «Парсинг».
*/
(function () {
"use strict";
const CHUNK_SIZE = 32 * 1024; // 32 KB
// ── MD5 (встроенная реализация без зависимостей) ──────────────────────────
// RFC 1321 — используется только для checksum, не для безопасности.
function md5(buffer) {
// SparkMD5 должен быть подключён отдельно, либо используем SubtleCrypto
// Здесь используем встроенный Web Crypto API (SHA-256 недостаточен — нужен MD5)
// Простая MD5 реализация:
return SparkMD5.ArrayBuffer.hash(buffer);
}
// ── UI ────────────────────────────────────────────────────────────────────
function getUI() {
return document.getElementById("upload-ui");
}
function setStatus(html) {
const ui = getUI();
if (ui) ui.innerHTML = html;
}
function renderProgress(filename, pct, elapsed) {
return `
<div class="upload-progress">
<strong>${escHtml(filename)}</strong><br>
<progress value="${pct}" max="100" style="width:100%"></progress>
<span>${pct}% &nbsp;|&nbsp; ${elapsed}с</span>
</div>`;
}
function escHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// ── XHR helper ────────────────────────────────────────────────────────────
function post(url, body) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.timeout = 15000;
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
try { resolve(JSON.parse(xhr.responseText)); }
catch (e) { reject(new Error("bad json: " + xhr.responseText)); }
} else {
reject(new Error("HTTP " + xhr.status + ": " + xhr.responseText));
}
};
xhr.onerror = function () { reject(new Error("network error")); };
xhr.ontimeout = function () { reject(new Error("timeout")); };
xhr.send(JSON.stringify(body));
});
}
// ── Base64 encode ArrayBuffer ─────────────────────────────────────────────
function toBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// ── Генерация upload_id ───────────────────────────────────────────────────
function genId() {
return "u" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
// ── Основная функция загрузки ─────────────────────────────────────────────
async function uploadFile(file) {
const uploadId = genId();
const total = Math.ceil(file.size / CHUNK_SIZE);
const startTs = Date.now();
// Считаем MD5 всего файла через SparkMD5
const fullBuffer = await file.arrayBuffer();
const checksum = md5(fullBuffer);
setStatus(renderProgress(file.name, 0, 0));
for (let i = 0; i < total; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const slice = fullBuffer.slice(start, end);
const b64 = toBase64(slice);
const elapsed = Math.round((Date.now() - startTs) / 1000);
const pct = Math.round(((i + 1) / total) * 90); // до 90%, финал = 100%
setStatus(renderProgress(file.name, pct, elapsed));
let res;
try {
res = await post("/v2/chunk", {
upload_id: uploadId,
index: i,
total: total,
data: b64,
});
} catch (e) {
setStatus(`<p class="error">Ошибка чанка ${i}: ${escHtml(e.message)}</p>`);
return;
}
if (!res.ok) {
setStatus(`<p class="error">Сервер отклонил чанк ${i}: ${escHtml(res.error)}</p>`);
return;
}
}
// Финализация
const elapsed = Math.round((Date.now() - startTs) / 1000);
setStatus(renderProgress(file.name, 95, elapsed));
let fin;
try {
fin = await post("/v2/finalize", {
upload_id: uploadId,
filename: file.name,
mime_type: file.type || "application/octet-stream",
total: total,
checksum: checksum,
});
} catch (e) {
setStatus(`<p class="error">Ошибка финализации: ${escHtml(e.message)}</p>`);
return;
}
if (!fin.ok) {
setStatus(`<p class="error">Финализация не удалась: ${escHtml(fin.error)}</p>`);
return;
}
const docId = fin.doc_id;
const done = Math.round((Date.now() - startTs) / 1000);
setStatus(`
<div class="upload-done">
<strong>${escHtml(file.name)}</strong> загружен за ${done}с.<br>
<button id="btn-parse" data-docid="${escHtml(docId)}">Парсинг</button>
<div id="parse-status"></div>
</div>`);
document.getElementById("btn-parse").addEventListener("click", function () {
pollStatus(docId);
});
}
// ── Опрос статуса парсинга ────────────────────────────────────────────────
function pollStatus(docId) {
const ps = document.getElementById("parse-status");
if (ps) ps.textContent = "Парсинг запущен, ожидаем…";
let attempts = 0;
const timer = setInterval(async function () {
attempts++;
try {
const r = await fetch("/v2/status/" + docId);
const d = await r.json();
if (d.status === "parsed") {
clearInterval(timer);
if (ps) ps.innerHTML = `<span style="color:green">Готово: ${escHtml(d.filename)}</span>`;
} else if (d.status === "error") {
clearInterval(timer);
if (ps) ps.innerHTML = `<span style="color:red">Ошибка: ${escHtml(d.error || "unknown")}</span>`;
} else {
if (ps) ps.textContent = `Статус: ${d.status} (${attempts * 2}с)`;
}
} catch (e) {
if (ps) ps.textContent = `Ошибка опроса: ${e.message}`;
}
if (attempts > 60) { clearInterval(timer); }
}, 2000);
}
// ── Инициализация ─────────────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", function () {
const input = document.getElementById("file-input");
if (!input) return;
input.addEventListener("change", function () {
const file = input.files[0];
if (!file) return;
uploadFile(file).catch(function (e) {
setStatus(`<p class="error">Критическая ошибка: ${escHtml(e.message)}</p>`);
});
});
});
})();