feat: implement Layer 2 per-file transit, RAM session, mock buffer, and tests (v0.2.0)

This commit is contained in:
“Naeel”
2026-09-06 12:01:56 +03:00
parent 0ea7ba058c
commit 112c84f11a
34 changed files with 2200 additions and 162 deletions
+163 -96
View File
@@ -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/).