Upload Platform

Модульная платформа загрузки и транзита документов (версия 0.2.0). Состоит из двух независимых автономных слоёв с чистыми API:

  1. Слой 1 (File Picker): Клиентский выбор файлов, папок и вложенных ZIP-архивов с дедупликацией, фильтрацией по расширениям, проверкой лимитов и визуализацией в виде дерева.
  2. Слой 2 (Transit Upload via VM Buffer): Пофайловый потоковый транзит файлов из браузера в оперативную память (RAM) сессии бэкенда через внешний WebDAV-буфер на ВМ. Решает проблему ограничения Ingress Kubernetes на входящий payload (64 КБ) за счёт исходящего pull из кластера.

Архитектура слоёв и API

[ Браузер ]
   │
   ├─► Слой 1: initFilePicker() ──► picker.getFiles() -> File[]
   │
   └─► Слой 2: uploadViaVM(files, { vmUploadUrl, backendUploadUrl })
          │
          ├─ 1. PUT файл k ──► [ ВМ-буфер (RAM) ]
          │                          ▲
          └─ 2. POST /api/upload_refs│
                     │               │
                     ▼               │
               [ Flask Backend ] ────┘ 3. Потоковый GET (pull) в RAM
                     │
                     ├─► 4. DELETE файл k с ВМ
                     └─► 5. session.add_file(sid, name, bytes)
                               │
                               ▼
                     [ Слой 3: drhider / contractor ]

Слой 1: File Picker API

  • Вход: DOM-контейнер (mount), разрешённые расширения (allowedExt), лимиты (limits).
  • Выход: picker.getFiles() возвращает плоский массив объектов { path, name, size, file }.
  • Автономность: Не зависит от транспорта и бэкенда.

Слой 2: Transit Upload API

  • Фронтенд: функция uploadViaVM(files, options):
    • vmUploadUrl: базовый URL буфера на ВМ (Zero Hardcode).
    • backendUploadUrl: эндпоинт приёма ссылок (/api/upload_refs).
    • signal: AbortSignal для отмены загрузки.
    • onProgress: колбэк прогресса загрузки текущего файла.
    • onFileStatus: колбэк статуса строки файла.
    • onFileComplete: колбэк завершения переноса файла в сессию бэкенда.
  • Бэкенд: Flask Blueprint create_upload_refs_blueprint(cfg):
    • Принимает ссылку на один файл (пофайловый транзит).
    • Проверяет URL на доверенный префикс (vmUploadPrefix, защита от SSRF).
    • Санитизирует имя файла (safe_name, защита от path traversal).
    • Вытягивает байты потоком исходящим GET (ретраи, таймауты) прямо в RAM.
    • Немедленно удаляет файл из буфера на ВМ (DELETE).
    • Сохраняет файл в RAM сессии (upload/backend/session).
    • При необходимости вызывает onFileReceived(sid, name, content) для передачи в Слой 3.

Структура репозитория

