161 lines
9.1 KiB
Markdown
161 lines
9.1 KiB
Markdown
# Справочник кода Upload Platform
|
||
|
||
Документ описывает актуальную picker-only реализацию. VM upload, backend sessions
|
||
и API загрузки относятся к отменённой архитектуре и в текущем коде отсутствуют.
|
||
|
||
## Архитектура
|
||
|
||
```text
|
||
site/app.py
|
||
-> site/templates/index.html
|
||
-> dist/file-picker.iife.js
|
||
-> upload/frontend/index.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 отдаёт страницу и готовый bundle. Все выбранные файлы остаются в памяти браузера;
|
||
сервер не принимает содержимое файлов.
|
||
|
||
## Модель данных
|
||
|
||
Каждый узел дерева имеет поля:
|
||
|
||
| Поле | Тип | Назначение |
|
||
|---|---|---|
|
||
| `id` | `string` | Уникальный идентификатор для DOM-кнопок и поиска. |
|
||
| `kind` | `file`, `folder`, `zip` | Тип узла. `file` является leaf. |
|
||
| `name` | `string` | Имя отображения в дереве. |
|
||
| `path` | `string` | Полный логический путь внутри выбора/архива. |
|
||
| `file` | `File \| null` | Исходный или созданный из ZIP browser File для leaf. |
|
||
| `children` | `Array` | Дочерние узлы группы. |
|
||
| `expanded` | `boolean` | Видимость дочерних строк в таблице. |
|
||
|
||
Внутреннее состояние picker-а:
|
||
|
||
| Поле | Назначение |
|
||
|---|---|
|
||
| `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.name`. |
|
||
|
||
Модуль является единственной реализацией 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 пропускается и передаётся в `cfg.onError(error, file)`, если
|
||
callback задан. `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, cfg)` | Узел, глубина и render config | HTML | Рекурсивно создаёт строки дерева. Внутренняя функция. |
|
||
| `render(state, elements, cfg)` | state, DOM и render config | `void` | Перерисовывает таблицу и счётчик. |
|
||
| `findNode(nodes, id)` | Массив узлов и id | `{node, nodes}` или `null` | Ищет узел и его родительский массив. |
|
||
| `flattenFiles(nodes, result)` | Дерево и аккумулятор | Массив leaf-файлов | Преобразует дерево для API приложения. |
|
||
|
||
`render` считает все leaf-файлы, включая свернутые группы. `renderNode` показывает
|
||
дочерние строки только при `expanded === true`.
|
||
|
||
## `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, limits)` | browser File, расширения и limits | `Promise<node|null>` | Публичная точка входа ZIP-парсера. |
|
||
|
||
Безопасность: отклоняются абсолютные пути, backslash, пустые сегменты, `.` и
|
||
`..`; применяются лимиты `maxEntries`, `maxTotalBytes`, `maxEntryBytes` и
|
||
`maxDepth` с безопасными значениями по умолчанию. ZIP без разрешённых leaf-файлов
|
||
возвращает `null` и не создаёт пустую группу.
|
||
|
||
## `upload/frontend/index.js`
|
||
|
||
`initFilePicker({ mount, allowedExt, labels, layout, limits, onChange, onError })`
|
||
строит весь DOM внутри HTMLElement или CSS-селектора `mount`, подключает inputs,
|
||
таблицу и обработчики. Возвращаемый API: `pickFiles`, `pickFolder`, `addFiles`,
|
||
`getFiles`, `remove`, `clear`, `render`, `destroy`. `getFiles` и `onChange`
|
||
возвращают копии массива leaf-файлов. `destroy` снимает слушатели и очищает mount.
|
||
|
||
IIFE-бандл экспортирует этот API как `FilePicker.initFilePicker`; ESM-бандл
|
||
экспортирует функцию напрямую. `fflate` включён в оба bundle, поэтому глобальный
|
||
vendor script не требуется.
|
||
|
||
## 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 и относительный путь | Совместимый маршрут для исходных frontend-модулей |
|
||
| `file_picker_dist(filename)` | HTTP GET и имя bundle | Bundle из `dist` |
|
||
|
||
### `site/templates/index.html`
|
||
|
||
Шаблон создаёт mount, подключает IIFE bundle с query-параметром версии для
|
||
сброса browser cache и вызывает `FilePicker.initFilePicker`.
|
||
|
||
Vendor `site/static/vendor/fflate.min.js` не документируется построчно: это
|
||
внешняя библиотека, используемая как готовый runtime dependency.
|