# 2026-09-05 — browser file-picker и иерархическое дерево > **Статус документа:** исторический журнал изменений. Устаревшие промежуточные > состояния помечены `LEGACY`; исправленные дефекты помечены `RESOLVED`. ## Текущее состояние - **Актуальная версия:** `0.1.9`. - **Актуальная архитектура:** browser-only file-picker без VM upload, backend sessions и upload API. - **Актуальный commit:** `1e30548 Fix async picker race`, отправлен в `origin/master`. ## Исходная задача - Требовался браузерный выбор документов в `upload-platform`. - После уточнения требований сервис должен работать только как file-picker. - Загрузка файлов на VM, pull-фаза, backend sessions и upload API не требовались. - Интерфейс должен поддерживать выбор отдельных файлов, папок и ZIP. - Выбранные данные должны отображаться в виде сворачиваемого иерархического дерева. - Должна быть возможность удалить отдельный файл или целую группу. - Последующий сервисный слой должен получать leaf-файлы и их полные пути. - Разрешены только расширения `.pdf`, `.doc`, `.docx`, `.txt`, `.md`. - Ограничения по размеру и количеству файлов не добавлялись. - Должны поддерживаться вложенные структуры: `folder -> ZIP -> subfolder -> ZIP -> file`. ## Реализовано на этапе версии 0.1.7 — LEGACY > **LEGACY:** раздел фиксирует состояние на момент версии `0.1.7`, а не текущий > состав кода. Для актуального состояния см. раздел «Текущее состояние» и > последующие исправления ниже. - Удалён VM upload layer: PUT на внешний VM endpoint, pull, backend sessions и upload API. - Удалены старые backend/API части upload-platform по явному запросу: - `upload/backend/`; - `site/routes/`; - `upload/frontend/upload/`. - Flask-приложение оставлено только для отдачи страницы и frontend-модулей: - `/`; - `/health`; - `/upload-frontend/`. - Реализован интерфейс выбора файлов, выбора папки и очистки. - Добавлена конфигурация разрешённых расширений. - Реализована рекурсивная модель дерева с типами `file`, `folder`, `zip`. - Для ZIP реализованы чтение через локальный `fflate`, сохранение полных путей, раскрытие вложенных ZIP и фильтрация расширений. - Для выбранной папки реализованы чтение `webkitRelativePath`, построение вложенных каталогов, раскрытие ZIP внутри папки и удаление пустых групп. - Добавлена дедупликация по `path + size`. - Реализовано рекурсивное удаление дублей и пустых групп. - В таблице реализованы рендер дерева, сворачивание/разворачивание групп, удаление leaf-файлов и удаление групп вместе со всем поддеревом. - Счётчики количества файлов и общего размера пересчитываются после изменений. - Состояние таблицы хранит исходные browser `File` objects и метаданные для последующего сервисного слоя. - Версия приложения повышена до `0.1.7`. ## Исправления по ходу работы - Первоначальная реализация отправляла файлы на VM; она заменена на picker-only архитектуру. - Убраны устаревшие фрагменты VM upload и API. - Исправлены дублированные фрагменты кода, обнаруженные синтаксической проверкой после первоначального патча. - Исправлено построение полных путей для файлов в ZIP и вложенных ZIP. - Исправлено построение путей корневых папок и обычных файлов при выборе директории. - Исправлено удаление пустых групп после дедупликации. - Исправлен показ запрещённых файлов при обработке ZIP: ZIP без разрешённых leaf-файлов больше не отображается как группа. - Исправлен fallback для ошибочно раскрывающихся ZIP: нераскрытый ZIP не показывается пользователю как содержимое. - Добавлен HTML-фильтр `accept` для системного диалога выбора: показываются только разрешённые документы и `.zip`, а не `Все файлы (*.*)`. ## Проверки - Синтаксис JavaScript проверен через Node.js для frontend-модулей. - Синтетический тест вложенного ZIP подтвердил пути `archive.zip/root.pdf` и `archive.zip/docs/inner.zip/deep.txt`, а `.png` был исключён. - Повторная загрузка одинакового дерева не добавляет дубликаты. - Удаление корневого дерева удаляет все leaf-файлы. - Flask smoke test подтвердил страницу, `/health` и frontend-маршруты. - `git diff --check` прошёл. - Реальный браузерный тест подтвердил: - отображение обычного `plain.md`; - ZIP с вложенным ZIP; - фильтрацию `.png` и `.jpg`; - три сохранённых leaf-файла и общий размер `16 B`; - сворачивание `archive.zip` через `aria-expanded=false`; - удаление группы `archive.zip` вместе со всем поддеревом; - удаление leaf-файла до состояния `0 файлов · 0 B`; - выбор папки и фильтрацию запрещённых `main.py` и `image.png`. - Несколько автоматических кликов через browser tool не сработали из-за нестабильности Playwright locator; точные accessibility-селекторы и DOM-события подтвердили те же операции без дефектов приложения. - Дополнительный браузерный тест подтвердил, что ZIP только с `image.jpg` оставляет таблицу пустой: `0 файлов · 0 B`. - Проверено наличие `accept=".pdf,.doc,.docx,.txt,.md,.zip"` у обоих file input. ## Код-ревью и исправления — 2026-09-05 Проведено ревью frontend-модулей file-picker после версии `0.1.6`. ### Найдено - **Критично:** удаление файла или группы не удаляло ключ из `state.fileMeta`. После удаления повторный выбор того же файла с тем же путём и размером ошибочно считался дубликатом. - **Средний приоритет:** для leaf-файлов `padding-left` задавался одновременно на `` и ``, поэтому отступ листьев удваивался. - **Средний приоритет:** `set_status.js` обращался к удалённому `state.files` и падал с `TypeError` при вызове. Кроме того, старый поиск по имени был бы неоднозначен для одинаковых имён в разных каталогах. - **Низкий приоритет:** `addFiles` и `onFilesChange` импортировались двумя отдельными строками из одного модуля. ### Исправлено - В `init_upload_table.js` добавлено рекурсивное удаление ключей из `state.fileMeta` перед удалением leaf или группы. Исправлены и UI-обработчик, и публичный `api.remove`. - В `render.js` убран второй источник отступа для leaf-файлов. - В `render.js` для файлов добавлен `data-path` с экранированным полным путём. - В `set_status.js` переход выполнен на актуальный `state.nodes` через `flattenFiles`; строка статуса ищется по полному `data-path`. - Объединён дублирующий импорт из `on_files_change.js`. - `state.fileMeta` заменён на `Set state.fileKeys`, поскольку значения идентификаторов узлов не использовались. - Логика удаления узла объединена в одну функцию и используется обработчиком таблицы и публичным `api.remove`. - Из `findNode` удалён неиспользуемый параметр `parent`. ### Проверено после ревью - Все frontend `.js` файлы проходят `node --input-type=module --check`. - `site/app.py` проходит `python3 -m py_compile`. - `git diff --check` проходит. - Браузерная проверка удаления leaf-файла показала `0 файлов · 0 B`; после удаления ключ дедупликации очищается. - Повторный выбор того же файла после удаления снова разрешён. - Изменения не затрагивают фильтрацию расширений, ZIP-рекурсию и публичный API picker. ## Жёсткие тесты по плану — 2026-09-05 — HISTORICAL SNAPSHOT > **HISTORICAL SNAPSHOT:** результаты ниже относятся к тестовому проходу до > исправления traversal. Актуальная повторная проверка приведена в разделе > «Повторная проверка» и в последующих записях. ### PASS - Расширения без учёта регистра: `UPPER.PDF`, `Doc.DOCX`, `note.TxT` и `file.name.md` приняты. - Некорректные расширения и имена без разрешённого суффикса: `archive.tar.gz`, `README` и `file.` отклонены. - Пустой ZIP не создаёт группу и оставляет состояние `0 файлов · 0 B`. - ZIP только с запрещёнными `.jpg` и `.py` не создаёт группу. - ZIP с разрешёнными файлами и вложенным ZIP оставляет только разрешённые leaf-файлы. - ZIP с вложенностью глубже 20 отклоняется без зависания страницы. - Дедупликация: одинаковый путь и размер схлопываются; другой путь сохраняется. - XSS-имена с `