Files
“Naeel” db58a433fb этап 2: переиспользуемый модуль upload (sink) вместо рукописного транспорта
- копирую модуль 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: план переиспользования + ревью Соннета
2026-08-26 08:07:43 +03:00

8.7 KiB
Raw Permalink Blame 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 на ВМ, а не код модуля.