# План: picker → универсальный встраиваемый компонент Документ для разработчика (GPT Luna). Все факты верифицированы по текущему коду (`upload-platform` на коммите `aec8ad4`, ветка `master`). Кодить строго по этому плану, не вносить изменений за рамками перечисленных пунктов. ## Цель Превратить слой выбора файлов в переиспользуемый компонент/АПИ, который встраивается в любое приложение (включая managed Flask `drhider`) одной строкой. На входе задаются: типы файлов, порядок/скрытие колонок, все надписи, тема. Контент файлов на сервер не уходит. ## Верифицированное текущее состояние - Язык: vanilla JS, ES-модули. Flask — только статический сервер. - Структура `upload/frontend/`: - `table/init_upload_table.js` — `initUploadTable(cfg)` ТРЕБУЕТ готовые DOM-элементы `fileInputEl`, `folderInputEl`, `tableBodyEl`, `countEl`. Возвращает `{pickFiles, pickFolder, addFiles, getFiles, remove, render, clear}`. - `table/on_files_change.js` — `addFiles`, `onFilesChange` (фильтр расширений, ZIP). - `table/on_folder_change.js` — `onFolderChange` (webkitdirectory). - `table/add_file_with_dedup.js` — `addFileWithDedup` (дедуп `path + NUL + size`). - `table/render.js` — `renderNode`, `render`, `findNode`, `flattenFiles`. ЗАШИТЫ строки: `'готов'`, `'Удалить ${name}'`, `'Нет выбранных файлов'`, `'${n} файлов · ${bytes}'`. - `table/rebase_tree.js`, `table/esc.js` (HTML-экранирование), `table/fs.js` (байты). - `table/set_status.js` — LEGACY, не используется, НЕ трогать. - `zip/list_zip_files.js` — `listZipFiles`; на строке 65 вызывает ГЛОБАЛЬНУЮ `fflate.unzipSync(data)`. Содержит `safeEntryParts` (path traversal), лимит глубины 20. - `site/app.py` — маршруты `/`, `/health`, `/upload-frontend/` (`send_from_directory(ROOT/"upload"/"frontend", filename)`). VERSION `0.1.10`. - `site/templates/index.html` — demo: глобально грузит `vendor/fflate.min.js`, импортирует модуль по АБСОЛЮТНОМУ пути `/upload-frontend/table/init_upload_table.js`, содержит зашитые `` «Путь/Размер/Статус» и кнопки. - `site/static/vendor/fflate.min.js` — локальная глобальная `fflate`. - `node_modules/fflate` есть, но корневого `package.json` НЕТ. - `requirements.txt` — Flask>=3.0, gunicorn, requests. ## Жёсткие связи (что убираем) 1. Глобальная зависимость `fflate` (реальная — `list_zip_files.js:65`). 2. Абсолютный путь импорта `/upload-frontend/...` (в `index.html` + маршрут в `app.py`). 3. Зашитые надписи/колонки в `render.js` и ``. 4. Принудительный DOM-контракт `initUploadTable`. 5. Нет сборки/единой точки входа. ## Целевая архитектура Единая точка входа `initFilePicker(config)`: - сама строит DOM внутри `config.mount` (не требует готовых элементов); - переиспользует существующие модули (дедуп, ZIP, рендер, обход папок); - возвращает API с `destroy()`. Один бандл (ESM + IIFE) с вшитым `fflate` и инжектом CSS. Без Shadow DOM, без системы тем, без npm-пайплайна (dist коммитится в git). ## Схема конфига `initFilePicker(config)` ```js initFilePicker({ mount, // HTMLElement | CSS-селектор (обязательно) allowedExt: ['.pdf', '.txt'],// обязательно; сопоставление регистронезависимое labels: { pickFiles: 'Выбрать файлы', pickFolder: 'Выбрать папку', clear: 'Очистить', empty: 'Нет выбранных файлов', statusReady: 'готов', remove: 'Удалить', // подставляется в aria-label и title columns: { path: 'Путь', size: 'Размер', status: 'Статус' }, count: (n, bytes) => `${n} файлов · ${formatBytes(bytes)}`, // необязательно }, layout: { columns: ['path', 'size', 'status'], // порядок + скрытие (пустой массив = все) controls: ['files', 'folder', 'clear'], theme: null, // строка-класс для кастомизации (не система тем) }, limits: { // защита ZIP (обязательные значения по умолчанию) maxEntries: 1000, maxTotalBytes: 100 * 1024 * 1024, // суммарный распакованный размер maxEntryBytes: 50 * 1024 * 1024, // размер одного entry maxDepth: 20, }, onChange(files) {}, // вызывается после каждого изменения дерева onError(err) {}, // необязательно }); // Возвращает: // { pickFiles, pickFolder, addFiles, getFiles, remove, clear, render, destroy } // getFiles() возвращает копию массива (не внутренний state). // destroy() снимает слушатели, чистит DOM и state. ``` ## Пошаговые изменения по файлам ### 1. `upload/frontend/zip/list_zip_files.js` - Заменить глобальный `fflate.unzipSync(data)` на импорт: `import { unzipSync } from 'fflate'`. - Убрать вызов глобальной `fflate`; использовать `unzipSync(data, { filter })`. `filter` получает entry-метаданные `{ name, size, originalSize, compression }` и возвращает `false` для пропуска ДО распаковки. Через `filter` реализовать: - счётчик обработанных entries > `limits.maxEntries` → выбросить ошибку/остановиться; - `originalSize > limits.maxEntryBytes` → пропустить entry; - накопительный распакованный размер > `limits.maxTotalBytes` → выбросить ошибку (защита от zip-бомбы). - `listZipFiles(file, allowedExt, limits)` — принять `limits`, прокинуть в `listEntries`. - Лимит `depth > limits.maxDepth` вместо зашитого `20`. - Сохранить `safeEntryParts` без изменений. ### 2. `upload/frontend/table/render.js` - `renderNode(node, depth, cfg)` и `render(state, elements, cfg)` принимают `cfg` (labels + layout). Заменить зашитые строки на `cfg.labels.*`: - `'готов'` → `cfg.labels.statusReady`; - `'Удалить ${name}'` → `${cfg.labels.remove} ${name}`; - `'Нет выбранных файлов'` → `cfg.labels.empty`; - счётчик → `cfg.labels.count(files.length, totalBytes)` или дефолт. - Колонки: рендерить `` только для колонок из `layout.columns` в заданном порядке; при скрытой колонке не выводить её ячейку и соответствующий ``. - `flattenFiles` оставить как есть (он уже возвращает новые объекты), но в `getFiles()` дополнительно отдавать `[...flattenFiles(state.nodes)]` (копия массива). ### 3. `upload/frontend/table/on_files_change.js` и `on_folder_change.js` - Прокинуть `limits` в `listZipFiles(file, cfg.allowedExt, cfg.limits)`. - Остальное (фильтр, busy, ZIP-catch) без изменений. ### 4. НОВЫЙ `upload/frontend/index.js` (единая точка входа) - Экспортировать `initFilePicker(config)`. - Внутри: построить разметку (кнопки, скрытые ``, таблица, счётчик) внутри `config.mount`, с подписями из `config.labels` и набором кнопок из `config.layout.controls`. - Создать state `{ nodes, fileKeys: new Set(), busy }`. - Переиспользовать существующие функции: навесить `onFilesChange`/`onFolderChange` на созданные inputs, делегирование кликов (toggle/remove) как в `init_upload_table.js`. - Вызывать `config.onChange` после каждой перерисовки с копией `getFiles()`. - Реализовать `destroy()`: снять все слушатели, очистить `mount.innerHTML`, обнулить state. - Оставить `initUploadTable` рабочим (обратная совместимость demo), но demo перевести на `initFilePicker`. - Инжектировать CSS: `import cssText from './style.css'` (через esbuild loader `text`), при первом вызове вставить `