feat: модуль upload — вынос слоёв 1 и 2 (выбор файлов + закачка через ВМ), файл-на-функцию, тесты

This commit is contained in:
“Naeel”
2026-08-25 08:08:14 +03:00
parent 5f5ed000b8
commit 5711e97fe5
42 changed files with 1671 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
# 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 — закачка учитываемых файлов на ВМ:
const res = await uploadViaVM(table.getFiles(), cfg.vmUploadUrl, {
session: currentSid,
onStatus(k, html) { table.setStatus(idxInSf[k], html); }, // маппинг idx → строка таблицы
onUploadStatus(text) { /* статусная строка */ },
});
// 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,
"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 мин (таймер в фоне).
---
## Конфиг (слой 0)
Всё drhider-специфичное задаётся конфигом, а не кодом слоёв:
| Поле | Назначение |
|---|---|
| `vmUploadUrl` | базовый URL ВМ-буфера для PUT (фронт) |
| `vmUploadPrefix` | тот же префикс для SSRF-валидации (бэк) |
| `allowedExt` | расширения документов из папки/архивов |
| `maxFileBytes` / `maxSessionBytes` | лимиты 50 МБ / 500 МБ |
| `apiPrefix` | префикс Blueprint `/api` |
| `pullRetries` / `pullRetryDelay` | ретраи pull (3 × 2с) |
| `estMbSec` | оценка времени обработки, сек/МБ (только UI) |
---
## Что НЕ трогать
- Слой 3 — логика приложения (обработка файлов из сессии, SSE-прогресс) у каждого своя.
- CORS на ВМ-буфере — если домен приложения другой, правится nginx на ВМ, а не код модуля.