- копирую модуль upload/ из drhider (слои 1-2) - blueprint: параметр sink (drhider-сессия по умолчанию, сверка — DB+парсинг) - upload_bp: contracts_upload_sink = _store_and_parse - routes: регистрирую create_upload_refs_blueprint(cfg, sink=...) - app.py: корень репо в sys.path (для import upload) - History: план переиспользования + ревью Соннета
184 lines
8.7 KiB
Markdown
184 lines
8.7 KiB
Markdown
# upload — переиспользуемые слои загрузки через ВМ
|
||
|
||
Два самодостаточных слоя для выноса в любой другой проект БЕЗ изменения кода
|
||
(меняется только конфиг). Поведение 1:1 с drhider v0.0.75.
|
||
|
||
```
|
||
upload/
|
||
frontend/
|
||
zip/ # распаковка ZIP (чистые функции)
|
||
table/ # слой 1: выбор файлов/папки/архива, дедуп, статусы, таблица
|
||
upload/ # слой 2 (фронт): PUT на ВМ + POST /api/upload_refs
|
||
backend/
|
||
upload_refs/ # слой 2 (бэк): Blueprint upload_refs (SSRF, _safe_name, ретраи)
|
||
session/ # in-memory сессия с TTL и лимитами
|
||
config.example.json
|
||
```
|
||
|
||
## Что это
|
||
|
||
| Слой | Где | Ответственность |
|
||
|---|---|---|
|
||
| **1. Выбор файлов** | фронт | таблица, дедуп, раскрытие ZIP, путь, статусы, кнопки |
|
||
| **2. Закачка через ВМ** | фронт + бэк | PUT на ВМ-буфер (фронт) → `upload_refs` pull (бэк) → сессия |
|
||
| **3. Логика приложения** | — | у каждого приложения своя (обфускация, SSE и т.п.). В модуле её НЕТ |
|
||
|
||
Паттерн (зачем ВМ): шлюз managed-кластера рвёт тела >64КБ, egress не ограничен.
|
||
Поэтому: браузер → `PUT` на ВМ-буфер → Flask `POST /api/upload_refs` → egress `GET` → сессия.
|
||
|
||
---
|
||
|
||
## Подключение фронта
|
||
|
||
Подключить ES-модули (`<script type="module">`) и собрать слой 1:
|
||
|
||
```js
|
||
import { initUploadTable } from './upload/frontend/table/init_upload_table.js';
|
||
import { uploadViaVM } from './upload/frontend/upload/upload_via_vm.js';
|
||
|
||
const cfg = {
|
||
allowedExt: ['.pdf', '.doc', '.docx', '.txt', '.md'],
|
||
maxFileBytes: 50 * 1024 * 1024,
|
||
maxSessionBytes: 500 * 1024 * 1024,
|
||
estMbSec: 12,
|
||
};
|
||
|
||
const table = initUploadTable(cfg, {
|
||
fileInput: document.getElementById('fileInput'), // <input type="file" multiple>
|
||
folderInput: document.getElementById('folderInput'), // <input webkitdirectory>
|
||
tableBody: document.getElementById('fileList'), // <tbody>
|
||
countEl: document.getElementById('fileCount'),
|
||
uploadBtnEl: document.getElementById('uploadBtn'),
|
||
onStatus(cls, text) { /* сообщения (cls: ''|'progress'|'done'|'error') */ },
|
||
});
|
||
|
||
// Слой 2 — закачка учитываемых файлов на ВМ:
|
||
// idxInSf[k] — индекс k-го отправляемого файла в строках таблицы (своя логика маппинга)
|
||
const res = await uploadViaVM(toSend, cfg.vmUploadUrl, {
|
||
session: currentSid,
|
||
onStatus(k, html) { table.setStatus(idxInSf[k], html); }, // прогресс → ячейка таблицы
|
||
onUploadStatus(text) { /* статусная строка */ },
|
||
onXHR(xhr) { activeXHR = xhr; }, // регистрация активного PUT для отмены (abort)
|
||
});
|
||
// res = {ok:true, session, count} | {ok:false, error}
|
||
// После этого у вас в сессии res.session лежат файлы — запускайте СВОЮ обработку.
|
||
```
|
||
|
||
API слоя 1 (`initUploadTable(cfg, els)` → `table`):
|
||
- `addFiles(File[])` — дедуп + раскрытие ZIP + фильтр + лимиты;
|
||
- `getFiles()` → `[{name, size, file}]` — **только учитываемые** (без сверхлимитных);
|
||
- `getOverNames()` → `Set` — имена сверх лимита;
|
||
- `setStatus(idx, html)` — статус в ячейке таблицы;
|
||
- `render()` — перерисовать таблицу;
|
||
- `clear()` — очистить список;
|
||
- `setBusy(bool)` — заблокировать список на время загрузки/обработки;
|
||
- `state` — доступ к состоянию (для слоя 3: установить `state.proc` для 3-секционной таблицы).
|
||
|
||
---
|
||
|
||
## Подключение бэка
|
||
|
||
```python
|
||
from flask import Flask
|
||
from upload.backend.upload_refs import create_upload_refs_blueprint
|
||
from upload.backend.session import create_session, add_file, get_files
|
||
|
||
app = Flask(__name__)
|
||
app.register_blueprint(create_upload_refs_blueprint({
|
||
"apiPrefix": "/api", # префикс эндпоинтов
|
||
"vmUploadPrefix": "https://.../drhider-upload/", # доверенный префикс (SSRF)
|
||
"maxFileBytes": 50 * 1024 * 1024,
|
||
"maxSessionBytes": 500 * 1024 * 1024,
|
||
"ttlSeconds": 1800,
|
||
"pullRetries": 3,
|
||
"pullRetryDelay": 2,
|
||
}))
|
||
```
|
||
|
||
Эндпоинт: `POST {apiPrefix}/upload_refs` — принимает JSON
|
||
`{"session": "...", "files": [{"name", "size", "url"}]}`, тянет каждый файл с ВМ
|
||
(SSRF-валидация по `vmUploadPrefix`, `_safe_name`, ретраи), кладёт в сессию.
|
||
Возвращает `{"ok": true, "session", "count"}`.
|
||
|
||
Сессия: `create_session()` → sid; `add_file(sid, name, content)` (лимит 500МБ);
|
||
`get_files(sid)` → `[(name, bytes), ...]` или `None`. TTL 30 мин (таймер в фоне).
|
||
|
||
---
|
||
|
||
## Раздача модуля в Flask (обязательно)
|
||
|
||
Фронт-модули — ES-модули, браузер грузит их по HTTP (не через file://).
|
||
Добавьте route, который отдаёт папку `upload/frontend` (как в drhider `site/routes/main_bp.py`):
|
||
|
||
```python
|
||
# в любом blueprint приложения
|
||
import os
|
||
from flask import send_from_directory
|
||
|
||
_UPLOAD_FRONTEND = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||
"upload", "frontend")
|
||
|
||
@bp.route("/upload/<path:filename>")
|
||
def upload_frontend(filename):
|
||
return send_from_directory(_UPLOAD_FRONTEND, filename)
|
||
```
|
||
|
||
Тогда импорты в браузере: `import { initUploadTable } from '/upload/table/init_upload_table.js';`.
|
||
|
||
> Если фронт-модули хостятся отдельно (другой домен/приложение) — путь `/upload/...`
|
||
> и CORS настраиваются под ваш случай (правка приложения/nginx, не кода модуля).
|
||
|
||
---
|
||
|
||
## Слой 3: как подключить обработку (опционально)
|
||
|
||
Модуль отдаёт файлы в сессию, а обрабатываете их ВЫ (обфускация, конвертация, ...).
|
||
После `uploadViaVM` файлы лежат в `res.session`:
|
||
|
||
```python
|
||
files = get_files(res.session) # [(name, bytes), ...]
|
||
# ... ваша обработка ...
|
||
store_result(res.session, result_zip) # если нужно отдать результат обратно
|
||
```
|
||
|
||
Для 3-секционной таблицы прогресса (готово/текущий/ожидают) модуль предоставляет
|
||
`renderProcTable` — достаточно дать слою 3 доступ к `table.state.proc`:
|
||
|
||
```js
|
||
table.state.proc = { phase: 'processing', procState: {}, procExtractRate: null };
|
||
function syncProcCtx() {
|
||
table.state.proc.phase = phase;
|
||
table.state.proc.procState = procState; // {idx: {st, elapsed, eta, chars, t0}}
|
||
table.state.proc.procExtractRate = rate; // сек/МБ (для оценок ожидающих)
|
||
}
|
||
syncProcCtx();
|
||
table.render(); // сам выберет renderProcTable при phase==='processing'
|
||
```
|
||
|
||
Статусы в ячейках: `table.setStatus(idx, html)`.
|
||
|
||
---
|
||
|
||
## Конфиг (слой 0)
|
||
|
||
Всё drhider-специфичное задаётся конфигом, а не кодом слоёв:
|
||
|
||
| Поле | Назначение |
|
||
|---|---|
|
||
| `vmUploadUrl` | базовый URL ВМ-буфера для PUT (фронт) |
|
||
| `vmUploadPrefix` | тот же префикс для SSRF-валидации (бэк) |
|
||
| `allowedExt` | расширения документов из папки/архивов |
|
||
| `maxFileBytes` / `maxSessionBytes` | лимиты 50 МБ / 500 МБ |
|
||
| `apiPrefix` | префикс Blueprint `/api` |
|
||
| `pullRetries` / `pullRetryDelay` | ретраи pull (3 × 2с) |
|
||
| `pullTimeout` | таймаут одного GET pull (сек) |
|
||
| `ttlSeconds` | TTL сессии (по умолчанию 1800) |
|
||
| `estMbSec` | оценка времени обработки, сек/МБ (только UI) |
|
||
|
||
---
|
||
|
||
## Что НЕ трогать
|
||
|
||
- Слой 3 — логика приложения (обработка файлов из сессии, SSE-прогресс) у каждого своя.
|
||
- CORS на ВМ-буфере — если домен приложения другой, правится nginx на ВМ, а не код модуля.
|