feat: implement Layer 2 per-file transit, RAM session, mock buffer, and tests (v0.2.0)
This commit is contained in:
@@ -1,129 +1,196 @@
|
||||
# Upload Platform
|
||||
|
||||
Переиспользуемый browser-only file-picker версии `0.1.13`. Он выбирает файлы,
|
||||
папки и ZIP, строит дерево и возвращает интегрирующему приложению browser
|
||||
`File` objects. Содержимое файлов остаётся в браузере: текущий проект не
|
||||
загружает его на VM, в Flask или в другое хранилище.
|
||||
Модульная платформа загрузки и транзита документов (версия `0.2.0`). Состоит из двух независимых автономных слоёв с чистыми API:
|
||||
|
||||
## Карта файлов
|
||||
1. **Слой 1 (File Picker)**: Клиентский выбор файлов, папок и вложенных ZIP-архивов с дедупликацией, фильтрацией по расширениям, проверкой лимитов и визуализацией в виде дерева.
|
||||
2. **Слой 2 (Transit Upload via VM Buffer)**: Пофайловый потоковый транзит файлов из браузера в оперативную память (RAM) сессии бэкенда через внешний WebDAV-буфер на ВМ. Решает проблему ограничения Ingress Kubernetes на входящий payload (64 КБ) за счёт исходящего pull из кластера.
|
||||
|
||||
### Корень проекта
|
||||
---
|
||||
|
||||
| Путь | Назначение | Статус |
|
||||
|---|---|---|
|
||||
| `package.json` | Версия пакета и команда `npm run build`. | Используется |
|
||||
| `package-lock.json` | Зафиксированные npm-зависимости. | Используется |
|
||||
| `build.mjs` | esbuild-сборка ESM и IIFE bundle. | Используется |
|
||||
| `requirements.txt` | Python-зависимости demo-сервера. | Используется demo |
|
||||
| `config.json` | Конфигурация demo: разрешённые расширения. | Используется demo |
|
||||
| `dist/file-picker.esm.js` | Готовый ESM bundle для интеграции. | Используется |
|
||||
| `dist/file-picker.iife.js` | Готовый IIFE bundle `FilePicker`. | Используется |
|
||||
| `upload/` | Исходники переиспользуемого picker-модуля. | Используется |
|
||||
| `site/` | Flask demo, который раздаёт страницу и bundle. | Используется только demo |
|
||||
| `docs/` | Справочная и историческая документация. | См. раздел LEGACY |
|
||||
| `HISTORY/` | Архив решений, ревью и результатов проверок. | Только история |
|
||||
## Архитектура слоёв и API
|
||||
|
||||
### `upload/` — исходники picker-а
|
||||
```
|
||||
[ Браузер ]
|
||||
│
|
||||
├─► Слой 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 ]
|
||||
```
|
||||
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `upload/README.md` | Краткая инструкция интеграции готового bundle. |
|
||||
| `upload/config.example.json` | Пример конфигурации разрешённых расширений. |
|
||||
| `upload/frontend/index.js` | Единственная актуальная точка входа `initFilePicker(config)`, DOM, lifecycle API. |
|
||||
| `upload/frontend/table/add_file_with_dedup.js` | Дедупликация файлов и слияние одинаковых корней. |
|
||||
| `upload/frontend/table/esc.js` | HTML-экранирование имён, путей и атрибутов. |
|
||||
| `upload/frontend/table/fs.js` | Форматирование размеров файлов. |
|
||||
| `upload/frontend/table/on_files_change.js` | Выбор файлов, фильтрация, ZIP-разбор и fallback ошибок. |
|
||||
| `upload/frontend/table/on_folder_change.js` | Выбор папки через `webkitdirectory` и построение дерева. |
|
||||
| `upload/frontend/table/rebase_tree.js` | Добавление префикса пути без изменения базового `File.name`. |
|
||||
| `upload/frontend/table/render.js` | Рендер дерева, счётчик, поиск узлов и `flattenFiles()`. |
|
||||
| `upload/frontend/zip/list_zip_files.js` | Безопасный рекурсивный разбор ZIP через встроенный `fflate`. |
|
||||
### Слой 1: File Picker API
|
||||
- **Вход**: DOM-контейнер (`mount`), разрешённые расширения (`allowedExt`), лимиты (`limits`).
|
||||
- **Выход**: `picker.getFiles()` возвращает плоский массив объектов `{ path, name, size, file }`.
|
||||
- **Автономность**: Не зависит от транспорта и бэкенда.
|
||||
|
||||
### `site/` — demo-обёртка
|
||||
### Слой 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.
|
||||
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `site/app.py` | Flask entrypoint, `/`, `/health`, раздача bundle и совместимый маршрут исходников. |
|
||||
| `site/templates/index.html` | Demo-страница и вызов `FilePicker.initFilePicker()`. |
|
||||
| `site/static/style.css` | Стили demo-страницы. |
|
||||
| `site/routes/` | Текущий каталог маршрутов; прикладного upload backend в нём нет. |
|
||||
---
|
||||
|
||||
### `docs/`
|
||||
## Структура репозитория
|
||||
|
||||
| Путь | Назначение | Статус |
|
||||
|---|---|---|
|
||||
| `docs/CODE-REFERENCE.md` | Справочник актуального picker-кода и API. | Использовать |
|
||||
| `docs/PLAN-componentization.md` | Исторический план перехода к bundle API. | LEGACY, не использовать как план |
|
||||
| `docs/sonnet-architecture-review-prompt.md` | Исторический prompt ревью старой архитектуры. | LEGACY, не использовать |
|
||||
| `docs/*-architecture-review-response.md` | Ответы на исторические ревью. | История, не спецификация |
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
### `HISTORY/`
|
||||
---
|
||||
|
||||
Файлы `HISTORY/` фиксируют состояние проекта на даты ревью, тестов и решений.
|
||||
Они могут описывать удалённые файлы, VM-upload или старые версии. Это архив,
|
||||
а не инструкция: при расхождении с кодом руководствоваться только исходниками,
|
||||
`README.md`, `upload/README.md` и `docs/CODE-REFERENCE.md`.
|
||||
|
||||
## Локальный запуск
|
||||
## Быстрый старт и запуск
|
||||
|
||||
### 1. Установка зависимостей
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
npm install
|
||||
npm run build
|
||||
python site/app.py
|
||||
# → http://127.0.0.1:5000/
|
||||
```
|
||||
|
||||
Для запуска уже собранного demo достаточно иметь зависимости Python и
|
||||
содержимое `dist/`. `npm run build` требуется после изменения исходников
|
||||
`upload/frontend/`.
|
||||
### 2. Сборка фронтенд-бандлов
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Деплой на Nubes
|
||||
### 3. Запуск локального demo-сервера
|
||||
```bash
|
||||
python site/app.py
|
||||
# Сервер доступен по адресу http://127.0.0.1:5000/
|
||||
```
|
||||
В стенде встроен локальный **in-memory mock-буфер** (`/mock-buffer/`), поэтому demo полностью функционально локально без подключения к боевой ВМ.
|
||||
|
||||
- Точка входа: `site/app.py` (платформа запускает `python site/app.py`).
|
||||
- `app.run(host="0.0.0.0", port=5000, debug=False)`.
|
||||
- Маршрут `/health` → `200` (иначе liveness-проба платформы убивает под).
|
||||
- Без `site/__init__.py` и factory pattern.
|
||||
### 4. Запуск всех тестов
|
||||
```bash
|
||||
# Бэкенд тесты (pytest)
|
||||
pytest tests/ -v
|
||||
|
||||
## Как интегрировать picker в другой проект
|
||||
# Фронтенд тесты (Node.js test runner)
|
||||
npm test
|
||||
```
|
||||
|
||||
1. Скопировать `dist/file-picker.iife.js` или `dist/file-picker.esm.js` в свой проект.
|
||||
2. Подключить IIFE через `<script>` и вызвать `FilePicker.initFilePicker`, либо импортировать ESM-бандл.
|
||||
3. Передать `mount`, `allowedExt`, а при необходимости `labels`, `layout`, `limits` и `onChange`.
|
||||
4. Получить выбранные leaf-файлы через `picker.getFiles()`; каждый элемент содержит
|
||||
`path`, `name`, `size` и исходный `file`.
|
||||
5. После удаления компонента вызвать `picker.destroy()`.
|
||||
---
|
||||
|
||||
Исходники `upload/frontend/` нужны только для разработки и пересборки bundle;
|
||||
встроенная библиотека `fflate` уже включена в готовые bundle.
|
||||
## Инструкция: интеграция в сторонний сервис за 2 минуты
|
||||
|
||||
Разрешённые расширения демо: `.pdf`, `.doc`, `.docx`, `.txt`, `.md`.
|
||||
ZIP-файлы используются как контейнеры и раскрываются в браузере; сам ZIP не
|
||||
возвращается как leaf-файл. По умолчанию действуют ограничения: 1000 entries,
|
||||
100 MiB суммарно, 50 MiB на entry и глубина 20.
|
||||
Чтобы встроить оба слоя в целевой сервис (`drhider`, `contractor` и др.):
|
||||
|
||||
## Legacy
|
||||
### 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/backend/` — старый backend VM-upload, sessions и pull API;
|
||||
- `upload/frontend/table/init_upload_table.js` — старый entry point с готовыми DOM-узлами;
|
||||
- `upload/frontend/table/set_status.js` — неиспользуемый legacy helper;
|
||||
- `upload/frontend/upload/` — старый frontend VM-upload layer.
|
||||
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)
|
||||
```
|
||||
|
||||
Эти части не входят в сборку `build.mjs`, не импортируются текущим picker-ом и
|
||||
не являются частью API. Не добавлять их обратно при интеграции.
|
||||
### 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'],
|
||||
});
|
||||
|
||||
`PLAN.md`, `docs/PLAN-componentization.md`, review prompts и записи `HISTORY/`
|
||||
могут содержать описания старой VM/backend-архитектуры. Они нужны для аудита
|
||||
решений, но не являются актуальной спецификацией и не должны использоваться
|
||||
как план разработки.
|
||||
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>
|
||||
```
|
||||
|
||||
Актуальная инструкция интеграции находится в `upload/README.md`.
|
||||
### 4. Получение файлов в Слое 3
|
||||
Бизнес-логика получает файлы из оперативной памяти:
|
||||
```python
|
||||
files = get_files(session_id)
|
||||
# files: [("document.pdf", b"...bytes..."), ...]
|
||||
```
|
||||
Никаких временных файлов на диске. Полная безопасность и изоляция.
|
||||
|
||||
Полный справочник функций, состояния, DOM-контрактов и ограничений находится в
|
||||
[`docs/CODE-REFERENCE.md`](docs/CODE-REFERENCE.md).
|
||||
|
||||
История решений и проверок находится в [`HISTORY/`](HISTORY/).
|
||||
|
||||
Reference in New Issue
Block a user