Files
upload-platform/PLAN.md
T

289 lines
17 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.
# Upload Platform — подробный план реализации
> **LEGACY — документ устарел и не является планом текущей реализации.**
> Этот файл описывает отменённую архитектуру с VM upload, backend sessions,
> Flask API, лимитами загрузки и слоем `/api/process`. Текущий проект работает
> как browser-only file-picker: выбор файлов, папок и ZIP выполняется в браузере,
> а backend не принимает и не хранит выбранные файлы.
>
> Актуальное описание: [README.md](README.md) и
> [upload/README.md](upload/README.md). История перехода на picker-only и
> исправлений находится в [HISTORY/2026-09-05-file-picker.md](HISTORY/2026-09-05-file-picker.md).
>
> Весь текст ниже сохранён только как исторический план и не должен использоваться
> для новых изменений без отдельного пересмотра требований.
> Целевой исполнитель: 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-прогресс). Здесь — только **стаб-заглушка**,
которая показывает, что файлы доехали до сессии (список + размеры), без реальной обработки.
**Жёсткие требования:**
1. Vanilla JS, без React/сборщика. Единственная новая зависимость — `fflate` (CDN) для ZIP.
2. Каждая функция — отдельный файл (мелкие файлы).
3. Всё, что отличает один проект от другого — **в конфиге**, не в коде слоёв.
4. Безопасность сохранить 1:1: SSRF-валидация URL, `_safe_name` (path traversal), ретраи pull,
лимиты, `esc()` имён (self-XSS).
5. Запуск на 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` **НЕ создавать** — конфликтует со stdlib `site.py` (Nubes HowTo).
> ⛔ Импорты бэка — всегда без префикса `site.` (запуск `python site/app.py` добавляет `site/` в path).
> ⛔ Без factory pattern — платформа ждёт `app` на уровне модуля.
---
## 3. Слой 0 — конфиг (`config.json` / `config.example.json`)
```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. Порядок реализации (по шагам, после каждого — проверка)
1. **`requirements.txt`** + `site/app.py` с `/` и `/health`, `debug=False`. Проверить локально.
2. **`backend/session/*`** — перенести 1:1 из drhider `session.py`, разбить на файлы.
3. **`backend/upload_refs/*`** — `safe_name`, `pull_file`, `config`, `blueprint`.
4. **`frontend/zip/list_zip_files.js`** — `fflate` (локально `static/vendor/fflate.min.js`),
`filter` по `allowedExt`, рекурсия по вложенным zip. Юнит: кириллица, вложенный zip, не-doc.
5. **`frontend/table/*`** — слой 1, состояние внутрь `init_upload_table.js`.
6. **`frontend/upload/*`** — `putToVm` + `uploadViaVM`.
7. **`site/templates/index.html`** — собрать слои 1+2, подключить стаб слоя 3.
8. **`upload/README.md` + `README.md`** — инструкция интеграции для агента нового проекта.
9. **Деплой на 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 под этот проект — модуль копируется, не связывается.