Add convert mode batch workflow with full progress
This commit is contained in:
@@ -4,12 +4,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initSourceType();
|
||||
initApiSource();
|
||||
initUpload();
|
||||
initConvertMode();
|
||||
initTabs();
|
||||
initFilters();
|
||||
loadParsersInfo();
|
||||
});
|
||||
|
||||
let sourceType = 'archive';
|
||||
let convertFiles = [];
|
||||
let isConvertRunning = false;
|
||||
let apiConnectPayload = null;
|
||||
let collectionJob = null;
|
||||
let collectionJobPollTimer = null;
|
||||
@@ -29,7 +32,13 @@ function initSourceType() {
|
||||
}
|
||||
|
||||
function setSourceType(nextType) {
|
||||
sourceType = nextType === 'api' ? 'api' : 'archive';
|
||||
if (nextType === 'api') {
|
||||
sourceType = 'api';
|
||||
} else if (nextType === 'convert') {
|
||||
sourceType = 'convert';
|
||||
} else {
|
||||
sourceType = 'archive';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.source-switch-btn').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.sourceType === sourceType);
|
||||
@@ -37,8 +46,12 @@ function setSourceType(nextType) {
|
||||
|
||||
const archiveContent = document.getElementById('archive-source-content');
|
||||
const apiSourceContent = document.getElementById('api-source-content');
|
||||
const convertSourceContent = document.getElementById('convert-source-content');
|
||||
archiveContent.classList.toggle('hidden', sourceType !== 'archive');
|
||||
apiSourceContent.classList.toggle('hidden', sourceType !== 'api');
|
||||
if (convertSourceContent) {
|
||||
convertSourceContent.classList.toggle('hidden', sourceType !== 'convert');
|
||||
}
|
||||
}
|
||||
|
||||
function initApiSource() {
|
||||
@@ -599,6 +612,295 @@ async function uploadFile(file) {
|
||||
}
|
||||
}
|
||||
|
||||
function initConvertMode() {
|
||||
const folderInput = document.getElementById('convert-folder-input');
|
||||
const runButton = document.getElementById('convert-run-btn');
|
||||
if (!folderInput || !runButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
folderInput.addEventListener('change', () => {
|
||||
convertFiles = Array.from(folderInput.files || []).filter(file => file && file.name);
|
||||
renderConvertSummary();
|
||||
});
|
||||
|
||||
runButton.addEventListener('click', async () => {
|
||||
await runConvertBatch();
|
||||
});
|
||||
renderConvertSummary();
|
||||
}
|
||||
|
||||
function renderConvertSummary() {
|
||||
const summary = document.getElementById('convert-folder-summary');
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (convertFiles.length === 0) {
|
||||
summary.textContent = 'Выберите папку с файлами, включая вложенные каталоги.';
|
||||
summary.className = 'api-connect-status';
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedFiles = convertFiles.filter(file => isSupportedConvertFileName(file.webkitRelativePath || file.name));
|
||||
const skippedCount = convertFiles.length - supportedFiles.length;
|
||||
const previewCount = 5;
|
||||
const previewFiles = supportedFiles.slice(0, previewCount).map(file => escapeHtml(file.webkitRelativePath || file.name));
|
||||
const remaining = supportedFiles.length - previewFiles.length;
|
||||
const previewText = previewFiles.length > 0 ? `Примеры: ${previewFiles.join(', ')}` : '';
|
||||
const skippedText = skippedCount > 0 ? ` Пропущено неподдерживаемых: ${skippedCount}.` : '';
|
||||
|
||||
summary.innerHTML = `<strong>${supportedFiles.length}</strong> файлов готовы к конвертации.${previewText ? ` ${previewText}` : ''}${remaining > 0 ? ` и ещё ${remaining}` : ''}.${skippedText}`;
|
||||
summary.className = 'api-connect-status';
|
||||
}
|
||||
|
||||
async function runConvertBatch() {
|
||||
const runButton = document.getElementById('convert-run-btn');
|
||||
if (!runButton || isConvertRunning) {
|
||||
return;
|
||||
}
|
||||
if (convertFiles.length === 0) {
|
||||
renderConvertStatus('Нет файлов для конвертации', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedFiles = convertFiles.filter(file => isSupportedConvertFileName(file.webkitRelativePath || file.name));
|
||||
if (supportedFiles.length === 0) {
|
||||
renderConvertStatus('В выбранной папке нет файлов поддерживаемого типа', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
isConvertRunning = true;
|
||||
runButton.disabled = true;
|
||||
renderConvertProgress(0, 'Подготовка загрузки...');
|
||||
renderConvertStatus('Выполняю пакетную конвертацию...', 'info');
|
||||
|
||||
const formData = new FormData();
|
||||
supportedFiles.forEach(file => {
|
||||
const relativePath = file.webkitRelativePath || file.name || 'file';
|
||||
formData.append('files[]', file, relativePath);
|
||||
});
|
||||
|
||||
try {
|
||||
const startResponse = await uploadConvertBatch(formData, (percent) => {
|
||||
const uploadPercent = Math.round(percent * 0.3);
|
||||
renderConvertProgress(uploadPercent, `Загрузка файлов: ${percent}%`);
|
||||
});
|
||||
|
||||
if (!startResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(startResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(errorPayload.error || 'Пакетная конвертация завершилась с ошибкой', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startResponse.jobId) {
|
||||
hideConvertProgress();
|
||||
renderConvertStatus('Сервер не вернул идентификатор задачи', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForConvertJob(startResponse.jobId, (statusPayload) => {
|
||||
const serverProgress = Number(statusPayload.progress || 0);
|
||||
const combined = 30 + Math.round(Math.max(0, Math.min(100, serverProgress)) * 0.7);
|
||||
renderConvertProgress(combined, `Конвертация: ${serverProgress}%`);
|
||||
});
|
||||
|
||||
renderConvertProgress(100, 'Подготовка выгрузки...');
|
||||
const downloadResponse = await downloadConvertArchive(startResponse.jobId);
|
||||
if (!downloadResponse.ok) {
|
||||
const errorPayload = parseConvertErrorPayload(downloadResponse.bodyText);
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(errorPayload.error || 'Не удалось скачать результат', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = downloadResponse.blob;
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
downloadBlob(blob, `logpile-convert-${timestamp}.zip`);
|
||||
const summary = downloadResponse.summaryHeader || 'Конвертация завершена';
|
||||
hideConvertProgress();
|
||||
renderConvertStatus(summary, 'success');
|
||||
} catch (err) {
|
||||
hideConvertProgress();
|
||||
renderConvertStatus('Ошибка соединения при конвертации', 'error');
|
||||
} finally {
|
||||
isConvertRunning = false;
|
||||
runButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadConvertBatch(formData, onUploadPercent) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/convert');
|
||||
xhr.responseType = 'text';
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
return;
|
||||
}
|
||||
const percent = Math.max(0, Math.min(100, Math.round((event.loaded / event.total) * 100)));
|
||||
onUploadPercent(percent);
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
let body = {};
|
||||
try {
|
||||
body = JSON.parse(xhr.responseText || '{}');
|
||||
} catch (err) {
|
||||
body = {};
|
||||
}
|
||||
resolve({
|
||||
ok: true,
|
||||
status: xhr.status,
|
||||
jobId: body.job_id || ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
ok: false,
|
||||
status: xhr.status,
|
||||
bodyText: xhr.responseText || ''
|
||||
});
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('network'));
|
||||
});
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForConvertJob(jobId, onProgress) {
|
||||
while (true) {
|
||||
const response = await fetch(`/api/convert/${encodeURIComponent(jobId)}`);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || 'Не удалось получить статус конвертации');
|
||||
}
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(payload);
|
||||
}
|
||||
|
||||
const status = String(payload.status || '').toLowerCase();
|
||||
if (status === 'success') {
|
||||
return payload;
|
||||
}
|
||||
if (status === 'failed' || status === 'canceled') {
|
||||
throw new Error(payload.error || 'Конвертация завершилась ошибкой');
|
||||
}
|
||||
|
||||
await delay(900);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadConvertArchive(jobId) {
|
||||
const response = await fetch(`/api/convert/${encodeURIComponent(jobId)}/download`);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
bodyText: await response.text().catch(() => '')
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
blob: await response.blob(),
|
||||
summaryHeader: response.headers.get('X-Convert-Summary') || ''
|
||||
};
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function parseConvertErrorPayload(bodyText) {
|
||||
if (!bodyText) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(bodyText);
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function isSupportedConvertFileName(filename) {
|
||||
const name = String(filename || '').trim().toLowerCase();
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
name.endsWith('.zip') ||
|
||||
name.endsWith('.tar') ||
|
||||
name.endsWith('.tar.gz') ||
|
||||
name.endsWith('.tgz') ||
|
||||
name.endsWith('.json') ||
|
||||
name.endsWith('.txt') ||
|
||||
name.endsWith('.log')
|
||||
);
|
||||
}
|
||||
|
||||
function renderConvertStatus(message, status) {
|
||||
const statusNode = document.getElementById('convert-status');
|
||||
if (!statusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusNode.textContent = message || '';
|
||||
statusNode.className = 'api-connect-status';
|
||||
if (status === 'success') {
|
||||
statusNode.classList.add('success');
|
||||
} else if (status === 'error') {
|
||||
statusNode.classList.add('error');
|
||||
} else if (status === 'info') {
|
||||
statusNode.classList.add('info');
|
||||
}
|
||||
}
|
||||
|
||||
function renderConvertProgress(percent, label) {
|
||||
const wrap = document.getElementById('convert-progress');
|
||||
const bar = document.getElementById('convert-progress-bar');
|
||||
const value = document.getElementById('convert-progress-value');
|
||||
const text = document.getElementById('convert-progress-label');
|
||||
if (!wrap || !bar || !value || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
wrap.classList.remove('hidden');
|
||||
bar.style.width = `${safePercent}%`;
|
||||
value.textContent = `${safePercent}%`;
|
||||
text.textContent = label || 'Выполняется...';
|
||||
}
|
||||
|
||||
function hideConvertProgress() {
|
||||
const wrap = document.getElementById('convert-progress');
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
wrap.classList.add('hidden');
|
||||
}
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Tab navigation
|
||||
function initTabs() {
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
|
||||
Reference in New Issue
Block a user