240 lines
21 KiB
Markdown
240 lines
21 KiB
Markdown
# 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/<path:filename>`.
|
||
- Реализован интерфейс выбора файлов, выбора папки и очистки.
|
||
- Добавлена конфигурация разрешённых расширений.
|
||
- Реализована рекурсивная модель дерева с типами `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` задавался одновременно на `<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 — 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-имена с `<script>`, `<img>`, кавычками и `&` экранируются: в DOM нет `script` и `img` элементов, исходный текст отображается безопасно.
|
||
- Выбор папки с одинаковыми именами в разных каталогах сохраняет оба файла.
|
||
- Рабочее дерево Git после тестов осталось чистым.
|
||
|
||
### FAIL — RESOLVED / LEGACY RESULT
|
||
|
||
> **RESOLVED:** перечисленные FAIL описывают состояние до исправления traversal
|
||
> и не являются текущими дефектами. Раздел сохранён как исторический результат
|
||
> тестирования.
|
||
|
||
- ZIP entry `folder/../../escape.txt` попадает в дерево с тем же traversal-путём.
|
||
- ZIP entry `/absolute.md` попадает в дерево с двойным слэшем `archive.zip//absolute.md`.
|
||
- Пути с `..` создают видимые группы `..`.
|
||
- Путь `<пустая часть>/absolute.md` создаёт группу с пустым отображаемым именем.
|
||
|
||
### Вывод — LEGACY
|
||
|
||
> **LEGACY:** это вывод на момент обнаружения дефекта; после исправления
|
||
> traversal он больше не описывает текущее состояние.
|
||
|
||
Фильтрация расширений, XSS-экранирование, дедупликация и обработка пустых/глубоких ZIP работают. Обнаружен отдельный дефект нормализации ZIP-путей: перед построением дерева нужно отклонять абсолютные пути, `..`, пустые сегменты и backslash traversal. В текущем тестовом проходе код не менялся; FAIL оставлен документированным для следующего исправления.
|
||
|
||
## Git — HISTORICAL REFERENCES
|
||
|
||
> **HISTORICAL REFERENCES:** ссылки ниже перечисляют коммиты этапов разработки,
|
||
> а не являются текущим состоянием ветки. Текущий commit указан в начале файла.
|
||
|
||
## Исправление traversal — 2026-09-05
|
||
|
||
### Причина
|
||
|
||
`list_zip_files.js` использовал сырой `entryName` из ZIP: он напрямую попадал в `path` и разбивался через `split('/')`. Поэтому `..`, ведущий `/`, пустые сегменты и Windows backslash создавали видимые небезопасные группы и пути.
|
||
|
||
### Исправление
|
||
|
||
- Добавлена проверка сегментов ZIP entry до построения дерева.
|
||
- Отклоняются пустые entry, абсолютные пути, backslash, `.`, `..` и пустые сегменты.
|
||
- Пути и имена leaf-файлов строятся только из проверенных сегментов.
|
||
- Вложенный ZIP с опасным путём пропускается без прерывания обработки внешнего ZIP.
|
||
- Версия на момент исправления повышена до `0.1.8`; **LEGACY:** текущая версия
|
||
уже `0.1.9` после исправления async race.
|
||
|
||
### Повторная проверка
|
||
|
||
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`; **LEGACY:** эта формулировка описывает историю публикации,
|
||
а не текущую операцию деплоя.
|
||
|
||
## Ревью Opus 4.8 — 2026-09-05
|
||
|
||
### Контекст ревью
|
||
|
||
Ревью ограничивалось проектом `upload-platform`. Проверены структура проекта,
|
||
frontend-модули, Flask-обёртка, конфигурация и HISTORY. Исходники backend
|
||
VM-upload отсутствуют; в рабочем дереве обнаруживались только артефакты
|
||
`__pycache__` от удалённых модулей.
|
||
|
||
### Замечания ревью и текущий статус
|
||
|
||
1. **Документация описывала отменённую VM-архитектуру.**
|
||
**RESOLVED:** `README.md`, `upload/README.md` и `PLAN.md` приведены к
|
||
picker-only архитектуре. Старые VM/backend разделы в `PLAN.md` и README
|
||
помечены как `LEGACY`.
|
||
2. **Документация содержала неиспользуемые параметры конфигурации и лимиты.**
|
||
**RESOLVED:** актуальная документация указывает только `allowedExt` и
|
||
прямо фиксирует отсутствие ограничений размера и количества файлов.
|
||
3. **В рабочем дереве присутствовали `.pyc` в `__pycache__`.**
|
||
**OPEN:** это локальные игнорируемые артефакты, не попадающие в Git. Удаление
|
||
не выполнялось, поскольку удаление файлов требует отдельного решения.
|
||
4. **`set_status.js` и `api.setBusy` выглядят неиспользуемыми.**
|
||
**OPEN:** код не удалялся; требуется отдельное решение, оставить ли их как
|
||
API-задел или удалить после проверки всех интеграций.
|
||
5. **`rebaseTree` дублируется в ZIP- и folder-обработчиках.**
|
||
**OPEN:** рефакторинг не выполнялся, чтобы не расширять текущую задачу
|
||
документирования ревью.
|
||
|
||
### Вывод ревью
|
||
|
||
Критичных дефектов в picker-only коде ревью не выявило. Основное замечание по
|
||
рассинхронизации документации устранено. Открыты три технические задачи:
|
||
решение по локальным `__pycache__`, решение по мёртвому API/модулю и устранение
|
||
дублирования `rebaseTree`. |