feat: implement Layer 2 per-file transit, RAM session, mock buffer, and tests (v0.2.0)

This commit is contained in:
“Naeel”
2026-09-06 12:01:56 +03:00
parent 0ea7ba058c
commit 112c84f11a
34 changed files with 2200 additions and 162 deletions
+96 -7
View File
@@ -7,25 +7,114 @@
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<!-- Основной контейнер демо: Flask только подставляет version, остальное работает в браузере. -->
<!-- Основной контейнер стенда: Слой 1 (выбор) + Слой 2 (пофайловый транзит через RAM) -->
<main class="page">
<!-- Заголовок показывает назначение picker и текущую версию сборки. -->
<header>
<p class="eyebrow">FILE INTAKE / {{ version }}</p>
<p class="eyebrow">FILE INTAKE &amp; TRANSIT / {{ version }}</p>
<h1>Загрузка документов</h1>
<p class="lede">Выберите отдельные файлы или целую папку. Архивы будут раскрыты автоматически.</p>
<p class="lede">Выберите отдельные файлы или целую папку. Архивы будут распакованы на клиенте, после чего пофайлово доставлены в память сессии через буфер.</p>
</header>
<!-- Скрытые input дают браузеру FileList; кнопки ниже программно вызывают click(). -->
<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="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>
FilePicker.initFilePicker({
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');
let abortCtrl = null;
let selectedFiles = [];
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();
const vmUploadUrl = new URL({{ config.vmUploadUrl|tojson }}, window.location.origin).href;
const res = await FilePicker.uploadViaVM(files, {
vmUploadUrl: vmUploadUrl,
backendUploadUrl: '/api/upload_refs',
signal: abortCtrl.signal,
onStatus: (msg) => { uploadStatus.textContent = msg; },
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);
}
});
cancelBtn.style.display = 'none';
uploadBtn.disabled = selectedFiles.length === 0;
if (res.ok) {
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) {
uploadStatus.innerHTML = '<span style="color:#8c5148;">Загрузка отменена пользователем</span>';
} else {
uploadStatus.innerHTML = '<span style="color:#b32f22; font-weight: bold;">✗ Ошибка: ' + res.error + '</span>';
}
});
cancelBtn.addEventListener('click', () => {
if (abortCtrl) abortCtrl.abort();
});
</script>
</body>
</html>
</html>