upload-platform/
├── upload/                              # Переиспользуемый встраиваемый модуль
│   ├── frontend/
│   │   ├── index.js                     # Точка входа фронтенда (initFilePicker, uploadViaVM, putToVm)
│   │   ├── table/                       # UI и дерево файлов (Слой 1)
│   │   ├── zip/                         # Распаковка архивов fflate (Слой 1)
│   │   └── upload/                      # Транспорт Слоя 2:
│   │       ├── put_to_vm.js             # XHR PUT одного файла на буфер с прогрессом и abort
│   │       └── upload_via_vm.js         # Пофайловый транзит (PUT -> upload_refs -> repeat)
│   └── backend/                         # Серверная часть Слоя 2:
│       ├── upload_refs/
│       │   ├── blueprint.py             # Flask Blueprint (POST /api/upload_refs)
│       │   ├── pull_file.py             # Исходящий потоковый GET с ретраями (httpx)
│       │   ├── safe_name.py             # Защита от path traversal
│       │   └── config.py                # Дефолтные параметры
│       └── session/
│           ├── state.py                 # In-memory хранилище сессий (_sessions, TTL, lock)
│           ├── create_session.py        # Создание сессии (UUID)
│           ├── add_file.py              # Добавление файла в RAM с проверкой лимита
│           ├── get_files.py             # Чтение файлов сессии
│           ├── store_result.py          # Хранение результирующего ZIP
│           ├── store_csv.py             # Хранение CSV
│           ├── ttl.py                   # touch, pause_ttl, resume_ttl
│           ├── cancel.py                # Мягкая отмена сессии (threading.Event)
│           └── cleanup.py               # Удаление сессии
├── site/                                # Тестовый стенд и demo-сервер
│   ├── app.py                           # Flask demo + встроенный RAM mock WebDAV буфер
│   ├── templates/index.html             # Demo UI со сквозным сценарием выбора и загрузки
│   └── static/style.css
├── dist/                                # Готовые бандлы (собираются через build.mjs)
│   ├── file-picker.esm.js
│   └── file-picker.iife.js
├── tests/                               # Автоматические тесты (100% offline)
│   ├── test_safe_name.py                # Тесты санитизации путей
│   ├── test_session.py                  # Тесты RAM-хранилища сессий и лимитов
│   ├── test_upload_refs.py              # Тесты Blueprint, SSRF-защиты и pull
│   ├── test_app_integration.py          # Сквозной интеграционный тест site/app.py
│   └── test_upload_layer2.test.mjs      # Юнит-тесты фронтенда Слоя 2 (node:test)
├── build.mjs                            # esbuild сборщик
├── package.json                         # Версия 0.2.0, npm-скрипты
└── requirements.txt                     # Python зависимости (Flask, httpx, pytest)

Быстрый старт и запуск

1. Установка зависимостей

pip install -r requirements.txt
npm install

2. Сборка фронтенд-бандлов

npm run build

3. Запуск локального demo-сервера

python site/app.py
# Сервер доступен по адресу http://127.0.0.1:5000/

В стенде встроен локальный in-memory mock-буфер (/mock-buffer/), поэтому demo полностью функционально локально без подключения к боевой ВМ.

4. Запуск всех тестов

# Бэкенд тесты (pytest)
pytest tests/ -v

# Фронтенд тесты (Node.js test runner)
npm test

Инструкция: интеграция в сторонний сервис за 2 минуты

Чтобы встроить оба слоя в целевой сервис (drhider, contractor и др.):

1. Скопировать модуль

Скопировать директорию upload/ в корень целевого проекта:

cp -r upload-platform/upload/ /path/to/service/upload/

2. Подключить бэкенд во Flask

В файле создания Flask-приложения (например, app.py):

from upload.backend.upload_refs import create_upload_refs_blueprint
from upload.backend.session import get_files

upload_bp = create_upload_refs_blueprint({
    "apiPrefix": "/api",
    "vmUploadPrefix": "https://contracts.kube5s.ru/drhider-upload/",  # Префикс вашего буфера
    "maxFileBytes": 50 * 1024 * 1024,
    "maxSessionBytes": 500 * 1024 * 1024,
})
app.register_blueprint(upload_bp)

3. Подключить фронтенд

В шаблоне страницы:

<div id="file-picker"></div>
<button id="upload-btn">Загрузить</button>

<script src="/dist/file-picker.iife.js"></script>
<script>
  const picker = FilePicker.initFilePicker({
    mount: '#file-picker',
    allowedExt: ['.pdf', '.docx', '.txt'],
  });

  document.getElementById('upload-btn').addEventListener('click', async () => {
    const files = picker.getFiles();
    const result = await FilePicker.uploadViaVM(files, {
      vmUploadUrl: 'https://contracts.kube5s.ru/drhider-upload/',
      backendUploadUrl: '/api/upload_refs',
      onFileStatus: (idx, status) => console.log(`File ${idx}: ${status}`),
    });
    if (result.ok) {
      console.log('Успешно загружено в сессию:', result.session);
      // Запуск Слоя 3 (обработка файлов сессии)
    }
  });
</script>

4. Получение файлов в Слое 3

Бизнес-логика получает файлы из оперативной памяти:

files = get_files(session_id)
# files: [("document.pdf", b"...bytes..."), ...]

Никаких временных файлов на диске. Полная безопасность и изоляция.

S
Description
No description provided
Readme
511 KiB
Languages
JavaScript 50.1%
Python 36.7%
HTML 9.4%
CSS 3.8%