diff --git a/History/2026-08-19-folder-select-recursive.md b/History/2026-08-19-folder-select-recursive.md new file mode 100644 index 0000000..0f7b020 --- /dev/null +++ b/History/2026-08-19-folder-select-recursive.md @@ -0,0 +1,45 @@ +# v0.0.47 — Выбор целой папки (рекурсивно, как раскрытие ZIP) — 2026-08-19 + +## Проблема +Нативный `` (`site/templates/index.html:133`) не умеет выбирать +папку целиком: клик по папке просто «входит» внутрь. Нужно выбирать целую папку +и рекурсивно забирать все файлы — как при раскрытии архива. + +## Решение +- Второй `` + кнопка + «📁 Выбрать папку» (вызов `folderInput.click()`). + `webkitdirectory` взаимоисключающ с обычным выбором файлов на одном input — + поэтому отдельный input, а не переключатель. +- Браузер сам отдаёт ВСЕ файлы папки рекурсивно (включая подпапки) с полем + `file.webkitRelativePath`. Имя добавляемого файла = `webkitRelativePath` + (структура папок сохраняется до итогового ZIP, как у архива сохраняется + внутренний `e.name`). +- Обработка — та же логика, что у раскрытия ZIP (`listZipFiles`): + - `.zip` → рекурсивное раскрытие (вложенные zip тоже); + - документы `.pdf .doc .docx .txt .md` → в таблицу; + - прочее (картинки и т.п.) → пропускается. +- Сортировка входящих файлов по `webkitRelativePath` — детерминированный порядок. + +## Файлы +- `site/templates/index.html` + - CSS: `.folder-btn` (полная ширина), `#folderInput { display:none }` + - HTML: скрытый `#folderInput` + кнопка в `.file-input-wrap` + - JS: `const folderInput`, обработчик `change` (рекурсия+фильтр+имена с путями) + - `resetAll()` и `load` — сброс `folderInput.value` +- `site/app.py` — VERSION 0.0.46 → 0.0.47 + +## Бэкенд — не менялся +- `extract_text` (drhider/extractor.py:204) берёт расширение через + `os.path.splitext` — пути в имени не мешают. +- `build_zip` (drhider/builder.py) пишет `ZipInfo` с путями как есть; + `_resolve_name` дедуплицирует по полному имени — одноимённые файлы в разных + подпапках не сталкиваются. +- Аплоад идёт из массива `sf` (`uploadFiles`), а не из `fi.files` — файлы из + папки уходят на бэк обычным путём. + +## Проверка +1. «Выбрать папку» → папка с подпапками, вложенными .zip и картинками → + в таблице только документы, рекурсивно; .zip раскрыт; картинки пропущены; + имена с путями. +2. Обфускация → скачать ZIP → вложенность папок сохранена, всё обфусцировано. +3. Регрессия: обычный «Выбрать файлы» работает как раньше. diff --git a/site/app.py b/site/app.py index 8523028..6e72dc4 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.46" +VERSION = "0.0.47" def create_app(): diff --git a/site/templates/index.html b/site/templates/index.html index 1aa780e..a59d98e 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -44,6 +44,8 @@ .sub { font-size: 12px; color: var(--muted); margin-bottom: 10px; line-height: 1.5; } .file-input-wrap { margin-bottom: 10px; } .file-input-wrap input[type="file"] { width: 100%; font-size: 13px; } + .folder-btn { margin-top: 6px; width: 100%; } + #folderInput { display: none; } .table-wrap { border: 1px solid var(--brand-gray); border-radius: 8px; overflow: hidden; } table { width: 100%; border-collapse: collapse; font-size: 13px; } th { @@ -131,6 +133,8 @@

+ +
@@ -188,6 +192,7 @@ const fl = document.getElementById('fileList'); const fc = document.getElementById('fileCount'); const ub = document.getElementById('uploadBtn'); const st = document.getElementById('status'); +const folderInput = document.getElementById('folderInput'); const db = document.getElementById('dlBtns'); let sf = []; let fileMeta = new Map(); // имя -> {size, mtime} для дедупа/суффиксов @@ -202,6 +207,7 @@ function resetAll() { sf = []; fileMeta = new Map(); fi.value = ''; + folderInput.value = ''; rr(); st.className = ''; st.textContent = ''; @@ -225,7 +231,7 @@ function rr() { function rm(i) { fileMeta.delete(sf[i].name); sf.splice(i, 1); const d = new DataTransfer(); sf.forEach(f => d.items.add(f)); fi.files = d.files; rr(); } -window.addEventListener('load', () => { sf = []; fileMeta = new Map(); fi.value = ''; rr(); /* прогрев upstream-соединения */ fetch('/health').catch(() => {}); }); +window.addEventListener('load', () => { sf = []; fileMeta = new Map(); fi.value = ''; folderInput.value = ''; rr(); /* прогрев upstream-соединения */ fetch('/health').catch(() => {}); }); // ═══════════ Нативная распаковка ZIP (без внешних библиотек) ═══════════ function dosToMs(date, time) { @@ -361,6 +367,34 @@ fi.addEventListener('change', async () => { rr(); }); +// ═══ Выбор целой папки: рекурсивно, как раскрытие ZIP ═══ +// webkitdirectory отдаёт все файлы папки (включая подпапки) с webkitRelativePath. +folderInput.addEventListener('change', async () => { + const docExts = ['.pdf', '.doc', '.docx', '.txt', '.md']; + const incoming = Array.from(folderInput.files).sort((a, b) => + (a.webkitRelativePath || a.name).localeCompare(b.webkitRelativePath || b.name)); + for (const f of incoming) { + const rel = f.webkitRelativePath || f.name; // сохраняем структуру папок + const low = rel.toLowerCase(); + if (low.endsWith('.zip')) { + try { + const nested = await listZipFiles(new File([f], rel, { lastModified: f.lastModified })); + nested.forEach(x => addFileWithDedup(x)); + } catch (err) { + addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); // не удалось — добавить как есть + } + } else if (docExts.some(ext => low.endsWith(ext))) { + addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); + } + // иначе — не документ, пропускаем + } + const d = new DataTransfer(); + sf.forEach(f => d.items.add(f)); + fi.files = d.files; + folderInput.files = d.files; + rr(); +}); + function ss(idx, h) { const e = document.getElementById('st-' + idx); if (e) e.innerHTML = h; } async function uploadFiles() {