16 KiB
Upload Platform — подробный план реализации
Целевой исполнитель: Flash / GPT-5.6 Luna. План самодостаточен: читай и кодируй по нему. Источник-референс (паттерн уже отлажен): drhider v0.0.75 (
site/templates/index.html,site/routes/api_bp.py,site/session.py,upload/). Здесь — standalone проект: те же 2 слоя, но вынесены в отдельный репозиторий и демонстрируются на минимальной обёртке.
0. Цель и границы
Что делаем: отдельный переиспользуемый сервис загрузки файлов через ВМ-буфер.
Паттерн: браузер → PUT на ВМ-буфер → Flask POST /api/upload_refs (egress pull) → сессия.
Что входит (переиспользуемое):
- Слой 1 — выбор файлов (фронт): таблица/дерево, дедуп, раскрытие ZIP, рекурсивный обход папки.
- Слой 2 — закачка через ВМ (фронт + бэк): PUT, pull, сессия, лимиты, валидации.
Что НЕ входит (у каждого приложения своё):
- Слой 3 — логика приложения (обфускация/обработка/SSE-прогресс). Здесь — только стаб-заглушка, которая показывает, что файлы доехали до сессии (список + размеры), без реальной обработки.
Жёсткие требования:
- Vanilla JS, без React/сборщика. Единственная новая зависимость —
fflate(CDN) для ZIP. - Каждая функция — отдельный файл (мелкие файлы).
- Всё, что отличает один проект от другого — в конфиге, не в коде слоёв.
- Безопасность сохранить 1:1: SSRF-валидация URL,
_safe_name(path traversal), ретраи pull, лимиты,esc()имён (self-XSS). - Запуск на Nubes: структура
site/app.py,python site/app.py,0.0.0.0:5000, маршрут/health→ 200 (иначе liveness-проба платформы убивает под),debug=False.
1. Архитектура (слои)
| Слой | Где | Ответственность |
|---|---|---|
| 0. Конфиг | общий | vmUploadUrl, allowedExt, лимиты, ретраи |
| 1. Выбор файлов | фронт | таблица/дерево, дедуп, раскрытие ZIP, папка, путь, статусы, кнопки |
| 2. Закачка через ВМ | фронт + бэк | PUT на ВМ (фронт) → upload_refs pull (бэк) → сессия |
| 3. Логика приложения | бэк | стаб (в реальном проекте своя — сюда приходит SSE/обработка) |
Слой 3 здесь — заглушка. Он НЕ переиспользуется. Его задача — доказать, что слой 2 корректно передаёт файлы в сессию и что в новом проекте к нему легко прицепить свою логику.
2. Структура проекта (обязательная)
upload-platform/
├── requirements.txt
├── README.md # что это + как интегрировать слои в другой проект
├── PLAN.md # этот план
├── config.json # слой 0 (рабочий конфиг ЭТОГО демо)
├── upload/ # ← переиспользуемый модуль (копируется в любой проект)
│ ├── README.md # короткая инструкция интеграции (для агента)
│ ├── config.example.json # образец конфига
│ ├── frontend/
│ │ ├── zip/
│ │ │ └── list_zip_files.js # listZipFiles(file, allowedExt) на fflate
│ │ ├── table/ # слой 1
│ │ │ ├── fs.js
│ │ │ ├── esc.js
│ │ │ ├── add_file_with_dedup.js
│ │ │ ├── render.js
│ │ │ ├── set_status.js
│ │ │ ├── on_files_change.js
│ │ │ ├── on_folder_change.js
│ │ │ └── init_upload_table.js
│ │ └── upload/ # слой 2 (фронт)
│ │ ├── put_to_vm.js
│ │ └── upload_via_vm.js
│ └── backend/
│ ├── upload_refs/ # слой 2 (бэк)
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── safe_name.py
│ │ ├── pull_file.py
│ │ └── blueprint.py
│ └── session/
│ ├── __init__.py
│ ├── state.py
│ ├── create_session.py
│ ├── add_file.py
│ ├── get_files.py
│ └── cleanup.py
└── site/ # демо-обёртка (НЕ переиспользуется)
├── app.py # точка входа, / и /health
├── routes/
│ └── api_bp.py # регистрирует blueprint из upload/ + стаб слоя 3
├── templates/
│ └── index.html # слой 1 + 2 (подключает ES-модули из upload/frontend)
└── static/
├── style.css
└── vendor/fflate.min.js # fflate (локально, не CDN — надёжнее)
⛔
site/__init__.pyНЕ создавать — конфликтует со stdlibsite.py(Nubes HowTo). ⛔ Импорты бэка — всегда без префиксаsite.(запускpython site/app.pyдобавляетsite/в path). ⛔ Без factory pattern — платформа ждётappна уровне модуля.
3. Слой 0 — конфиг (config.json / config.example.json)
{
"vmUploadUrl": "https://contracts.kube5s.ru/drhider-upload/",
"allowedExt": [".pdf", ".doc", ".docx", ".txt", ".md"],
"maxFileBytes": 52428800,
"maxSessionBytes": 524288000,
"apiPrefix": "/api",
"pullRetries": 3,
"pullRetryDelay": 2
}
Каждое поле:
vmUploadUrl— префикс ВМ-буфера (для PUT и SSRF-валидации pull).allowedExt— допустимые расширения (фильтр и в ZIP, и в папке).maxFileBytes/maxSessionBytes— лимиты.apiPrefix— префикс маршрутов бэка.pullRetries/pullRetryDelay— ретраи pull (защита от разовых DNS/сетевых сбоев).
В бэке конфиг читается из os.getenv (на Nubes — через jsonEnv), значения по умолчанию —
из config.example.json. Никаких drhider-специфичных значений в коде слоёв.
4. Слой 1 — выбор файлов (фронт)
Поведение (1:1 с drhider)
- Выбор файлов (
<input type="file" multiple>). - Выбор папки (
webkitdirectory) → рекурсивный обход, в таблице виден относительный путь. .zip→ раскрытие черезfflate.unzipсfilterпоallowedExt(не распаковывать не-doc), рекурсивно по вложенным.zip; путь в таблице —Папка/архив.zip/Внутри/файл.txt.- Дедуп по «путь+размер»; имя совпало, размер другой → суффикс
_2. - Лимиты: файл >
maxFileBytes→ «не учитывается»; сумма учитываемых >maxSessionBytes→ тоже. - Имена экранируются через
esc()(self-XSS). - Кнопка загрузки disabled при 0 учитываемых файлов; UI замораживается во время загрузки.
Интерфейс initUploadTable(cfg) -> api
cfg = {
allowedExt, maxFileBytes, maxSessionBytes,
fileInputEl, folderInputEl, tableBodyEl, countEl, uploadBtnEl,
onFilesChanged(list)
}
api = {
pickFiles(), pickFolder(),
addFiles(File[]), // дедуп + раскрытие zip + фильтр + лимиты
getFiles() -> [{path,name,size,file}], // ТОЛЬКО учитываемые
getOverNames() -> Set,
setStatus(path, html),
render(), clear()
}
Состояние (sf, fileMeta, overNames) — внутри init_upload_table.js, наружу не торчит.
Файлы и их функции
| Файл | Функция | Примечание |
|---|---|---|
zip/list_zip_files.js |
listZipFiles(file, allowedExt) |
fflate.unzip + filter + рекурсия |
table/fs.js |
fs(b) |
формат размера |
table/esc.js |
esc(s) |
экранирование HTML |
table/add_file_with_dedup.js |
addFileWithDedup(file) |
дедуп + лимиты |
table/render.js |
render() |
обычная таблица |
table/set_status.js |
setStatus(path, html) |
статус в ячейке |
table/on_files_change.js |
обработчик file input | |
table/on_folder_change.js |
обработчик webkitdirectory | |
table/init_upload_table.js |
initUploadTable(cfg) |
сборка слоя 1 |
Дерево (сворачивание по папкам/архивам) — фаза 2, см.
PLAN-file-picker.md(в drhider). В первой итерации — плоская таблица с колонкой «путь», как сейчас в drhider.
5. Слой 2 — закачка через ВМ
Фронт: uploadViaVM(files, vmUploadUrl) -> Promise<{ok, session, count, error?}>
1. token = crypto.randomUUID()
2. для каждого файла: PUT vmUploadUrl + token + '_' + k (XHR, progress -> setStatus)
refs.push({name, size, url})
3. POST /api/upload_refs {session, files: refs}
-> {ok, session, count} | {ok:false, error}
put_to_vm.js—putToVm(file, url, onProgress)(XHR, сырое тело, прогресс в ячейку).upload_via_vm.js—uploadViaVM(files, vmUploadUrl).- SSE-часть (слой 3) сюда НЕ входит — возвращаем
{ok, session, count}, дальше приложение само.
Бэк: create_upload_refs_blueprint(cfg) -> Blueprint
POST /api/upload_refs {session?, files:[{name,size,url}]}
- name = _safe_name(name) (path traversal)
- url.startswith(vmUploadUrl) (SSRF)
- size > maxFileBytes -> delete+skip
- pull: pullRetries × (пауза pullRetryDelay), stream, лимит maxFileBytes
- len(content) > maxFileBytes -> delete+skip
- add_file: нет сессии -> 404; лимит сессии -> skip
- delete url (best-effort)
-> {ok, session, count}
upload_refs/config.py— ретраи/префикс из конфига.upload_refs/safe_name.py—safe_name(name)(path traversal, сохраняет подпапки).upload_refs/pull_file.py—pull_file(client, url)(ретраи, stream, лимит).upload_refs/blueprint.py—create_upload_refs_blueprint(cfg).
Сессия (backend/session/)
Самодостаточный модуль, дробится на файл-на-функцию. Общее состояние (_sessions, _lock) — в
state.py; операции импортируют state.
create_session.py—create_session()add_file.py—add_file(sid, name, content)→ bool (лимит)get_files.py—get_files(sid),file_count(sid)cleanup.py—cleanup(sid)- (при необходимости позже:
store_result,store_csv,ttl,cancel)
6. Слой 3 — стаб (демо-обёртка site/)
site/app.py—app = Flask(...),@app.route("/")→index.html,@app.route("/health")→"ok", 200,if __name__ == "__main__": app.run(debug=False, host="0.0.0.0", port=5000).site/routes/api_bp.py— регистрируетcreate_upload_refs_blueprint(cfg), добавляет демо-маршрутPOST /api/process→ возвращает список файлов сессии (имя + размер) — доказательство, что слой 2 довёз файлы; в реальном проекте здесь будет своя обработка.site/templates/index.html—<script type="module">подключаетinitUploadTable+uploadViaVM, по завершении закачки вызывает/api/processи показывает результат.
7. Порядок реализации (по шагам, после каждого — проверка)
requirements.txt+site/app.pyс/и/health,debug=False. Проверить локально.backend/session/*— перенести 1:1 из drhidersession.py, разбить на файлы.backend/upload_refs/*—safe_name,pull_file,config,blueprint.frontend/zip/list_zip_files.js—fflate(локальноstatic/vendor/fflate.min.js),filterпоallowedExt, рекурсия по вложенным zip. Юнит: кириллица, вложенный zip, не-doc.frontend/table/*— слой 1, состояние внутрьinit_upload_table.js.frontend/upload/*—putToVm+uploadViaVM.site/templates/index.html— собрать слои 1+2, подключить стаб слоя 3.upload/README.md+README.md— инструкция интеграции для агента нового проекта.- Деплой на Nubes — убедиться, что
/,/health, закачка и/api/processработают.
8. Чек-лист приёмки (без этого не готово)
/health→ 200;/→ 200 с версией;debug=False.- дедуп путь+размер, суффикс
_2при другом размере; - рекурсивный обход папки, путь (в т.ч. из архива) в таблице;
- раскрытие ZIP только документов, вложенные ZIP;
- SSRF-валидация URL,
safe_name, ретраи pull — на месте; - лимиты файл/сессия, статусы «пропущен (лимит)» и «не извлечён»;
esc()имён (нет XSS);- кнопка загрузки disabled при 0 файлов; заморозка UI во время загрузки; прогресс PUT;
- после закачки
/api/processвозвращает список файлов сессии.
9. Что НЕ делать
- React / сборщик / jQuery-деревья (jsTree, Fancytree).
site/__init__.py,from site.xxx import ..., factory pattern.- CDN для
fflate(класть локально вstatic/vendor/). - Реальную обработку файлов (слой 3) — только стаб.
- Менять drhider под этот проект — модуль копируется, не связывается.