185 lines
9.9 KiB
HTML
185 lines
9.9 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Upload Platform {{ version }}</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
</head>
|
|
<body>
|
|
<!-- Основной контейнер стенда: Слой 1 (выбор) + Слой 2 (пофайловый транзит через RAM) -->
|
|
<main class="page">
|
|
<header>
|
|
<p class="eyebrow">FILE INTAKE & TRANSIT / {{ version }}</p>
|
|
<h1>Загрузка документов</h1>
|
|
<p class="lede">Выберите отдельные файлы или целую папку. Архивы будут распакованы на клиенте, после чего пофайлово доставлены в память сессии через буфер.</p>
|
|
</header>
|
|
|
|
<section class="toolbar" aria-label="Выбор файлов">
|
|
<div id="file-picker"></div>
|
|
</section>
|
|
|
|
<!-- Секция действий Слоя 2: пофайловый транзит в RAM сессии бэкенда -->
|
|
<section class="upload-section" style="margin-top: 24px; padding: 20px; background: #f2f6f1; border-radius: 6px; border: 1px solid #c8d6c7;">
|
|
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
|
<button id="upload-btn" type="button" disabled style="background: #1b4d3e;">
|
|
⚡ Загрузить в сессию (Слой 2 транзит)
|
|
</button>
|
|
<button id="cancel-btn" type="button" class="quiet" style="display: none; color: #8c5148;">
|
|
✕ Отмена
|
|
</button>
|
|
<span id="upload-status" style="font: 14px sans-serif; color: #2d4a36;"></span>
|
|
</div>
|
|
<div id="upload-progress-explanation" class="upload-progress-explanation" hidden>
|
|
<strong id="upload-phase">Подготовка загрузки</strong>
|
|
<span id="upload-phase-help">Первое соединение может занять время: устанавливаются соединения с буфером ВМ и сервисом.</span>
|
|
<span id="upload-elapsed">Прошло: 00:00</span>
|
|
</div>
|
|
<div id="session-info" style="margin-top: 14px; font: 13px sans-serif; display: none;">
|
|
<strong>Сессия в RAM:</strong> <code id="session-id"></code>
|
|
<div id="session-files" style="margin-top: 6px; color: #46604e;"></div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script src="{{ url_for('file_picker_dist', filename='file-picker.iife.js', v=version) }}"></script>
|
|
<script>
|
|
const uploadBtn = document.getElementById('upload-btn');
|
|
const cancelBtn = document.getElementById('cancel-btn');
|
|
const uploadStatus = document.getElementById('upload-status');
|
|
const sessionInfo = document.getElementById('session-info');
|
|
const sessionIdEl = document.getElementById('session-id');
|
|
const sessionFilesEl = document.getElementById('session-files');
|
|
const progressExplanation = document.getElementById('upload-progress-explanation');
|
|
const uploadPhase = document.getElementById('upload-phase');
|
|
const uploadPhaseHelp = document.getElementById('upload-phase-help');
|
|
const uploadElapsed = document.getElementById('upload-elapsed');
|
|
|
|
let abortCtrl = null;
|
|
let selectedFiles = [];
|
|
let uploadTimer = null;
|
|
let uploadStartedAt = 0;
|
|
|
|
const formatElapsed = (seconds) => {
|
|
const minutes = Math.floor(seconds / 60).toString().padStart(2, '0');
|
|
const remainder = (seconds % 60).toString().padStart(2, '0');
|
|
return `${minutes}:${remainder}`;
|
|
};
|
|
|
|
const stopUploadTimer = () => {
|
|
if (uploadTimer) window.clearInterval(uploadTimer);
|
|
uploadTimer = null;
|
|
};
|
|
|
|
const startUploadTimer = () => {
|
|
stopUploadTimer();
|
|
uploadStartedAt = Date.now();
|
|
progressExplanation.hidden = false;
|
|
uploadTimer = window.setInterval(() => {
|
|
uploadElapsed.textContent = `Прошло: ${formatElapsed(Math.floor((Date.now() - uploadStartedAt) / 1000))}`;
|
|
}, 1000);
|
|
};
|
|
|
|
const showUploadPhase = (phase, explanation) => {
|
|
progressExplanation.hidden = false;
|
|
uploadPhase.textContent = phase;
|
|
uploadPhaseHelp.textContent = explanation;
|
|
};
|
|
|
|
const picker = FilePicker.initFilePicker({
|
|
mount: '#file-picker',
|
|
allowedExt: {{ config.allowedExt|tojson }},
|
|
onChange: (files) => {
|
|
selectedFiles = files;
|
|
uploadBtn.disabled = files.length === 0;
|
|
uploadStatus.textContent = files.length ? `Готово к отправке: ${files.length} файл(ов)` : '';
|
|
}
|
|
});
|
|
|
|
uploadBtn.addEventListener('click', async () => {
|
|
const files = picker.getFiles();
|
|
if (!files.length) return;
|
|
|
|
uploadBtn.disabled = true;
|
|
cancelBtn.style.display = 'inline-block';
|
|
sessionInfo.style.display = 'none';
|
|
abortCtrl = new AbortController();
|
|
startUploadTimer();
|
|
showUploadPhase(
|
|
'Первый файл: устанавливаем соединение',
|
|
'Это не зависание. Сначала файл отправляется в буфер ВМ, затем сервис забирает его в RAM и очищает буфер.'
|
|
);
|
|
|
|
const vmUploadUrl = new URL({{ config.vmUploadUrl|tojson }}, window.location.origin).href;
|
|
|
|
let res;
|
|
try {
|
|
res = await FilePicker.uploadViaVM(files, {
|
|
vmUploadUrl: vmUploadUrl,
|
|
backendUploadUrl: '/api/upload_refs',
|
|
signal: abortCtrl.signal,
|
|
onStatus: (msg) => {
|
|
uploadStatus.textContent = msg;
|
|
if (msg.startsWith('Загрузка в буфер')) {
|
|
showUploadPhase('Отправка файла в буфер ВМ', 'Идёт PUT текущего файла. После этого сервис сразу перенесёт его в RAM.');
|
|
} else if (msg.startsWith('Приём сервисом')) {
|
|
showUploadPhase('Перенос файла в RAM', 'Сервис забирает текущий файл из буфера ВМ и удаляет его после успешного приёма.');
|
|
}
|
|
},
|
|
onFileStatus: (k, statusText) => {
|
|
const row = document.querySelectorAll('.tree-row.tree-file')[k];
|
|
if (row) {
|
|
const statusCell = row.querySelectorAll('td')[2];
|
|
if (statusCell) statusCell.innerHTML = statusText;
|
|
}
|
|
},
|
|
onFileComplete: (info) => {
|
|
console.log('File delivered to RAM:', info);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
stopUploadTimer();
|
|
cancelBtn.style.display = 'none';
|
|
uploadBtn.disabled = selectedFiles.length === 0;
|
|
showUploadPhase('Загрузка остановлена', 'Произошла непредвиденная ошибка. Повторите загрузку.');
|
|
uploadStatus.innerHTML = '<span style="color:#b32f22; font-weight: bold;">✗ Ошибка загрузки</span>';
|
|
console.error('Upload failed unexpectedly', error);
|
|
return;
|
|
}
|
|
|
|
stopUploadTimer();
|
|
cancelBtn.style.display = 'none';
|
|
uploadBtn.disabled = selectedFiles.length === 0;
|
|
|
|
if (res.ok) {
|
|
showUploadPhase('Загрузка завершена', 'Все файлы доставлены в RAM сессии.');
|
|
uploadStatus.innerHTML = '<span style="color:#1f5a3b; font-weight: bold;">✓ Все ' + res.count + ' файл(ов) успешно доставлены в RAM бэкенда!</span>';
|
|
sessionIdEl.textContent = res.session;
|
|
sessionInfo.style.display = 'block';
|
|
|
|
try {
|
|
const sResp = await fetch(`/api/session/${res.session}/files`);
|
|
const sData = await sResp.json();
|
|
if (sData.ok) {
|
|
sessionFilesEl.innerHTML = 'Сохранено в сессии RAM: ' +
|
|
sData.files.map(f => `<code>${f.name}</code> (${f.size} B)`).join(', ');
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load session files', e);
|
|
}
|
|
} else if (res.aborted) {
|
|
showUploadPhase('Загрузка отменена', 'Переданные до отмены файлы уже находятся в RAM сессии.');
|
|
uploadStatus.innerHTML = '<span style="color:#8c5148;">Загрузка отменена пользователем</span>';
|
|
} else {
|
|
showUploadPhase('Загрузка остановлена', 'Проверьте сообщение об ошибке. Уже доставленные файлы остаются в RAM сессии.');
|
|
uploadStatus.innerHTML = '<span style="color:#b32f22; font-weight: bold;">✗ Ошибка: ' + res.error + '</span>';
|
|
}
|
|
});
|
|
|
|
cancelBtn.addEventListener('click', () => {
|
|
if (abortCtrl) abortCtrl.abort();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|