- копирую модуль 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: план переиспользования + ревью Соннета
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:
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-секционной таблицы).
Подключение бэка
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):
# в любом 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:
files = get_files(res.session) # [(name, bytes), ...]
# ... ваша обработка ...
store_result(res.session, result_zip) # если нужно отдать результат обратно
Для 3-секционной таблицы прогресса (готово/текущий/ожидают) модуль предоставляет
renderProcTable — достаточно дать слою 3 доступ к table.state.proc:
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 на ВМ, а не код модуля.