Files
contracts-flask/upload/README.md
T
RepinoidandCopilot bb5dd6538e
Deploy contracts-flask / validate (push) Canceled after 0s
Fix Contracts upload buffer endpoint
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-30 12:08:55 +03:00

174 lines
8.9 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 — переиспользуемые слои загрузки через ВМ
Два самодостаточных слоя для выноса в любой другой проект БЕЗ изменения кода
(меняется только конфиг). Поведение 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-модули. Для обычной страницы соберите IIFE-бандл:
```sh
cd upload/frontend
npm install
npm run build
```
Бандл появится в `site/static/dist/file-picker.iife.js`; Contracts отдаёт его по
`/file-picker/file-picker.iife.js`. Инициализация picker:
```js
const picker = window.FilePicker.initFilePicker({
mount: '#filePicker',
allowedExt: ['.pdf', '.doc', '.docx'],
onChange(files) { /* синхронизировать выбранные leaf-файлы с приложением */ },
});
```
Picker строит дерево файлов/папок/ZIP, раскрывает вложенные ZIP, фильтрует
неподдерживаемые расширения и удаляет дубликаты по `path + size`. В выбранной
папке корневой `obscure.lst` применяется как manifest включения/исключения.
`getFiles()` возвращает leaf-записи `{id, path, name, size, file}`. Методы
`setStatus(id, {kind, text})`, `setDocument(id, docId)`, `setMessage(text)`,
`remove(id)`, `clear()`, `render()` и `destroy()` обслуживают UI.
Передача файлов выполняется по одному файлу за вызов/итерацию:
```js
const result = await window.FilePicker.uploadViaVM(files, {
vmUploadUrl: 'https://example.invalid/contracts-upload/',
backendUploadUrl: '/api/upload_refs',
getExtraPayload(fileEntry) {
return {batch_id, contract_id, zip_source: fileEntry.zip_source};
},
onFileComplete({result}) { /* результат sink: doc_id, parsed или ошибка */ },
});
```
`uploadViaVM` принимает `AbortSignal`, сообщает прогресс и передаёт поля
`getExtraPayload` в JSON POST к `upload_refs`. При настроенном `sink` результат
каждого файла передаётся в `onFileComplete` как `result`. Обработку и сохранение
результатов приложение определяет самостоятельно.
---
## Подключение бэка
```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`
и сквозными полями `batch_id`, `contract_id`, `zip_source`; тянет каждый файл с
ВМ (SSRF-валидация по `vmUploadPrefix`, безопасное имя, ретраи). Без `sink`
файлы кладутся в in-memory сессию и ответ содержит `session`/`count`. С `sink`
ответ содержит `results` с результатом каждого файла и обработку/хранение
определяет приложение.
Сессия: `create_session()` → sid; `add_file(sid, name, content)` (лимит 500МБ);
`get_files(sid)` → `[(name, bytes), ...]` или `None`. TTL 30 мин (таймер в фоне).
---
## Раздача фронтенда
В `contracts-flask` `pages_bp.py` отдаёт исходные модули по `/upload/<path>`
и собранный IIFE по `/file-picker/<path>`. Для другого приложения нужно
раздавать его собранный `file-picker.iife.js` по HTTP и подключать `<script src>`.
---
## Слой 3: обработка приложения
Без `sink` модуль отдаёт файлы в in-memory сессию, а обрабатываете их ВЫ
(обфускация, конвертация и т. п.). После `uploadViaVM` файлы лежат в `res.session`:
```python
files = get_files(res.session) # [(name, bytes), ...]
# ... ваша обработка ...
store_result(res.session, result_zip) # если нужно отдать результат обратно
```
С `sink` файлы не попадают в in-memory сессию: обработчик получает их как
`sink(name, content, batch_id=..., contract_id=..., zip_source=...)`, а результат
каждой обработки возвращается в `onFileComplete`.
---
## Конфиг (слой 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 на ВМ, а не код модуля.
## Инцидент Contracts VM-буфера (2026-10-30)
Контрактор использовал `contracts.kube5s.ru/contracts-upload/`, но DNS для
`contracts.kube5s.ru` возвращал `NXDOMAIN`. DNS-запись для отдельного hostname
`contracts-upload.sinefo.ru` теперь добавлена и проверена на публичных
резолверах. Переключение только на существующий
DrHider или Obfuscator endpoint недостаточно: у них другие пути и CORS allowlist,
и тестовый путь `/contracts-upload/` на этих хостах отвечал `404`. Новый
hostname теперь настроен на VM213 с отдельным TLS-сертификатом, Nginx vhost и
cron-очисткой временных файлов. Проверены HTTPS, CORS для Контрактора и прямой
синтетический цикл PUT/GET/DELETE.
Код приложения и source Nginx настроены локально; Nginx-конфиг буфера уже
установлен на VM213, но само приложение пока не redeployed. Причина, результаты
DNS/CORS/TLS-проверок и дальнейший план описаны в
[отчёте об инциденте](../docs/upload-buffer-incident-2026-10-30.md).