Integrate Obfuscator file picker into Contracts
Deploy contracts-flask / validate (push) Canceled after 0s
Deploy contracts-flask / validate (push) Canceled after 0s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+56
-83
@@ -30,49 +30,49 @@ upload/
|
||||
|
||||
## Подключение фронта
|
||||
|
||||
Подключить ES-модули (`<script type="module">`) и собрать слой 1:
|
||||
Исходники — ES-модули. Для обычной страницы соберите IIFE-бандл:
|
||||
|
||||
```js
|
||||
import { initUploadTable } from './upload/frontend/table/init_upload_table.js';
|
||||
import { uploadViaVM } from './upload/frontend/upload/upload_via_vm.js';
|
||||
|
||||
const cfg = {
|
||||
allowedExt: ['.pdf', '.doc', '.docx', '.txt', '.md'],
|
||||
maxFileBytes: 50 * 1024 * 1024,
|
||||
maxSessionBytes: 500 * 1024 * 1024,
|
||||
estMbSec: 12,
|
||||
};
|
||||
|
||||
const table = initUploadTable(cfg, {
|
||||
fileInput: document.getElementById('fileInput'), // <input type="file" multiple>
|
||||
folderInput: document.getElementById('folderInput'), // <input webkitdirectory>
|
||||
tableBody: document.getElementById('fileList'), // <tbody>
|
||||
countEl: document.getElementById('fileCount'),
|
||||
uploadBtnEl: document.getElementById('uploadBtn'),
|
||||
onStatus(cls, text) { /* сообщения (cls: ''|'progress'|'done'|'error') */ },
|
||||
});
|
||||
|
||||
// Слой 2 — закачка учитываемых файлов на ВМ:
|
||||
// idxInSf[k] — индекс k-го отправляемого файла в строках таблицы (своя логика маппинга)
|
||||
const res = await uploadViaVM(toSend, cfg.vmUploadUrl, {
|
||||
session: currentSid,
|
||||
onStatus(k, html) { table.setStatus(idxInSf[k], html); }, // прогресс → ячейка таблицы
|
||||
onUploadStatus(text) { /* статусная строка */ },
|
||||
onXHR(xhr) { activeXHR = xhr; }, // регистрация активного PUT для отмены (abort)
|
||||
});
|
||||
// res = {ok:true, session, count} | {ok:false, error}
|
||||
// После этого у вас в сессии res.session лежат файлы — запускайте СВОЮ обработку.
|
||||
```sh
|
||||
cd upload/frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
API слоя 1 (`initUploadTable(cfg, els)` → `table`):
|
||||
- `addFiles(File[])` — дедуп + раскрытие ZIP + фильтр + лимиты;
|
||||
- `getFiles()` → `[{name, size, file}]` — **только учитываемые** (без сверхлимитных);
|
||||
- `getOverNames()` → `Set` — имена сверх лимита;
|
||||
- `setStatus(idx, html)` — статус в ячейке таблицы;
|
||||
- `render()` — перерисовать таблицу;
|
||||
- `clear()` — очистить список;
|
||||
- `setBusy(bool)` — заблокировать список на время загрузки/обработки;
|
||||
- `state` — доступ к состоянию (для слоя 3: установить `state.proc` для 3-секционной таблицы).
|
||||
Бандл появится в `site/static/dist/file-picker.iife.js`; Contracts отдаёт его по
|
||||
`/file-picker/file-picker.iife.js`. Инициализация picker:
|
||||
|
||||
```js
|
||||
const picker = window.FilePicker.initFilePicker({
|
||||
mount: '#filePicker',
|
||||
allowedExt: ['.pdf', '.doc', '.docx'],
|
||||
onChange(files) { /* синхронизировать выбранные leaf-файлы с приложением */ },
|
||||
});
|
||||
```
|
||||
|
||||
Picker строит дерево файлов/папок/ZIP, раскрывает вложенные ZIP, фильтрует
|
||||
неподдерживаемые расширения и удаляет дубликаты по `path + size`. В выбранной
|
||||
папке корневой `obscure.lst` применяется как manifest включения/исключения.
|
||||
`getFiles()` возвращает leaf-записи `{id, path, name, size, file}`. Методы
|
||||
`setStatus(id, {kind, text})`, `setDocument(id, docId)`, `setMessage(text)`,
|
||||
`remove(id)`, `clear()`, `render()` и `destroy()` обслуживают UI.
|
||||
|
||||
Передача файлов выполняется по одному файлу за вызов/итерацию:
|
||||
|
||||
```js
|
||||
const result = await window.FilePicker.uploadViaVM(files, {
|
||||
vmUploadUrl: 'https://example.invalid/contracts-upload/',
|
||||
backendUploadUrl: '/api/upload_refs',
|
||||
getExtraPayload(fileEntry) {
|
||||
return {batch_id, contract_id, zip_source: fileEntry.zip_source};
|
||||
},
|
||||
onFileComplete({result}) { /* результат sink: doc_id, parsed или ошибка */ },
|
||||
});
|
||||
```
|
||||
|
||||
`uploadViaVM` принимает `AbortSignal`, сообщает прогресс и передаёт поля
|
||||
`getExtraPayload` в JSON POST к `upload_refs`. При настроенном `sink` результат
|
||||
каждого файла передаётся в `onFileComplete` как `result`. Обработку и сохранение
|
||||
результатов приложение определяет самостоятельно.
|
||||
|
||||
---
|
||||
|
||||
@@ -95,45 +95,30 @@ app.register_blueprint(create_upload_refs_blueprint({
|
||||
}))
|
||||
```
|
||||
|
||||
Эндпоинт: `POST {apiPrefix}/upload_refs` — принимает JSON
|
||||
`{"session": "...", "files": [{"name", "size", "url"}]}`, тянет каждый файл с ВМ
|
||||
(SSRF-валидация по `vmUploadPrefix`, `_safe_name`, ретраи), кладёт в сессию.
|
||||
Возвращает `{"ok": true, "session", "count"}`.
|
||||
Эндпоинт: `POST {apiPrefix}/upload_refs` — принимает JSON с `session`, `files`
|
||||
и сквозными полями `batch_id`, `contract_id`, `zip_source`; тянет каждый файл с
|
||||
ВМ (SSRF-валидация по `vmUploadPrefix`, безопасное имя, ретраи). Без `sink`
|
||||
файлы кладутся в in-memory сессию и ответ содержит `session`/`count`. С `sink`
|
||||
ответ содержит `results` с результатом каждого файла и обработку/хранение
|
||||
определяет приложение.
|
||||
|
||||
Сессия: `create_session()` → sid; `add_file(sid, name, content)` (лимит 500МБ);
|
||||
`get_files(sid)` → `[(name, bytes), ...]` или `None`. TTL 30 мин (таймер в фоне).
|
||||
|
||||
---
|
||||
|
||||
## Раздача модуля в Flask (обязательно)
|
||||
## Раздача фронтенда
|
||||
|
||||
Фронт-модули — ES-модули, браузер грузит их по HTTP (не через file://).
|
||||
Добавьте route, который отдаёт папку `upload/frontend` (как в drhider `site/routes/main_bp.py`):
|
||||
|
||||
```python
|
||||
# в любом blueprint приложения
|
||||
import os
|
||||
from flask import send_from_directory
|
||||
|
||||
_UPLOAD_FRONTEND = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||
"upload", "frontend")
|
||||
|
||||
@bp.route("/upload/<path:filename>")
|
||||
def upload_frontend(filename):
|
||||
return send_from_directory(_UPLOAD_FRONTEND, filename)
|
||||
```
|
||||
|
||||
Тогда импорты в браузере: `import { initUploadTable } from '/upload/table/init_upload_table.js';`.
|
||||
|
||||
> Если фронт-модули хостятся отдельно (другой домен/приложение) — путь `/upload/...`
|
||||
> и CORS настраиваются под ваш случай (правка приложения/nginx, не кода модуля).
|
||||
В `contracts-flask` `pages_bp.py` отдаёт исходные модули по `/upload/<path>`
|
||||
и собранный IIFE по `/file-picker/<path>`. Для другого приложения нужно
|
||||
раздавать его собранный `file-picker.iife.js` по HTTP и подключать `<script src>`.
|
||||
|
||||
---
|
||||
|
||||
## Слой 3: как подключить обработку (опционально)
|
||||
## Слой 3: обработка приложения
|
||||
|
||||
Модуль отдаёт файлы в сессию, а обрабатываете их ВЫ (обфускация, конвертация, ...).
|
||||
После `uploadViaVM` файлы лежат в `res.session`:
|
||||
Без `sink` модуль отдаёт файлы в in-memory сессию, а обрабатываете их ВЫ
|
||||
(обфускация, конвертация и т. п.). После `uploadViaVM` файлы лежат в `res.session`:
|
||||
|
||||
```python
|
||||
files = get_files(res.session) # [(name, bytes), ...]
|
||||
@@ -141,21 +126,9 @@ files = get_files(res.session) # [(name, bytes), ...]
|
||||
store_result(res.session, result_zip) # если нужно отдать результат обратно
|
||||
```
|
||||
|
||||
Для 3-секционной таблицы прогресса (готово/текущий/ожидают) модуль предоставляет
|
||||
`renderProcTable` — достаточно дать слою 3 доступ к `table.state.proc`:
|
||||
|
||||
```js
|
||||
table.state.proc = { phase: 'processing', procState: {}, procExtractRate: null };
|
||||
function syncProcCtx() {
|
||||
table.state.proc.phase = phase;
|
||||
table.state.proc.procState = procState; // {idx: {st, elapsed, eta, chars, t0}}
|
||||
table.state.proc.procExtractRate = rate; // сек/МБ (для оценок ожидающих)
|
||||
}
|
||||
syncProcCtx();
|
||||
table.render(); // сам выберет renderProcTable при phase==='processing'
|
||||
```
|
||||
|
||||
Статусы в ячейках: `table.setStatus(idx, html)`.
|
||||
С `sink` файлы не попадают в in-memory сессию: обработчик получает их как
|
||||
`sink(name, content, batch_id=..., contract_id=..., zip_source=...)`, а результат
|
||||
каждой обработки возвращается в `onFileComplete`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user