197 lines
10 KiB
Markdown
197 lines
10 KiB
Markdown
# 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. Установка зависимостей
|
||
```bash
|
||
pip install -r requirements.txt
|
||
npm install
|
||
```
|
||
|
||
### 2. Сборка фронтенд-бандлов
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
### 3. Запуск локального demo-сервера
|
||
```bash
|
||
python site/app.py
|
||
# Сервер доступен по адресу http://127.0.0.1:5000/
|
||
```
|
||
В стенде встроен локальный **in-memory mock-буфер** (`/mock-buffer/`), поэтому demo полностью функционально локально без подключения к боевой ВМ.
|
||
|
||
### 4. Запуск всех тестов
|
||
```bash
|
||
# Бэкенд тесты (pytest)
|
||
pytest tests/ -v
|
||
|
||
# Фронтенд тесты (Node.js test runner)
|
||
npm test
|
||
```
|
||
|
||
---
|
||
|
||
## Инструкция: интеграция в сторонний сервис за 2 минуты
|
||
|
||
Чтобы встроить оба слоя в целевой сервис (`drhider`, `contractor` и др.):
|
||
|
||
### 1. Скопировать модуль
|
||
Скопировать директорию `upload/` в корень целевого проекта:
|
||
```bash
|
||
cp -r upload-platform/upload/ /path/to/service/upload/
|
||
```
|
||
|
||
### 2. Подключить бэкенд во Flask
|
||
В файле создания Flask-приложения (например, `app.py`):
|
||
```python
|
||
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. Подключить фронтенд
|
||
В шаблоне страницы:
|
||
```html
|
||
<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
|
||
Бизнес-логика получает файлы из оперативной памяти:
|
||
```python
|
||
files = get_files(session_id)
|
||
# files: [("document.pdf", b"...bytes..."), ...]
|
||
```
|
||
Никаких временных файлов на диске. Полная безопасность и изоляция.
|
||
|