feat: integrate upload-platform v0.2.2 into drhider (v0.0.78)
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
- Update upload/ module to v0.2.2 with modular Layer 1 (FilePicker) and Layer 2 (Streaming transit upload) - Replace legacy manual file table in site/templates/index.html with FilePicker.initFilePicker - Wire uploadViaVM with per-file status updates and abort signal support - Add dist bundles to dist/ and site/static/dist/ with routes in site/routes/main_bp.py - Add test_hardening.py and test_safe_name.py from upload-platform - Bump version to 0.0.78 in site/app.py - Document integration plan and report in History/upload-integration/
This commit is contained in:
+120
-25
@@ -1,29 +1,124 @@
|
||||
// render — рендер таблицы выбора файлов (обычная).
|
||||
// Если идёт обработка (state.proc.phase === 'processing') — 3-секционная.
|
||||
|
||||
import { esc } from './esc.js';
|
||||
import { fs } from './fs.js';
|
||||
import { fmtSec } from './fmt_sec.js';
|
||||
import { estForFile } from './est_for_file.js';
|
||||
import { renderProcTable } from './render_proc_table.js';
|
||||
|
||||
export function render(state, els, cfg) {
|
||||
if (state.proc && state.proc.phase === 'processing') { renderProcTable(state, els, cfg); return; }
|
||||
if (state.files.length === 0) {
|
||||
els.tableBody.innerHTML = '<tr class="empty-row"><td colspan="4">Нет выбранных файлов</td></tr>';
|
||||
} else {
|
||||
els.tableBody.innerHTML = state.files.map((f, i) => {
|
||||
const over = state.overNames.has(f.name);
|
||||
const rowCls = over ? ' class="row-over"' : '';
|
||||
const stTxt = over ? '<span style="color:#c0392b;">🔥 не учитывается</span>'
|
||||
: '<span style="color:#7d3c98;">~' + fmtSec(estForFile(f, cfg.estMbSec)) + '</span>';
|
||||
return '<tr id="row-' + i + '"' + rowCls + '><td class="name-cell">' + esc(f.name) + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">' + stTxt + '</td><td><button class="remove-btn" data-idx="' + i + '">✕</button></td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
const overCount = state.files.filter(f => state.overNames.has(f.name)).length;
|
||||
const totalSize = state.files.reduce((s, f) => s + (state.overNames.has(f.name) ? 0 : f.size), 0);
|
||||
const cntMain = state.files.length - overCount;
|
||||
const totEst = state.files.reduce((s, f) => s + (state.overNames.has(f.name) ? 0 : estForFile(f, cfg.estMbSec)), 0);
|
||||
els.countEl.textContent = (overCount ? cntMain + ' учитываются + ' + overCount + ' свыше лимита' : state.files.length) + ' файлов · ' + fs(totalSize) + ' · ~' + fmtSec(totEst);
|
||||
els.uploadBtnEl.disabled = cntMain === 0;
|
||||
const DEFAULT_RENDER_CONFIG = {
|
||||
labels: {
|
||||
statusReady: 'готов',
|
||||
remove: 'Удалить',
|
||||
empty: 'Нет выбранных файлов',
|
||||
columns: { path: 'Путь', size: 'Размер', status: 'Статус' },
|
||||
count: (count, bytes) => `${count} файлов · ${fs(bytes)}`,
|
||||
},
|
||||
layout: { columns: ['path', 'size', 'status'] },
|
||||
};
|
||||
|
||||
function renderConfig(cfg = {}) {
|
||||
return {
|
||||
labels: {
|
||||
...DEFAULT_RENDER_CONFIG.labels,
|
||||
...(cfg.labels || {}),
|
||||
columns: {
|
||||
...DEFAULT_RENDER_CONFIG.labels.columns,
|
||||
...(cfg.labels?.columns || {}),
|
||||
},
|
||||
},
|
||||
layout: { ...DEFAULT_RENDER_CONFIG.layout, ...(cfg.layout || {}) },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендерит один узел дерева и его раскрытых потомков в HTML-строки таблицы.
|
||||
*
|
||||
* @param {{kind: string, id: string, name: string, path: string, file?: File,
|
||||
* children: Array, expanded: boolean, error?: string}} node Узел file/folder/zip.
|
||||
* @param {number} depth Глубина узла; используется для визуального отступа.
|
||||
* @returns {string} HTML одной строки или поддерева строк.
|
||||
*
|
||||
* Имена и пути проходят через esc до вставки в innerHTML. Группа получает
|
||||
* кнопку раскрытия, leaf-файл — размер, статус и data-path для будущего API статусов.
|
||||
*/
|
||||
function renderNode(node, depth, cfg) {
|
||||
const isGroup = node.kind !== 'file';
|
||||
const padding = depth * 4;
|
||||
const name = esc(node.name);
|
||||
const action = isGroup
|
||||
? `<button class="tree-name tree-group tree-${node.kind}" style="padding-left:${padding}ch" data-toggle="${node.id}" `
|
||||
+ `aria-expanded="${node.expanded}"><span class="tree-chevron">${node.expanded ? '▼' : '▶'}</span>${name}</button>`
|
||||
: `<span class="tree-name" style="padding-left:${padding}ch">${name}</span>`;
|
||||
const remove = `<button class="remove-btn" type="button" data-remove="${node.id}" aria-label="${esc(cfg.labels.remove)} ${name}">×</button>`;
|
||||
const pathAttribute = node.kind === 'file' ? ` data-path="${esc(node.path)}"` : '';
|
||||
const cells = {
|
||||
path: action,
|
||||
size: node.kind === 'file' ? fs(node.file.size) : '',
|
||||
status: node.error ? esc(node.error) : node.kind === 'file' ? esc(cfg.labels.statusReady) : '',
|
||||
};
|
||||
const columns = cfg.layout.columns?.length
|
||||
? cfg.layout.columns
|
||||
: Object.keys(DEFAULT_RENDER_CONFIG.labels.columns);
|
||||
const row = `<tr class="tree-row tree-${node.kind}"${pathAttribute}>`
|
||||
+ columns.map((column) => `<td>${cells[column] || ''}</td>`).join('')
|
||||
+ `<td>${remove}</td></tr>`;
|
||||
if (!isGroup || !node.expanded) return row;
|
||||
// Дочерние строки добавляются только для раскрытой группы.
|
||||
return row + node.children.map((child) => renderNode(child, depth + 1, cfg)).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Полностью синхронизирует DOM таблицы и счётчик с текущим state.nodes.
|
||||
*
|
||||
* @param {{nodes: Array}} state Состояние дерева.
|
||||
* @param {{tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-вывод.
|
||||
* @returns {void}
|
||||
*
|
||||
* Обход для счётчика независим от раскрытия: свернутая группа всё равно входит
|
||||
* в количество leaf-файлов и суммарный размер.
|
||||
*/
|
||||
export function render(state, elements, cfg) {
|
||||
const normalized = renderConfig(cfg);
|
||||
const columns = normalized.layout.columns?.length
|
||||
? normalized.layout.columns
|
||||
: Object.keys(DEFAULT_RENDER_CONFIG.labels.columns);
|
||||
elements.tableBodyEl.innerHTML = state.nodes.length
|
||||
? state.nodes.map((node) => renderNode(node, 0, normalized)).join('')
|
||||
: `<tr><td colspan="${columns.length + 1}">${esc(normalized.labels.empty)}</td></tr>`;
|
||||
const files = [];
|
||||
// Собирает все leaf-файлы, включая содержимое свернутых групп.
|
||||
const visit = (node) => {
|
||||
if (node.kind === 'file') files.push(node);
|
||||
else node.children.forEach(visit);
|
||||
};
|
||||
state.nodes.forEach(visit);
|
||||
const totalBytes = files.reduce((sum, node) => sum + node.file.size, 0);
|
||||
elements.countEl.textContent = normalized.labels.count(files.length, totalBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ищет узел по уникальному id и возвращает также массив его непосредственного родителя.
|
||||
*
|
||||
* @param {Array} nodes Массив узлов текущего уровня.
|
||||
* @param {string} id Идентификатор, созданный через crypto.randomUUID().
|
||||
* @returns {{node: object, nodes: Array}|null} Узел и контейнер для удаления либо null.
|
||||
*/
|
||||
export function findNode(nodes, id) {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return { node, nodes };
|
||||
const found = findNode(node.children || [], id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует иерархическое дерево в плоский список leaf-файлов.
|
||||
*
|
||||
* @param {Array} nodes Узлы любого уровня.
|
||||
* @param {Array} result Внутренний аккумулятор рекурсивного вызова.
|
||||
* @returns {Array<{path: string, name: string, size: number, file: File}>} Файлы для приложения.
|
||||
*/
|
||||
export function flattenFiles(nodes, result = []) {
|
||||
nodes.forEach((node) => {
|
||||
if (node.kind === 'file') result.push({ path: node.path, name: node.name, size: node.file.size, file: node.file });
|
||||
else flattenFiles(node.children, result);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user