Files
upload-platform/README.md

197 lines
10 KiB
Markdown
Raw Permalink 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 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..."), ...]
```
Никаких временных файлов на диске. Полная безопасность и изоляция.