16 KiB
2026-09-05 — browser file-picker и иерархическое дерево
Исходная задача
- Требовался браузерный выбор документов в
upload-platform. - После уточнения требований сервис должен работать только как file-picker.
- Загрузка файлов на VM, pull-фаза, backend sessions и upload API не требовались.
- Интерфейс должен поддерживать выбор отдельных файлов, папок и ZIP.
- Выбранные данные должны отображаться в виде сворачиваемого иерархического дерева.
- Должна быть возможность удалить отдельный файл или целую группу.
- Последующий сервисный слой должен получать leaf-файлы и их полные пути.
- Разрешены только расширения
.pdf,.doc,.docx,.txt,.md. - Ограничения по размеру и количеству файлов не добавлялись.
- Должны поддерживаться вложенные структуры:
folder -> ZIP -> subfolder -> ZIP -> file.
Реализовано
- Удалён 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/<path:filename>.
- Реализован интерфейс выбора файлов, выбора папки и очистки.
- Добавлена конфигурация разрешённых расширений.
- Реализована рекурсивная модель дерева с типами
file,folder,zip. - Для ZIP реализованы чтение через локальный
fflate, сохранение полных путей, раскрытие вложенных ZIP и фильтрация расширений. - Для выбранной папки реализованы чтение
webkitRelativePath, построение вложенных каталогов, раскрытие ZIP внутри папки и удаление пустых групп. - Добавлена дедупликация по
path + size. - Реализовано рекурсивное удаление дублей и пустых групп.
- В таблице реализованы рендер дерева, сворачивание/разворачивание групп, удаление leaf-файлов и удаление групп вместе со всем поддеревом.
- Счётчики количества файлов и общего размера пересчитываются после изменений.
- Состояние таблицы хранит исходные browser
Fileobjects и метаданные для последующего сервисного слоя. - Версия приложения повышена до
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задавался одновременно на<td>и<span>, поэтому отступ листьев удваивался. - Средний приоритет:
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
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-имена с
<script>,<img>, кавычками и&экранируются: в DOM нетscriptиimgэлементов, исходный текст отображается безопасно. - Выбор папки с одинаковыми именами в разных каталогах сохраняет оба файла.
- Рабочее дерево Git после тестов осталось чистым.
FAIL
- ZIP entry
folder/../../escape.txtпопадает в дерево с тем же traversal-путём. - ZIP entry
/absolute.mdпопадает в дерево с двойным слэшемarchive.zip//absolute.md. - Пути с
..создают видимые группы... - Путь
<пустая часть>/absolute.mdсоздаёт группу с пустым отображаемым именем.
Вывод
Фильтрация расширений, XSS-экранирование, дедупликация и обработка пустых/глубоких ZIP работают. Обнаружен отдельный дефект нормализации ZIP-путей: перед построением дерева нужно отклонять абсолютные пути, .., пустые сегменты и backslash traversal. В текущем тестовом проходе код не менялся; FAIL оставлен документированным для следующего исправления.
Git
Исправление traversal — 2026-09-05
Причина
list_zip_files.js использовал сырой entryName из ZIP: он напрямую попадал в path и разбивался через split('/'). Поэтому .., ведущий /, пустые сегменты и Windows backslash создавали видимые небезопасные группы и пути.
Исправление
- Добавлена проверка сегментов ZIP entry до построения дерева.
- Отклоняются пустые entry, абсолютные пути, backslash,
.,..и пустые сегменты. - Пути и имена leaf-файлов строятся только из проверенных сегментов.
- Вложенный ZIP с опасным путём пропускается без прерывания обработки внешнего ZIP.
- Версия повышена до
0.1.8.
Повторная проверка
ZIP с folder/../../escape.txt, /absolute.md, windows\\..\\bad.txt, folder/./dot.txt и безопасным safe/normal.txt оставил только paths.zip/safe/normal.txt. Группы .., пустая группа и абсолютный путь больше не отображаются. Результат: 1 файлов · 2 B.
Исправление async race — 2026-09-05
Причина
onFilesChange запускал асинхронный addFiles без ожидания и без установки state.busy до первого await. При быстрых повторных событиях выбора ZIP несколько обработчиков могли одновременно менять дерево и вызывать render.
Исправление
onFilesChangeтеперь сразу устанавливаетstate.busy, ожидаетaddFilesи сбрасывает флаг вfinally.onFolderChangeиспользует тот же порядок: ранний выход при занятом состоянии, блокировка до асинхронного разбора ZIP и гарантированный сброс вfinally.- Ошибка разбора ZIP больше не оставляет picker заблокированным.
Проверки
-
Все frontend
.jsфайлы проходятnode --input-type=module --check. -
site/app.pyпроходитpython3 -m py_compile. -
git diff --checkпроходит. -
Flask smoke test загрузил страницу и оба изменённых ES-модуля без HTTP-ошибок.
-
Узкий Node-тест не применён: в проекте отсутствует
package.jsonсtype: module, поэтому Node трактует browser ES-модули как CommonJS; синтаксис проверен отдельным module-check, а загрузка проверена браузером. -
c0b8788 Implement reusable upload platform— первоначальный reusable upload platform. -
85605fe Remove VM upload layer; keep file picker only— удаление VM upload и переход к picker-only. -
a965f18 Add hierarchical file picker— иерархическое дерево, вложенные ZIP, удаление групп и файлов. -
До добавления этой записи
masterбыл синхронизирован сorigin/master. -
Текущая запись добавляется отдельным коммитом и отправляется в
origin/master.