14 KiB
14 KiB
План: 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/<path:filename>(send_from_directory(ROOT/"upload"/"frontend", filename)). VERSION0.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.
Жёсткие связи (что убираем)
- Глобальная зависимость
fflate(реальная —list_zip_files.js:65). - Абсолютный путь импорта
/upload-frontend/...(вindex.html+ маршрут вapp.py). - Зашитые надписи/колонки в
render.jsи<thead>. - Принудительный DOM-контракт
initUploadTable. - Нет сборки/единой точки входа.
Целевая архитектура
Единая точка входа initFilePicker(config):
- сама строит DOM внутри
config.mount(не требует готовых элементов); - переиспользует существующие модули (дедуп, ZIP, рендер, обход папок);
- возвращает API с
destroy().
Один бандл (ESM + IIFE) с вшитым fflate и инжектом CSS. Без Shadow DOM, без системы тем,
без npm-пайплайна (dist коммитится в git).
Схема конфига initFilePicker(config)
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-бомбы).
- счётчик обработанных entries >
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 loadertext), при первом вызове вставить<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(devDependencyesbuild). 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под новый APIinitFilePickerи бандл. ПометитьinitUploadTable/set_status.jsкак legacy.
Требования безопасности (обязательно сохранить)
- Экранирование всех имён/путей через
escпередinnerHTML(уже есть — не сломать). - Path traversal в ZIP (
safeEntryParts) — без изменений. - Новые лимиты ZIP из п.1 — обязательны (zip-бомба).
getFiles()/onChangeотдают копию, а не внутренний state.- Не полагаться на абсолютные URL; пути бандла/ассетов задаются интегратором.
Критерии приёмки
initFilePicker({ mount, allowedExt, labels, layout, onChange })встраивается в пустую страницу одной строкой и работает (файл/папка/ZIP, дедуп, удаление, очистка).- Глобальной
fflateбольше нет (только внутри бандла). - Нет ни одного абсолютного импорта
/upload-frontend/.... - Надписи и порядок/скрытие колонок меняются через
labels/layoutбез правки кода. destroy()полностью чистит DOM и слушатели; повторныйinitFilePickerв тот жеmountработает.- ZIP-бомба/превышение лимитов не вешает браузер (срабатывают
limits). getFiles()возвращает копию; мутация результата не ломает state.- 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):
- ZIP-лимиты + импорт fflate (п.1, п.3).
- Параметризация render/labels/layout (п.2).
- Новая точка входа
initFilePicker+ CSS + destroy (п.4, п.5). - Сборка esbuild (п.6).
- Demo на бандл (п.7).
- Документация (п.8) + bump VERSION в
site/app.py.