# Справочник кода Upload Platform Документ описывает актуальную picker-only реализацию. VM upload, backend sessions и API загрузки являются legacy и в текущем коде не используются. ## Архитектура ```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 отдаёт страницу и 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` | Видимость дочерних строк в таблице. | Внутреннее состояние 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 с новым именем. | Модуль является единственной реализацией rebasing для ZIP внутри выбранной папки и для nested ZIP. ### `on_files_change.js` | Функция | Вход | Выход | Назначение | |---|---|---|---| | `addFiles(state, cfg, files, elements)` | state, `allowedExt`, FileList/Array, DOM | `Promise` | Фильтрует обычные файлы, раскрывает 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)` | Узел и глубина | 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` (legacy) | Функция | Вход | Выход | Назначение | |---|---|---|---| | `removeFileMeta(state, node)` | state и удаляемое поддерево | `void` | Рекурсивно удаляет dedup-ключи. Внутренняя функция. | | `removeNode(state, id)` | state и id | `void` | Удаляет узел из родительского массива. Внутренняя функция. | | `initUploadTable(cfg)` | `allowedExt` и четыре DOM-элемента | API-объект | Создаёт state, события, render и публичные операции. | Публичный API legacy-совместимости: | Метод | Вход | Результат | |---|---|---| | `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` | Рекурсивно обрабатывает ZIP. Внутренняя. | | `listZipFiles(file, allowedExt, limits)` | browser File, расширения и limits | `Promise` | Публичная точка входа 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 и относительный путь | Legacy-файл из `upload/frontend` | | `file_picker_dist(filename)` | HTTP GET и имя bundle | Bundle из `dist` | ### `site/templates/index.html` Шаблон создаёт mount, подключает IIFE bundle с query-параметром версии для сброса browser cache и вызывает `FilePicker.initFilePicker`. ## Legacy `upload/frontend/table/set_status.js` не импортируется актуальной страницей и сохранён как legacy-заготовка. Vendor `site/static/vendor/fflate.min.js` не документируется построчно: это внешняя библиотека, используемая как готовый runtime dependency.