Document picker code and architecture

This commit is contained in:
“Naeel”
2026-09-05 13:24:00 +03:00
parent 13ea0efcd7
commit e4b4dca53a
18 changed files with 437 additions and 2 deletions
+169
View File
@@ -0,0 +1,169 @@
# Справочник кода Upload Platform
Документ описывает актуальную picker-only реализацию. VM upload, backend sessions
и API загрузки являются legacy и в текущем коде не используются.
## Архитектура
```text
site/app.py
-> site/templates/index.html
-> upload/frontend/table/init_upload_table.js
-> table/on_files_change.js
-> table/on_folder_change.js
-> table/add_file_with_dedup.js
-> table/render.js
-> zip/list_zip_files.js
```
Flask отдаёт страницу и ES-модули. Все выбранные файлы остаются в памяти браузера;
сервер не принимает содержимое файлов.
## Модель данных
Каждый узел дерева имеет поля:
| Поле | Тип | Назначение |
|---|---|---|
| `id` | `string` | Уникальный идентификатор для DOM-кнопок и поиска. |
| `kind` | `file`, `folder`, `zip` | Тип узла. `file` является leaf. |
| `name` | `string` | Имя отображения в дереве. |
| `path` | `string` | Полный логический путь внутри выбора/архива. |
| `file` | `File \| null` | Исходный или созданный из ZIP browser File для leaf. |
| `children` | `Array` | Дочерние узлы группы. |
| `expanded` | `boolean` | Видимость дочерних строк в таблице. |
Внутреннее состояние `initUploadTable`:
| Поле | Назначение |
|---|---|
| `nodes` | Корневые узлы дерева. |
| `fileKeys` | Set ключей `path + NUL + size` для дедупликации. |
| `busy` | Защита от параллельной обработки async change-событий. |
## `upload/frontend/table`
### `add_file_with_dedup.js`
| Функция | Вход | Выход | Побочные эффекты |
|---|---|---|---|
| `addFileWithDedup(state, fileNode)` | state и корневой file/folder/zip-узел | `boolean` | Фильтрует дубли и пустые группы, добавляет узел и ключи в state. |
Дедупликация выполняется рекурсивно. Один и тот же путь с другим размером не
считается дублем.
### `esc.js`
| Функция | Вход | Выход |
|---|---|---|
| `esc(value)` | Любое значение | HTML-экранированная строка |
Используется перед `innerHTML`, `data-path`, `aria-label` и текстом имени.
### `fs.js`
| Функция | Вход | Выход |
|---|---|---|
| `fs(bytes)` | Размер в байтах | Строка в `B`, `KB` или `MB` |
### `rebase_tree.js`
| Функция | Вход | Выход | Побочные эффекты |
|---|---|---|---|
| `rebaseTree(root, prefix)` | Дерево и префикс папки | То же дерево | Рекурсивно меняет `path`; для leaf создаёт File с новым именем. |
Модуль является единственной реализацией rebasing для ZIP внутри выбранной папки
и для nested ZIP.
### `on_files_change.js`
| Функция | Вход | Выход | Назначение |
|---|---|---|---|
| `addFiles(state, cfg, files, elements)` | state, `allowedExt`, FileList/Array, DOM | `Promise<void>` | Фильтрует обычные файлы, раскрывает ZIP, вызывает render. |
| `onFilesChange(state, cfg, elements)` | state, config, DOM | async callback | Блокирует повторные события через `busy` и вызывает `addFiles`. |
Ошибка отдельного ZIP пропускается. `finally` обязательно освобождает `busy`.
### `on_folder_change.js`
| Функция | Вход | Выход | Назначение |
|---|---|---|---|
| `onFolderChange(state, cfg, elements)` | state, `allowedExt`, folder input и DOM | async callback | Строит дерево по `webkitRelativePath`, раскрывает ZIP и рендерит. |
Корень пути становится folder-узлом. Вложенные папки создаются по сегментам
пути, затем корень проходит общую дедупликацию.
### `render.js`
| Функция | Вход | Выход | Назначение |
|---|---|---|---|
| `renderNode(node, depth)` | Узел и глубина | HTML | Рекурсивно создаёт строки дерева. Внутренняя функция. |
| `render(state, elements)` | state и DOM | `void` | Перерисовывает таблицу и счётчик. |
| `findNode(nodes, id)` | Массив узлов и id | `{node, nodes}` или `null` | Ищет узел и его родительский массив. |
| `flattenFiles(nodes, result)` | Дерево и аккумулятор | Массив leaf-файлов | Преобразует дерево для API приложения. |
`render` считает все leaf-файлы, включая свернутые группы. `renderNode` показывает
дочерние строки только при `expanded === true`.
### `init_upload_table.js`
| Функция | Вход | Выход | Назначение |
|---|---|---|---|
| `removeFileMeta(state, node)` | state и удаляемое поддерево | `void` | Рекурсивно удаляет dedup-ключи. Внутренняя функция. |
| `removeNode(state, id)` | state и id | `void` | Удаляет узел из родительского массива. Внутренняя функция. |
| `initUploadTable(cfg)` | `allowedExt` и четыре DOM-элемента | API-объект | Создаёт state, события, render и публичные операции. |
Публичный API:
| Метод | Вход | Результат |
|---|---|---|
| `pickFiles()` | нет | Открывает обычный file input. |
| `pickFolder()` | нет | Открывает folder input. |
| `addFiles(files)` | FileList/Array | Добавляет файлы с фильтрацией и ZIP-разбором. |
| `getFiles()` | нет | `{path, name, size, file}[]` только для leaf. |
| `remove(id)` | id узла | Удаляет узел и его dedup-ключи. |
| `render()` | нет | Перерисовывает текущий state. |
| `clear()` | нет | Очищает дерево и dedup Set. |
## `upload/frontend/zip`
### `list_zip_files.js`
| Функция | Вход | Выход | Назначение |
|---|---|---|---|
| `extensionAllowed(name, allowedExt)` | Имя и расширения | boolean | Проверяет расширение. Внутренняя. |
| `makeFile(data, name)` | Байты и путь | `File` | Создаёт browser File. Внутренняя. |
| `safeEntryParts(entryName)` | Сырой ZIP-путь | сегменты или null | Отбрасывает traversal и опасные пути. Внутренняя. |
| `node(kind, name, path, children, file)` | Метаданные узла | node | Создаёт узел. Внутренняя. |
| `addPath(root, parts, fileNode)` | Дерево, сегменты, узел | `void` | Создаёт папки и вставляет узел. Внутренняя. |
| `listEntries(data, zipName, allowedExt, depth)` | ZIP bytes и контекст | `Promise<node|null>` | Рекурсивно обрабатывает ZIP. Внутренняя. |
| `listZipFiles(file, allowedExt)` | browser File и расширения | `Promise<node|null>` | Публичная точка входа ZIP-парсера. |
Безопасность: отклоняются абсолютные пути, backslash, пустые сегменты, `.` и
`..`; глубина nested ZIP ограничена 20. ZIP без разрешённых leaf-файлов возвращает
`null` и не создаёт пустую группу.
## Flask и HTML
### `site/app.py`
| Маршрут/объект | Вход | Выход |
|---|---|---|
| `CONFIG` | `config.json` | JSON-конфигурация picker |
| `VERSION` | константа | Версия страницы |
| `index()` / `/` | HTTP GET | HTML с version и config |
| `health()` / `/health` | HTTP GET | `ok`, HTTP 200 |
| `upload_frontend(filename)` | HTTP GET и относительный путь | ES/CSS-файл из `upload/frontend` |
### `site/templates/index.html`
Шаблон создаёт два скрытых input: обычный file input и `webkitdirectory`,
передаёт четыре DOM-элемента в `initUploadTable`, подключает локальный `fflate`
до ES-модуля и связывает три кнопки с `pickFiles`, `pickFolder` и `clear`.
## Legacy
`upload/frontend/table/set_status.js` не импортируется актуальной страницей и
сохранён как legacy-заготовка. Vendor `site/static/vendor/fflate.min.js` не
документируется построчно: это внешняя библиотека, используемая как готовый
runtime dependency.