Add README and implementation plan
This commit is contained in:
@@ -0,0 +1,275 @@
|
|||||||
|
# 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-прогресс). Здесь — только **стаб-заглушка**,
|
||||||
|
которая показывает, что файлы доехали до сессии (список + размеры), без реальной обработки.
|
||||||
|
|
||||||
|
**Жёсткие требования:**
|
||||||
|
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 под этот проект — модуль копируется, не связывается.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Upload Platform
|
||||||
|
|
||||||
|
Переиспользуемый сервис загрузки файлов через ВМ-буфер. Выносит два слоя —
|
||||||
|
**выбор файлов** (фронт) и **закачку через ВМ** (фронт + бэк) — в отдельный
|
||||||
|
проект, который копируется в любой другой проект.
|
||||||
|
|
||||||
|
Паттерн: браузер → `PUT` на ВМ-буфер → Flask `POST /api/upload_refs` (egress pull) → сессия.
|
||||||
|
|
||||||
|
## Слои
|
||||||
|
|
||||||
|
| Слой | Ответственность |
|
||||||
|
|---|---|
|
||||||
|
| 0. Конфиг | `vmUploadUrl`, `allowedExt`, лимиты, ретраи |
|
||||||
|
| 1. Выбор файлов | таблица, дедуп, раскрытие ZIP, рекурсивный обход папки |
|
||||||
|
| 2. Закачка через ВМ | PUT на ВМ → pull в сессию |
|
||||||
|
| 3. Логика приложения | здесь — стаб-заглушка (в каждом проекте своя) |
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
upload-platform/
|
||||||
|
├── requirements.txt
|
||||||
|
├── config.json # слой 0 (рабочий конфиг этого демо)
|
||||||
|
├── upload/ # ← переиспользуемый модуль (копируется в любой проект)
|
||||||
|
│ ├── README.md # инструкция интеграции
|
||||||
|
│ ├── config.example.json
|
||||||
|
│ ├── frontend/ # слои 1+2 (vanilla JS, ES-модули)
|
||||||
|
│ │ ├── zip/
|
||||||
|
│ │ ├── table/
|
||||||
|
│ │ └── upload/
|
||||||
|
│ └── backend/ # слой 2 (Python)
|
||||||
|
│ ├── upload_refs/
|
||||||
|
│ └── session/
|
||||||
|
└── site/ # демо-обёртка (не переиспользуется)
|
||||||
|
├── app.py
|
||||||
|
├── routes/
|
||||||
|
├── templates/
|
||||||
|
└── static/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Локальный запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python site/app.py
|
||||||
|
# → http://127.0.0.1:5000/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Деплой на Nubes
|
||||||
|
|
||||||
|
- Точка входа: `site/app.py` (платформа запускает `python site/app.py`).
|
||||||
|
- `app.run(host="0.0.0.0", port=5000, debug=False)`.
|
||||||
|
- Маршрут `/health` → `200` (иначе liveness-проба платформы убивает под).
|
||||||
|
- Без `site/__init__.py` и factory pattern.
|
||||||
|
|
||||||
|
## Как интегрировать слои в другой проект
|
||||||
|
|
||||||
|
1. Скопировать папку `upload/` в свой проект.
|
||||||
|
2. Задать свой конфиг (по образцу `upload/config.example.json`).
|
||||||
|
3. Фронт: подключить ES-модули из `upload/frontend/` через `<script type="module">`,
|
||||||
|
вызвать `initUploadTable(cfg)` и `uploadViaVM(...)`.
|
||||||
|
4. Бэк: зарегистрировать `create_upload_refs_blueprint(cfg)`, импортировать `upload.backend.session`.
|
||||||
|
5. Слой 3 (обработка файлов) — свой; сюда приходит список файлов сессии после закачки.
|
||||||
|
|
||||||
|
Подробнее — `PLAN.md`.
|
||||||
Reference in New Issue
Block a user