Files
upload-platform/docs/PLAN-componentization.md
T

207 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: picker → универсальный встраиваемый компонент — LEGACY
> Переход завершён. Этот исторический план не является инструкцией или
> спецификацией текущего кода. Актуальные документы: `README.md`,
> `upload/README.md` и `docs/CODE-REFERENCE.md`.
Документ для разработчика (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/<path:filename>`
(`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`,
содержит зашитые `<thead>` «Путь/Размер/Статус» и кнопки.
- `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` и `<thead>`.
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)` или дефолт.
- Колонки: рендерить `<td>` только для колонок из `layout.columns` в заданном порядке;
при скрытой колонке не выводить её ячейку и соответствующий `<th>`.
- `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)`.
- Внутри: построить разметку (кнопки, скрытые `<input type=file>`, таблица, счётчик)
внутри `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`),
при первом вызове вставить `<style data-file-picker>` в `<head>` один раз.
### 5. НОВЫЙ `upload/frontend/style.css`
- Перенести релевантные стили из `site/static/style.css`, ВСЕ селекторы обернуть
под корневой класс `.file-picker` (префикс-изоляция, без Shadow DOM).
- Классы: `.file-picker`, `.fp-toolbar`, `.fp-table`, `.fp-tree-*`, `.fp-remove-btn`,
`.fp-status`, `.fp-count`. Поддержать `config.layout.theme` как доп. класс на контейнере.
### 6. Сборка
- Добавить `package.json` (devDependency `esbuild`).
- `build`-скрипт собирает из `upload/frontend/index.js`:
- `dist/file-picker.esm.js` (format=esm, fflate inline);
- `dist/file-picker.iife.js` (format=iife, глобал `FilePicker`, fflate inline);
- CSS инлайнится через loader `text`.
- `dist/` коммитится в git.
### 7. Demo `site/`
- `site/templates/index.html`: убрать глобальный `<script fflate>` и абсолютный import;
подключить собранный бандл (`dist/file-picker.iife.js` или ESM) и вызвать
`initFilePicker({ mount, allowedExt: {{ config|tojson }}.allowedExt, ... })`.
- `site/app.py`: убрать или оставить маршрут `/upload-frontend/...` (рекомендуется
оставить для совместимости, но demo использует бандл); добавить раздачу `dist/`
при необходимости. VERSION поднять (см. ниже).
### 8. Документация
- Обновить `README.md`, `upload/README.md`, `docs/CODE-REFERENCE.md` под новый API
`initFilePicker` и бандл. Пометить `initUploadTable`/`set_status.js` как legacy.
## Требования безопасности (обязательно сохранить)
- Экранирование всех имён/путей через `esc` перед `innerHTML` (уже есть — не сломать).
- Path traversal в ZIP (`safeEntryParts`) — без изменений.
- Новые лимиты ZIP из п.1 — обязательны (zip-бомба).
- `getFiles()`/`onChange` отдают копию, а не внутренний state.
- Не полагаться на абсолютные URL; пути бандла/ассетов задаются интегратором.
## Критерии приёмки
1. `initFilePicker({ mount, allowedExt, labels, layout, onChange })` встраивается
в пустую страницу одной строкой и работает (файл/папка/ZIP, дедуп, удаление, очистка).
2. Глобальной `fflate` больше нет (только внутри бандла).
3. Нет ни одного абсолютного импорта `/upload-frontend/...`.
4. Надписи и порядок/скрытие колонок меняются через `labels`/`layout` без правки кода.
5. `destroy()` полностью чистит DOM и слушатели; повторный `initFilePicker` в тот же
`mount` работает.
6. ZIP-бомба/превышение лимитов не вешает браузер (срабатывают `limits`).
7. `getFiles()` возвращает копию; мутация результата не ломает state.
8. Demo в `site/` работает через бандл; `/health` → 200.
## Что НЕ делать
- Не вводить Shadow DOM, не делать систему тем/плагинов, не публиковать в npm/CDN.
- Не трогать `set_status.js`, `site/static/vendor/fflate.min.js` (после перехода на
бандл тег можно удалить из index.html, но файл vendor можно оставить).
- Не менять формат возвращаемых leaf-объектов (`{ path, name, size, file }`).
## Порядок коммитов
Логические шаги (после каждого — `node --check`/сборка + `git diff --check`):
1. ZIP-лимиты + импорт fflate (п.1, п.3).
2. Параметризация render/labels/layout (п.2).
3. Новая точка входа `initFilePicker` + CSS + destroy (п.4, п.5).
4. Сборка esbuild (п.6).
5. Demo на бандл (п.7).
6. Документация (п.8) + bump VERSION в `site/app.py`.