Deploy contracts-flask / validate (push) Canceled after 0s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
107 lines
5.7 KiB
JavaScript
107 lines
5.7 KiB
JavaScript
import { listZipFiles } from '../zip/list_zip_files.js';
|
||
import { addFileWithDedup } from './add_file_with_dedup.js';
|
||
import { rebaseTree } from './rebase_tree.js';
|
||
import { render } from './render.js';
|
||
import { lstFilter } from './lst_filter.js';
|
||
|
||
/**
|
||
* Создаёт async-обработчик выбора каталога через webkitdirectory.
|
||
*
|
||
* @param {{nodes: Array, fileKeys: Set<string>, busy: boolean}} state Состояние picker.
|
||
* @param {{allowedExt: string[]}} cfg Разрешённые расширения.
|
||
* @param {{folderInputEl: HTMLInputElement, tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-элементы.
|
||
* @returns {Function} Async-обработчик события change.
|
||
*
|
||
* Браузер отдаёт плоский FileList с webkitRelativePath. Обработчик группирует
|
||
* его по первой части пути, создаёт промежуточные папки и затем добавляет каждый
|
||
* корень через общую дедупликацию. ZIP внутри каталога раскрывается тем же кодом,
|
||
* что и ZIP из обычного file input.
|
||
*/
|
||
export function onFolderChange(state, cfg, elements) {
|
||
return async () => {
|
||
if (state.busy) return;
|
||
state.busy = true;
|
||
// Сброс статуса перед новым выбором: иначе сообщение об ошибке предыдущего
|
||
// выбора (например, «Ошибка чтения архива») останется висеть в интерфейсе.
|
||
if (elements.statusEl) elements.statusEl.textContent = '';
|
||
try {
|
||
const roots = new Map();
|
||
// Manifest-фильтр: при наличии корневого `obscure.lst` набор файлов
|
||
// ограничивается его правилами, а сам manifest в дерево не попадает.
|
||
// Без manifest модуль возвращает исходный список без изменений.
|
||
const filtered = await lstFilter(elements.folderInputEl.files, [...cfg.allowedExt, '.zip']);
|
||
if (filtered.manifest) {
|
||
// Диагностику показываем в statusEl: ошибки строк и предупреждения правил.
|
||
const messages = filtered.diagnostics.map((item) => (
|
||
item.line ? `строка ${item.line}: ${item.message}` : item.message
|
||
));
|
||
if (messages.length && elements.statusEl) {
|
||
elements.statusEl.textContent = messages.join('; ');
|
||
}
|
||
}
|
||
for (const file of filtered.files) {
|
||
// Для webkitdirectory путь начинается с выбранной корневой папки.
|
||
const parts = (file.webkitRelativePath || file.name).split('/');
|
||
const relativePath = parts.slice(1).join('/') || file.name;
|
||
const lowerPath = relativePath.toLowerCase();
|
||
const rootName = parts[0] || file.name;
|
||
if (!roots.has(rootName)) {
|
||
// Один root на выбранный каталог позволяет сохранить дерево целиком.
|
||
roots.set(rootName, {
|
||
id: crypto.randomUUID(), kind: 'folder', name: rootName,
|
||
path: rootName, children: [], expanded: false
|
||
});
|
||
}
|
||
const root = roots.get(rootName);
|
||
// Вставляет узел по его пути, создавая отсутствующие промежуточные папки.
|
||
const addToFolder = (node) => {
|
||
let current = root;
|
||
const nodeParts = node.path.split('/').slice(1);
|
||
nodeParts.forEach((part, index) => {
|
||
const last = index === nodeParts.length - 1;
|
||
let child = current.children.find((item) => item.name === part);
|
||
if (!child) {
|
||
child = last ? node : {
|
||
id: crypto.randomUUID(), kind: 'folder', name: part,
|
||
path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`,
|
||
children: [], expanded: false
|
||
};
|
||
current.children.push(child);
|
||
}
|
||
current = child;
|
||
});
|
||
};
|
||
if (lowerPath.endsWith('.zip')) {
|
||
try {
|
||
// ZIP rebased на имя выбранной папки перед вставкой в общий root.
|
||
const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits);
|
||
if (zip) {
|
||
rebaseTree(zip, rootName);
|
||
addToFolder(zip);
|
||
}
|
||
} catch (error) {
|
||
// Ошибка одного ZIP не должна терять остальные файлы каталога.
|
||
if (typeof cfg.onError === 'function') {
|
||
cfg.onError(error, file);
|
||
} else if (elements.statusEl) {
|
||
elements.statusEl.textContent = `Ошибка чтения архива ${file.name}: ${error.message}`;
|
||
}
|
||
continue;
|
||
}
|
||
} else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) {
|
||
addToFolder({
|
||
id: crypto.randomUUID(), kind: 'file', name: parts.at(-1),
|
||
path: `${rootName}/${relativePath}`, file, children: [], expanded: true
|
||
});
|
||
}
|
||
}
|
||
// Дедупликация выполняется после сборки каждого корня каталога.
|
||
roots.forEach((root) => addFileWithDedup(state, root));
|
||
// Сброс value позволяет выбрать тот же каталог повторно.
|
||
elements.folderInputEl.value = '';
|
||
render(state, elements, cfg);
|
||
} finally {
|
||
state.busy = false;
|
||
}
|
||
};
|
||
} |