From 49bb4d70d86e081a8ea085f450e615ef31c4538d Mon Sep 17 00:00:00 2001 From: Repinoid Date: Tue, 15 Sep 2026 12:23:31 +0300 Subject: [PATCH] feat: integrate upload-platform v0.2.2 into drhider (v0.0.78) - Update upload/ module to v0.2.2 with modular Layer 1 (FilePicker) and Layer 2 (Streaming transit upload) - Replace legacy manual file table in site/templates/index.html with FilePicker.initFilePicker - Wire uploadViaVM with per-file status updates and abort signal support - Add dist bundles to dist/ and site/static/dist/ with routes in site/routes/main_bp.py - Add test_hardening.py and test_safe_name.py from upload-platform - Bump version to 0.0.78 in site/app.py - Document integration plan and report in History/upload-integration/ --- .gitignore | 3 + History/README.md | 6 + ...-09-15-upload-platform-integration-plan.md | 102 ++ ...upload-platform-v022-integration-report.md | 46 + History/upload-integration/README.md | 21 + dist/file-picker.esm.js | 1194 ++++++++++++++++ dist/file-picker.iife.js | 1218 +++++++++++++++++ site/app.py | 2 +- site/routes/main_bp.py | 18 +- site/static/dist/file-picker.esm.js | 1194 ++++++++++++++++ site/static/dist/file-picker.iife.js | 1218 +++++++++++++++++ site/templates/index.html | 472 +++---- tests/test_hardening.py | 116 ++ tests/test_safe_name.py | 17 + tests/test_upload.py | 2 +- upload/README.md | 204 +-- upload/__init__.py | 11 +- upload/backend/session/__init__.py | 2 +- upload/backend/upload_refs/__init__.py | 17 +- upload/backend/upload_refs/blueprint.py | 62 +- upload/backend/upload_refs/config.py | 2 +- upload/config.example.json | 14 +- upload/frontend/index.js | 193 +++ upload/frontend/package.json | 6 - upload/frontend/table/add_file_with_dedup.js | 93 +- upload/frontend/table/esc.js | 23 +- upload/frontend/table/est_for_file.js | 8 - upload/frontend/table/fmt_sec.js | 6 - upload/frontend/table/fs.js | 18 +- upload/frontend/table/init_upload_table.js | 56 - upload/frontend/table/on_files_change.js | 106 +- upload/frontend/table/on_folder_change.js | 120 +- upload/frontend/table/proc_row.js | 13 - upload/frontend/table/rebase_tree.js | 16 + upload/frontend/table/render.js | 145 +- upload/frontend/table/render_proc_table.js | 74 - upload/frontend/table/rm_file.js | 19 - upload/frontend/table/set_status.js | 6 - upload/frontend/test_upload_frontend.mjs | 344 ----- upload/frontend/test_upload_layer2.mjs | 199 --- upload/frontend/upload/put_to_vm.js | 81 +- upload/frontend/upload/upload_via_vm.js | 217 ++- upload/frontend/zip/decode_zip_name.js | 22 - upload/frontend/zip/dos_to_ms.js | 11 - upload/frontend/zip/inflate_raw.js | 8 - upload/frontend/zip/list_zip_files.js | 141 +- upload/frontend/zip/parse_zip.js | 50 - 47 files changed, 6355 insertions(+), 1561 deletions(-) create mode 100644 History/upload-integration/2026-09-15-upload-platform-integration-plan.md create mode 100644 History/upload-integration/2026-09-15-upload-platform-v022-integration-report.md create mode 100644 History/upload-integration/README.md create mode 100644 dist/file-picker.esm.js create mode 100644 dist/file-picker.iife.js create mode 100644 site/static/dist/file-picker.esm.js create mode 100644 site/static/dist/file-picker.iife.js create mode 100644 tests/test_hardening.py create mode 100644 tests/test_safe_name.py create mode 100644 upload/frontend/index.js delete mode 100644 upload/frontend/package.json delete mode 100644 upload/frontend/table/est_for_file.js delete mode 100644 upload/frontend/table/fmt_sec.js delete mode 100644 upload/frontend/table/init_upload_table.js delete mode 100644 upload/frontend/table/proc_row.js create mode 100644 upload/frontend/table/rebase_tree.js delete mode 100644 upload/frontend/table/render_proc_table.js delete mode 100644 upload/frontend/table/rm_file.js delete mode 100644 upload/frontend/table/set_status.js delete mode 100644 upload/frontend/test_upload_frontend.mjs delete mode 100644 upload/frontend/test_upload_layer2.mjs delete mode 100644 upload/frontend/zip/decode_zip_name.js delete mode 100644 upload/frontend/zip/dos_to_ms.js delete mode 100644 upload/frontend/zip/inflate_raw.js delete mode 100644 upload/frontend/zip/parse_zip.js diff --git a/.gitignore b/.gitignore index c7644af..69e83b4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ htmlcov/ *.log build/ dist/ +!dist/ +!dist/file-picker.esm.js +!dist/file-picker.iife.js *.egg-info/ # TMP/ # about1500.md diff --git a/History/README.md b/History/README.md index 63d4191..d26e0d2 100644 --- a/History/README.md +++ b/History/README.md @@ -18,6 +18,7 @@ | [infra/](infra/) | Инфраструктура: кластер, ingress, RST-диагностика, HTTP/2, ВМ-буфер | | [ux-frontend/](ux-frontend/) | Фичи фронтенда: таймеры, имена файлов, ZIP-раскрытие, лимиты, прерывание/ETA | | [upload/](upload/) | Загрузка файлов: диагностика, лимиты, стресс-тесты | +| [upload-integration/](upload-integration/) | Интеграция переиспользуемого модуля upload-platform | | [code-fixes/](code-fixes/) | Исправления кода по версиям (баги, оптимизация, безопасность, логирование) | | [tests/](tests/) | Тесты и бенчмарки: замеры UI, обфускация, нагрузка | @@ -122,6 +123,11 @@ - **2026-08-23-upload-stress-test.md** — тесты загрузки и стресс (v0.0.59–0.0.60, кластер naeel-test-3). - **2026-08-24-upload-limits.md** — v0.0.62: лимиты 50 МБ на файл, 500 МБ на сессию. +## upload-integration/ — интеграция платформы загрузки + +- [README.md](upload-integration/README.md) — стандарт и журнал процесса интеграции `upload-platform`. +- [2026-09-15-upload-platform-integration-plan.md](upload-integration/2026-09-15-upload-platform-integration-plan.md) — детальный архитектурный план и этапы интеграции версии v0.2.2 в drhider. + ## code-fixes/ — исправления кода (по версиям) - **2026-08-31-vm-sync-readme.md** — документирован `rsync` текущего проекта на ВМ diff --git a/History/upload-integration/2026-09-15-upload-platform-integration-plan.md b/History/upload-integration/2026-09-15-upload-platform-integration-plan.md new file mode 100644 index 0000000..0a4a291 --- /dev/null +++ b/History/upload-integration/2026-09-15-upload-platform-integration-plan.md @@ -0,0 +1,102 @@ +# План интеграции upload-platform (v0.2.2) в drhider + +**Дата:** 2026-09-15 +**Статус:** План составлен, ожидает команды на выполнение +**Целевой сервис:** drhider (Managed Flask на платформе Штурвал) +**Источник модуля:** `upload-platform/` (релиз v0.2.2, commit `df0576a`) + +--- + +## 1. Контекст и цели + +В `drhider` (v0.0.77) модуль загрузки `upload/` зафиксирован в промежуточном состоянии: +- Фронтенд Слоя 1 использует устаревшую табличную вёрстку с ручным парсингом ZIP без поддержки древовидного сворачивания. +- Слой 2 работает по смешанной схеме, в то время как в `upload-platform` v0.2.2 реализован и протестирован чистый пофайловый транзит (Streaming transit: PUT $\to$ POST `/api/upload_refs` $\to$ исходящий pull в RAM $\to$ немедленный DELETE $\to$ repeat). + +**Цель интеграции:** +Заменить устаревшие наработки в `upload/` и `site/templates/index.html` на проверенную автономную версию `upload-platform` v0.2.2 без нарушения бизнес-логики обфускации (Слой 3: LLM, SSE-стриминг, генерация ZIP). + +--- + +## 2. Границы ответственности слоёв + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ БРАУЗЕР │ +│ [ Слой 1: FilePicker ] ──► [ Слой 2: uploadViaVM ] │ +└─────────────────────────────────────┬──────────────────────────────────┘ + │ 1. PUT файл + ▼ + ┌─────────────────────────────┐ + │ ВМ-буфер (RAM WebDAV) │ + └──────────────┬──────────────┘ + │ 3. Исходящий GET (pull в RAM) + │ 4. DELETE с ВМ + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ FLASK BACKEND │ +│ [ Слой 2: Blueprint upload_refs ] ──► [ RAM-сессия: add_file ] │ +│ │ │ +│ [ Слой 3: drhider engine ] ◄─────────────────────┘ get_files(sid) │ +│ (SSE-прогресс, обфускация, mapping, download) │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +- **Слой 1 (Фронтенд)**: инициализация через `FilePicker.initFilePicker()`. Полностью изолирован от транспорта. Возвращает список объектов `File` через `picker.getFiles()`. +- **Слой 2 (Фронтенд + Бэкенд)**: `uploadViaVM` передаёт файлы по одному через буфер на ВМ в RAM сессии Flask. +- **Слой 3 (drhider)**: логика обфускации забирает готовые файлы из `upload.backend.session.get_files(sid)` и запускает существующий пайплайн (`/api/drhider/`). + +--- + +## 3. Этапы интеграции + +### Этап 1. Синхронизация модуля ядра (`upload/`) +1. Заменить содержимое `upload/` в корне `drhider` на актуальную версию из `upload-platform/upload/`: + - `upload/backend/`: актуальные `upload_refs/` (поддержка `httpxTransport`, безопасный pull, санитизация `safe_name`) и `session/` (хранилище в RAM, TTL, блокировки). + - `upload/frontend/`: чистые ES-модули (`index.js`, `table/`, `upload/`, `zip/` на базе `fflate`). +2. Скопировать готовые бандлы из `upload-platform/dist/` в `site/static/dist/` (или настроить прямую раздачу из `upload/frontend` через `main_bp.py`). + +### Этап 2. Проверка и настройка бэкенда (`site/routes/`) +1. Проверить регистрацию Blueprint в [site/routes/__init__.py](site/routes/__init__.py): + ```python + app.register_blueprint(create_upload_refs_blueprint({ + "apiPrefix": "/api", + "vmUploadPrefix": "https://contracts.kube5s.ru/drhider-upload/", + "maxFileBytes": 50 * 1024 * 1024, + "maxSessionBytes": 500 * 1024 * 1024, + "ttlSeconds": 30 * 60, + "pullRetries": 3, + "pullRetryDelay": 2, + })) + ``` +2. Убедиться, что [site/routes/api_bp.py](site/routes/api_bp.py) бесшовно использует сессии из обновлённого `upload.backend.session`. + +### Этап 3. Обновление пользовательского интерфейса (`site/templates/index.html`) +1. Заменить устаревшую ручную вёрстку таблицы и инпутов на контейнер для монтирования Слой 1: + ```html +
+ ``` +2. Подключить модуль `initFilePicker` и сконфигурировать фильтрацию по `.pdf, .doc, .docx, .txt, .md`. +3. Модифицировать обработчик кнопки «🛡️ Обфусцировать»: + - Вызов `uploadViaVM(files, { vmUploadUrl, backendUploadUrl, onFileStatus, onProgress })`. + - По завершении загрузки всех файлов — автоматический переход к Стек/Фазе 2: запуск SSE-стрима `/api/drhider/`. +4. Сохранить существующий трёхсекционный рендер обработки (Готово / Обрабатывается / Ожидают) и индикацию ETA/статистики LLM. + +### Этап 4. Тестирование и валидация +1. **Автономные тесты модуля `upload`**: + - Запуск pytest для `upload/` внутри `drhider`. +2. **Интеграционные тесты `drhider`**: + - Запуск существующих тестов `tests/test_builder.py`, `tests/test_extractor.py`, `tests/test_replacer.py`, `tests/test_scanner.py`. +3. **Ручная/Smoke проверка сквозного цикла**: + - Выбор одиночного документа, папки и архива ZIP. + - Проверка пофайловой загрузки через буфер и немедленного удаления с ВМ. + - Проверка успешного прохождения двухпроходной обфускации и скачивания результирующего архива. + +--- + +## 4. Контрольные точки (Definition of Done) +- [ ] Все тесты `pytest tests/` завершаются успешно (PASS). +- [ ] Отсутствуют временные файлы на диске (всё в RAM). +- [ ] Не нарушена логика отмены (`cancel`) и работы со стримами SSE. +- [ ] Сборка и синтаксис проверены через `py_compile`. +- [ ] Документ с отчётом о завершении зафиксирован в `History/upload-integration/`. diff --git a/History/upload-integration/2026-09-15-upload-platform-v022-integration-report.md b/History/upload-integration/2026-09-15-upload-platform-v022-integration-report.md new file mode 100644 index 0000000..16e3482 --- /dev/null +++ b/History/upload-integration/2026-09-15-upload-platform-v022-integration-report.md @@ -0,0 +1,46 @@ +# 2026-09-15: Интеграция upload-platform v0.2.2 в drhider + +## 1. Цель и контекст + +Интеграция протестированного модульного релиза `upload-platform` v0.2.2 в сервис `drhider`. +Версия сервиса повышена: `v0.0.77` $\rightarrow$ `v0.0.78`. + +## 2. Выполненные изменения + +### Ядро модуля `upload/` +- **Слой 1 (Frontend)**: переход на модульный `initFilePicker` с распаковкой ZIP через `fflate`, древовидным представлением папок и архивов, встроенным сворачиванием/разворачиванием групп и дедупликацией. Устаревшие ручные парсеры удалены. +- **Слой 2 (Transit Upload)**: + - **Frontend**: пофайловый стриминговый транзит `uploadViaVM` (PUT на ВМ $\rightarrow$ POST `/api/upload_refs` $\rightarrow$ исходящий pull $\rightarrow$ DELETE из буфера $\rightarrow$ повтор для следующего файла). + - **Backend**: обновлён Blueprint `create_upload_refs_blueprint`: добавлена поддержка локальных путей/mock-буфера (`is_path_only_prefix`), кастомного `httpxTransport` для безопасного тестирования, возвращение параметра `added`. + - **Session**: in-memory хранилище сессий с потокобезопасным TTL и проверкой лимитов. + +### Приложение Flask и маршруты +- В [site/routes/main_bp.py](site/routes/main_bp.py) добавлены маршруты раздачи бандлов: `/file-picker/` и `/dist/`. +- Бандлы `file-picker.iife.js` и `file-picker.esm.js` размещены в `dist/` и `site/static/dist/`. + +### Пользовательский интерфейс ([site/templates/index.html](site/templates/index.html)) +- Вёрстка выбора файлов заменена контейнером `#filePicker`. +- Инициализирован `FilePicker.initFilePicker` с разрешёнными расширениями (`.pdf`, `.doc`, `.docx`, `.txt`, `.md`). +- Логика кнопки «🛡️ Обфусцировать» переведена на `FilePicker.uploadViaVM` с поддержкой отмены (`AbortController`) и пофайловым отображением статусов. +- Сохранён и адаптирован этап 2 обработки (SSE `/api/process_stream/`, тикеры времени, статистика LLM, выгрузка ZIP и CSV). + +### Тесты +- Скопированы и адаптированы тесты: + - [tests/test_hardening.py](tests/test_hardening.py) (проверка устойчивости, malformed requests, SSRF, traversal). + - [tests/test_safe_name.py](tests/test_safe_name.py) (санитизация путей). +- Обновлён [tests/test_upload.py](tests/test_upload.py) (длина сессии `uuid4().hex` = 32 символа). + +## 3. Проверки и верификация + +- `pytest tests/`: **39 passed** в 4 тестовых файлах. +- `python3 tests/test_builder.py`: **20/20 PASS**. +- `python3 tests/test_extractor.py`: **10/10 PASS**. +- `python3 tests/test_replacer.py`: **10/10 PASS**. +- `python3 tests/test_scanner.py`: **20/20 PASS**. +- `python3 tests/test_zip.py`: **28/28 PASS**. +- Проверка синтаксиса `py_compile` по всем `.py` файлам проекта: **ALL PY FILES COMPILED OK**. +- Проверка импортов ядра: `from drhider import obfuscate_files, LLMClient` $\rightarrow$ **OK**. +- Проверка HTTP: + - `GET /` $\rightarrow$ **HTTP 200**, HTML отрендерен (28571 байт). + - `GET /health` $\rightarrow$ **HTTP 200**, `{"ok":true,"version":"0.0.78"}`. + - `GET /file-picker/file-picker.iife.js` $\rightarrow$ **HTTP 200** (44465 байт). diff --git a/History/upload-integration/README.md b/History/upload-integration/README.md new file mode 100644 index 0000000..c6d2048 --- /dev/null +++ b/History/upload-integration/README.md @@ -0,0 +1,21 @@ +# History / upload-integration — Журнал интеграции upload-platform + +В этом каталоге фиксируется полный жизненный цикл интеграции автономного модуля +`upload-platform` (Слой 1: File Picker + Слой 2: пофайловый транзит через ВМ-буфер) +в сервис `drhider`. + +## Стандарт ведения документации по интеграции + +Каждый этап или значимый результат документируется отдельным файлом по формату: +`YYYY-MM-DD-<краткое-действие>.md` + +Обязательные разделы каждого документа: +1. **Цель и контекст**: что делалось, версии компонентов (`drhider`, `upload-platform`). +2. **Выполненные изменения**: затронутые файлы и границы слоёв (Слой 1, Слой 2, Слой 3). +3. **Проверки и тесты**: команды запуска тестов и результаты (PASS/FAIL). +4. **Обнаруженные нюансы/отклонения**: проблемы обратной совместимости, сетевые или платформенные особенности. + +## Документы раздела + +- [2026-09-15-upload-platform-integration-plan.md](2026-09-15-upload-platform-integration-plan.md) — детальный архитектурный план и этапы интеграции версии v0.2.2 в drhider. +- [2026-09-15-upload-platform-v022-integration-report.md](2026-09-15-upload-platform-v022-integration-report.md) — отчёт об успешной интеграции и верификации v0.2.2 (версия drhider v0.0.78). diff --git a/dist/file-picker.esm.js b/dist/file-picker.esm.js new file mode 100644 index 0000000..d9f7ac1 --- /dev/null +++ b/dist/file-picker.esm.js @@ -0,0 +1,1194 @@ +// node_modules/fflate/esm/browser.js +var u8 = Uint8Array; +var u16 = Uint16Array; +var i32 = Int32Array; +var fleb = new u8([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, + /* unused */ + 0, + 0, + /* impossible */ + 0 +]); +var fdeb = new u8([ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, + /* unused */ + 0, + 0 +]); +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +var freb = function(eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = j - b[i] << 5 | i; + } + } + return { b, r }; +}; +var _a = freb(fleb, 2); +var fl = _a.b; +var revfl = _a.r; +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0); +var fd = _b.b; +var revfd = _b.r; +var rev = new u16(32768); +for (i = 0; i < 32768; ++i) { + x = (i & 43690) >> 1 | (i & 21845) << 1; + x = (x & 52428) >> 2 | (x & 13107) << 2; + x = (x & 61680) >> 4 | (x & 3855) << 4; + rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; +} +var x; +var i; +var hMap = (function(cd, mb, r) { + var s = cd.length; + var i = 0; + var l = new u16(mb); + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = le[i - 1] + l[i - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + if (cd[i]) { + var sv = i << 4 | cd[i]; + var r_1 = mb - cd[i]; + var v = le[cd[i] - 1]++ << r_1; + for (var m = v | (1 << r_1) - 1; v <= m; ++v) { + co[rev[v] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i]; + } + } + } + return co; +}); +var flt = new u8(288); +for (i = 0; i < 144; ++i) + flt[i] = 8; +var i; +for (i = 144; i < 256; ++i) + flt[i] = 9; +var i; +for (i = 256; i < 280; ++i) + flt[i] = 7; +var i; +for (i = 280; i < 288; ++i) + flt[i] = 8; +var i; +var fdt = new u8(32); +for (i = 0; i < 32; ++i) + fdt[i] = 5; +var i; +var flrm = /* @__PURE__ */ hMap(flt, 9, 1); +var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1); +var max = function(a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; +}; +var bits = function(d, p, m) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8) >> (p & 7) & m; +}; +var bits16 = function(d, p) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); +}; +var shft = function(p) { + return (p + 7) / 8 | 0; +}; +var slc = function(v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + return new u8(v.subarray(s, e)); +}; +var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + // determined by compression function + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" + // determined by unknown compression method +]; +var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; +}; +var inflt = function(dat, st, buf, dict) { + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + var resize = noBuf || st.i != 2; + var noSt = st.i; + if (noBuf) + buf = new u8(sl * 3); + var cbuf = function(l2) { + var bl = buf.length; + if (l2 > bl) { + var nbuf = new u8(Math.max(bl * 2, l2)); + nbuf.set(buf); + buf = nbuf; + } + }; + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + var tbts = sl * 8; + do { + if (!lm) { + final = bits(dat, pos, 1); + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + l); + buf.set(dat.subarray(s, t), bt); + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + var ldt = new u8(tl); + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + var clb = max(clt), clbmsk = (1 << clb) - 1; + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl; ) { + var r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + var s = r >> 4; + if (s < 16) { + ldt[i++] = s; + } else { + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } else { + var add = sym - 254; + if (sym > 264) { + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + var d = dm[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1, pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); +}; +var et = /* @__PURE__ */ new u8(0); +var b2 = function(d, b) { + return d[b] | d[b + 1] << 8; +}; +var b4 = function(d, b) { + return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0; +}; +var b8 = function(d, b) { + return b4(d, b) + b4(d, b + 4) * 4294967296; +}; +function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder(); +var tds = 0; +try { + td.decode(et, { stream: true }); + tds = 1; +} catch (e) { +} +var dutf8 = function(d) { + for (var r = "", i = 0; ; ) { + var c = d[i++]; + var eb = (c > 127) + (c > 223) + (c > 239); + if (i + eb > d.length) + return { s: r, r: slc(d, i - 1) }; + if (!eb) + r += String.fromCharCode(c); + else if (eb == 3) { + c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023); + } else if (eb & 1) + r += String.fromCharCode((c & 31) << 6 | d[i++] & 63); + else + r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63); + } +}; +function strFromU8(dat, latin1) { + if (latin1) { + var r = ""; + for (var i = 0; i < dat.length; i += 16384) + r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384)); + return r; + } else if (td) { + return td.decode(dat); + } else { + var _a2 = dutf8(dat), s = _a2.s, r = _a2.r; + if (r.length) + err(8); + return s; + } +} +var slzh = function(d, b) { + return b + 30 + b2(d, b + 26) + b2(d, b + 28); +}; +var zh = function(d, b, z) { + var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl; + var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2]; + return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off]; +}; +var z64hs = function(d, b, l, z, sc, su, off) { + var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l; + var nf = nsc + nsu + noff; + if (z && nf) { + for (; b + 4 < e; b += 4 + b2(d, b + 2)) { + if (b2(d, b) == 1) { + return [ + nsc ? b8(d, b + 4 + 8 * nsu) : sc, + nsu ? b8(d, b + 4) : su, + noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off, + 1 + ]; + } + } + if (z < 2) + err(13); + } + return [sc, su, off, 0]; +}; +function unzipSync(data, opts) { + var files = {}; + var e = data.length - 22; + for (; b4(data, e) != 101010256; --e) { + if (!e || data.length - e > 65558) + err(13); + } + ; + var c = b2(data, e + 8); + if (!c) + return {}; + var o = b4(data, e + 16); + var z = b4(data, e - 20) == 117853008; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 101075792; + if (z) { + c = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + for (var i = 0; i < c; ++i) { + var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off); + o = no; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_2 + })) { + if (!c_2) + files[fn] = slc(data, b, b + sc); + else if (c_2 == 8) + files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) }); + else + err(14, "unknown compression type " + c_2); + } + } + return files; +} + +// upload/frontend/table/rebase_tree.js +function rebaseTree(root, prefix) { + root.path = `${prefix}/${root.path}`; + if (root.file) root.file = new File([root.file], root.file.name, { lastModified: root.file.lastModified }); + root.children.forEach((child) => rebaseTree(child, prefix)); + return root; +} + +// upload/frontend/zip/list_zip_files.js +var DEFAULT_LIMITS = { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 +}; +function extensionAllowed(name, allowedExt) { + const lowerName = name.toLowerCase(); + return allowedExt.some((extension) => lowerName.endsWith(extension.toLowerCase())); +} +function makeFile(data, name) { + return new File([data], name.split("/").at(-1)); +} +function safeEntryParts(entryName) { + if (!entryName || entryName.startsWith("/") || entryName.includes("\\")) return null; + const parts = entryName.split("/"); + if (parts.some((part) => !part || part === "." || part === "..")) return null; + return parts; +} +function node(kind, name, path, children = [], file = null) { + return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" }; +} +function addPath(root, parts, fileNode) { + let current = root; + parts.forEach((part, index) => { + const last = index === parts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? fileNode : node("folder", part, `${current.path}/${part}`); + current.children.push(child); + } + current = child; + }); +} +async function listEntries(data, zipName, allowedExt, depth, limits, budget) { + if (depth > limits.maxDepth) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0433\u043B\u0443\u0431\u043E\u043A\u0430\u044F \u0432\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0441\u0442\u044C ZIP"); + const entries = unzipSync(data, { + filter: (entry) => { + budget.entries += 1; + if (budget.entries > limits.maxEntries) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E ZIP entries"); + const lowerName = entry.name.toLowerCase(); + if (!extensionAllowed(lowerName, allowedExt) && !lowerName.endsWith(".zip")) return false; + if (entry.originalSize > limits.maxEntryBytes) return false; + if (budget.totalBytes + entry.originalSize > limits.maxTotalBytes) { + throw new Error("\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u0441\u0443\u043C\u043C\u0430\u0440\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0445 ZIP entries"); + } + budget.totalBytes += entry.originalSize; + return true; + } + }); + const root = node("zip", zipName, zipName); + for (const [entryName, entryData] of Object.entries(entries)) { + if (entryName.endsWith("/")) continue; + const parts = safeEntryParts(entryName); + if (!parts) continue; + const normalizedName = parts.join("/"); + if (normalizedName.toLowerCase().endsWith(".zip")) { + const nested = await listEntries(entryData, entryName, allowedExt, depth + 1, limits, budget); + if (nested) { + rebaseTree(nested, zipName); + nested.name = parts.at(-1); + addPath(root, parts, nested); + } + } else if (extensionAllowed(normalizedName, allowedExt)) { + const path = `${zipName}/${normalizedName}`; + const file = makeFile(entryData, path); + addPath(root, parts, node("file", parts.at(-1), path, [], file)); + } + } + return root.children.length ? root : null; +} +async function listZipFiles(file, allowedExt, customLimits = {}) { + const data = new Uint8Array(await file.arrayBuffer()); + const limits = { ...DEFAULT_LIMITS, ...customLimits }; + return listEntries(data, file.name, allowedExt, 0, limits, { entries: 0, totalBytes: 0 }); +} + +// upload/frontend/table/add_file_with_dedup.js +function addFileWithDedup(state, fileNode) { + const accept = (node2) => { + if (node2.kind === "file") { + const key = `${node2.path}\0${node2.file.size}`; + if (state.fileKeys.has(key)) return null; + state.fileKeys.add(key); + return node2; + } + node2.children = node2.children.map(accept).filter(Boolean); + return node2.children.length ? node2 : null; + }; + const accepted = accept(fileNode); + if (!accepted || accepted.kind !== "file" && !accepted.children.length) return false; + if (accepted.kind !== "file") { + const existing = state.nodes.find((node2) => node2.kind !== "file" && node2.path === accepted.path); + if (existing) { + mergeChildren(existing, accepted); + return true; + } + } + state.nodes.push(accepted); + return true; +} +function mergeChildren(target, incoming) { + incoming.children.forEach((child) => { + if (child.kind === "file") { + const duplicate = target.children.some((existing2) => existing2.kind === "file" && existing2.path === child.path && existing2.file.size === child.file.size); + if (!duplicate) target.children.push(child); + return; + } + const existing = target.children.find((candidate) => candidate.kind !== "file" && candidate.path === child.path); + if (existing) mergeChildren(existing, child); + else target.children.push(child); + }); +} + +// upload/frontend/table/esc.js +function esc(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); +} + +// upload/frontend/table/fs.js +function fs(bytes) { + return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`; +} + +// upload/frontend/table/render.js +var DEFAULT_RENDER_CONFIG = { + labels: { + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${fs(bytes)}` + }, + layout: { columns: ["path", "size", "status"] } +}; +function renderConfig(cfg = {}) { + return { + labels: { + ...DEFAULT_RENDER_CONFIG.labels, + ...cfg.labels || {}, + columns: { + ...DEFAULT_RENDER_CONFIG.labels.columns, + ...cfg.labels?.columns || {} + } + }, + layout: { ...DEFAULT_RENDER_CONFIG.layout, ...cfg.layout || {} } + }; +} +function renderNode(node2, depth, cfg) { + const isGroup = node2.kind !== "file"; + const padding = depth * 4; + const name = esc(node2.name); + const action = isGroup ? `` : `${name}`; + const remove = ``; + const pathAttribute = node2.kind === "file" ? ` data-path="${esc(node2.path)}"` : ""; + const cells = { + path: action, + size: node2.kind === "file" ? fs(node2.file.size) : "", + status: node2.error ? esc(node2.error) : node2.kind === "file" ? esc(cfg.labels.statusReady) : "" + }; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + const row = `` + columns.map((column) => `${cells[column] || ""}`).join("") + `${remove}`; + if (!isGroup || !node2.expanded) return row; + return row + node2.children.map((child) => renderNode(child, depth + 1, cfg)).join(""); +} +function render(state, elements, cfg) { + const normalized = renderConfig(cfg); + const columns = normalized.layout.columns?.length ? normalized.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node2) => renderNode(node2, 0, normalized)).join("") : `${esc(normalized.labels.empty)}`; + const files = []; + const visit = (node2) => { + if (node2.kind === "file") files.push(node2); + else node2.children.forEach(visit); + }; + state.nodes.forEach(visit); + const totalBytes = files.reduce((sum, node2) => sum + node2.file.size, 0); + elements.countEl.textContent = normalized.labels.count(files.length, totalBytes); +} +function findNode(nodes, id) { + for (const node2 of nodes) { + if (node2.id === id) return { node: node2, nodes }; + const found = findNode(node2.children || [], id); + if (found) return found; + } + return null; +} +function flattenFiles(nodes, result = []) { + nodes.forEach((node2) => { + if (node2.kind === "file") result.push({ path: node2.path, name: node2.name, size: node2.file.size, file: node2.file }); + else flattenFiles(node2.children, result); + }); + return result; +} + +// upload/frontend/table/on_files_change.js +async function addFiles(state, cfg, files, elements) { + for (const file of Array.from(files)) { + if (!file.name.toLowerCase().endsWith(".zip")) { + if (!cfg.allowedExt.some((extension) => file.name.toLowerCase().endsWith(extension.toLowerCase()))) { + continue; + } + addFileWithDedup(state, { + id: crypto.randomUUID(), + kind: "file", + name: file.name, + path: file.name, + file, + children: [], + expanded: true + }); + continue; + } + try { + const zipTree = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zipTree) addFileWithDedup(state, zipTree); + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } + render(state, elements, cfg); +} +function onFilesChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + await addFiles(state, cfg, elements.fileInputEl.files, elements); + } finally { + elements.fileInputEl.value = ""; + state.busy = false; + } + }; +} + +// upload/frontend/table/on_folder_change.js +function onFolderChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + const roots = /* @__PURE__ */ new Map(); + for (const file of Array.from(elements.folderInputEl.files)) { + const parts = (file.webkitRelativePath || file.name).split("/"); + const relativePath = parts.slice(1).join("/") || file.name; + const lowerPath = relativePath.toLowerCase(); + const rootName = parts[0] || file.name; + if (!roots.has(rootName)) { + roots.set(rootName, { + id: crypto.randomUUID(), + kind: "folder", + name: rootName, + path: rootName, + children: [], + expanded: false + }); + } + const root = roots.get(rootName); + const addToFolder = (node2) => { + let current = root; + const nodeParts = node2.path.split("/").slice(1); + nodeParts.forEach((part, index) => { + const last = index === nodeParts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? node2 : { + id: crypto.randomUUID(), + kind: "folder", + name: part, + path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`, + children: [], + expanded: false + }; + current.children.push(child); + } + current = child; + }); + }; + if (lowerPath.endsWith(".zip")) { + try { + const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zip) { + rebaseTree(zip, rootName); + addToFolder(zip); + } + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) { + addToFolder({ + id: crypto.randomUUID(), + kind: "file", + name: parts.at(-1), + path: `${rootName}/${relativePath}`, + file, + children: [], + expanded: true + }); + } + } + roots.forEach((root) => addFileWithDedup(state, root)); + elements.folderInputEl.value = ""; + render(state, elements, cfg); + } finally { + state.busy = false; + } + }; +} + +// upload/frontend/upload/put_to_vm.js +function putToVm(file, url, options = {}) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + if (options.onXHR) { + options.onXHR(xhr); + } + const abortHandler = () => { + xhr.abort(); + reject(new DOMException("Upload aborted", "AbortError")); + }; + if (options.signal) { + if (options.signal.aborted) { + reject(new DOMException("Upload aborted", "AbortError")); + return; + } + options.signal.addEventListener("abort", abortHandler, { once: true }); + } + xhr.open("PUT", url); + xhr.timeout = options.timeoutMs || 3e5; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable && options.onProgress) { + const pct = Math.round(e.loaded / e.total * 100); + options.onProgress(pct, e.loaded, e.total); + } + }; + xhr.onload = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`\u0411\u0443\u0444\u0435\u0440: HTTP ${xhr.status} ${xhr.statusText}`)); + } + }; + xhr.onerror = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error("\u0421\u0435\u0442\u0435\u0432\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435 \u0432 \u0431\u0443\u0444\u0435\u0440")); + }; + xhr.ontimeout = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error(`\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u0431\u0443\u0444\u0435\u0440\u0430 (${Math.round(xhr.timeout / 1e3)}\u0441)`)); + }; + xhr.onabort = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new DOMException("Upload aborted", "AbortError")); + }; + xhr.send(file); + }); +} + +// upload/frontend/upload/upload_via_vm.js +function generateUuid() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === "x" ? r : r & 3 | 8; + return v.toString(16); + }); +} +function normalizeFileEntry(item) { + if (item instanceof File || item instanceof Blob) { + return { name: item.name || "unnamed", size: item.size, file: item, path: item.name || "" }; + } + if (item && item.file) { + return { + name: item.name || item.file.name || "unnamed", + size: item.size ?? item.file.size ?? 0, + file: item.file, + path: item.path || item.name || "" + }; + } + throw new Error("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0431\u044A\u0435\u043A\u0442 \u0444\u0430\u0439\u043B\u0430"); +} +async function uploadViaVM(files, options = {}) { + if (!options.vmUploadUrl) { + return { ok: false, error: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 options.vmUploadUrl \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D" }; + } + const normalizedFiles = Array.from(files).map(normalizeFileEntry); + const total = normalizedFiles.length; + if (total === 0) { + return { ok: true, session: options.session || "", count: 0 }; + } + const backendUrl = options.backendUploadUrl || (options.apiBase || "") + "/api/upload_refs"; + const vmBase = options.vmUploadUrl.endsWith("/") ? options.vmUploadUrl : `${options.vmUploadUrl}/`; + let currentSession = options.session || ""; + let totalAdded = 0; + for (let k = 0; k < total; k++) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + const { name, size, file } = normalizedFiles[k]; + const fileKey = `${generateUuid()}_${k}`; + const fileUrl = `${vmBase}${fileKey}`; + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0432 \u0431\u0443\u0444\u0435\u0440 (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u23F3 \u0412 \u0431\u0443\u0444\u0435\u0440..."); + } + const t0 = performance.now(); + try { + await putToVm(file, fileUrl, { + signal: options.signal, + onXHR: options.onXHR, + onProgress(pct, loaded, fileTotal) { + const elapsed = (performance.now() - t0) / 1e3; + const speed = elapsed > 0 ? loaded / elapsed : 0; + if (options.onFileStatus) { + options.onFileStatus(k, `\u23F3 ${pct}% (${fs(speed)}/s)`); + } + if (options.onProgress) { + options.onProgress({ index: k, total, pct, speed, loaded, fileTotal, name }); + } + } + }); + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u0431\u0443\u0444\u0435\u0440\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u0430\u0439\u043B\u0430 ${name} \u0432 \u0431\u0443\u0444\u0435\u0440: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + if (options.onStatus) { + options.onStatus(`\u041F\u0440\u0438\u0451\u043C \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u043C (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u26A1 \u0412 \u0441\u0435\u0441\u0441\u0438\u044E..."); + } + try { + const resp = await fetch(backendUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session: currentSession, + files: [{ name, size, url: fileUrl }] + }), + signal: options.signal + }); + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} ${resp.statusText}`); + } + const data = await resp.json(); + if (!data.ok) { + throw new Error(data.error || "\u0411\u044D\u043A\u0435\u043D\u0434 \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443"); + } + currentSession = data.session || currentSession; + totalAdded = data.count ?? totalAdded + 1; + if (options.onFileStatus) { + options.onFileStatus(k, "\u2713 \u0414\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D"); + } + if (options.onFileComplete) { + options.onFileComplete({ + fileIndex: k, + name, + size, + session: currentSession, + totalAdded + }); + } + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430 \u0444\u0430\u0439\u043B\u0430 ${name} \u0431\u044D\u043A\u0435\u043D\u0434\u043E\u043C: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + } + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E. \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E \u0444\u0430\u0439\u043B\u043E\u0432: ${total}`); + } + return { ok: true, session: currentSession, count: totalAdded }; +} + +// upload/frontend/index.js +var DEFAULTS = { + allowedExt: [], + labels: { + pickFiles: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B", + pickFolder: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443", + clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${bytes} B` + }, + layout: { columns: ["path", "size", "status"], controls: ["files", "folder", "clear"] }, + limits: { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + } +}; +function resolveMount(mount) { + const element = typeof mount === "string" ? document.querySelector(mount) : mount; + if (!element) throw new Error("File picker mount element \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"); + return element; +} +function normalizeConfig(config) { + return { + ...config, + allowedExt: config.allowedExt || DEFAULTS.allowedExt, + labels: { + ...DEFAULTS.labels, + ...config.labels || {}, + columns: { ...DEFAULTS.labels.columns, ...config.labels?.columns || {} } + }, + layout: { ...DEFAULTS.layout, ...config.layout || {} }, + limits: { ...DEFAULTS.limits, ...config.limits || {} } + }; +} +function injectStyles() { + if (document.querySelector("style[data-file-picker]")) return; + const style = document.createElement("style"); + style.dataset.filePicker = ""; + style.textContent = ` +.file-picker { color: #17211b; font-family: Georgia, 'Times New Roman', serif; } +.file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; } +.file-picker .fp-toolbar button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; } +.file-picker .fp-toolbar button.secondary { color: #1f5a3b; background: #cbdcc9; } +.file-picker .fp-toolbar button.quiet { color: #5d6a61; background: transparent; } +.file-picker .fp-table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; } +.file-picker .fp-table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; } +.file-picker .fp-table th, .file-picker .fp-table td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; } +.file-picker .fp-table th { color: #56745f; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; } +.file-picker .fp-status, .file-picker .fp-count { color: #5d6a61; font: 13px sans-serif; } +.file-picker .fp-count { min-height: 22px; } +.file-picker .tree-name { display: inline-flex; align-items: center; gap: 8px; min-width: 240px; padding-top: 0; padding-bottom: 0; border: 0; background: transparent; color: #17211b; font: inherit; text-align: left; } +.file-picker .tree-group { padding-left: 0; cursor: pointer; } +.file-picker .tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; } +.file-picker .remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; } +.file-picker .tree-row td:first-child { white-space: nowrap; } +@media (max-width: 600px) { .file-picker .fp-toolbar button { flex: 1 1 42%; } } +`; + document.head.append(style); +} +function buildMarkup(root, cfg) { + const controls = cfg.layout.controls || []; + const control = (name, id, label, className = "") => controls.includes(name) ? `` : ""; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(cfg.labels.columns); + const headings = columns.map((column) => `${esc(cfg.labels.columns[column] || column)}`).join(""); + root.innerHTML = ` +
+
+ + + ${control("files", "fp-files-btn", cfg.labels.pickFiles)} + ${control("folder", "fp-folder-btn", cfg.labels.pickFolder, "secondary")} + ${control("clear", "fp-clear-btn", cfg.labels.clear, "quiet")} +
+

+
${headings}
+

+
`; +} +function notify(cfg, state) { + if (typeof cfg.onChange === "function") cfg.onChange([...flattenFiles(state.nodes)]); +} +function removeMetadata(state, node2) { + if (node2.kind === "file") { + state.fileKeys.delete(`${node2.path}\0${node2.file.size}`); + return; + } + node2.children.forEach((child) => removeMetadata(state, child)); +} +function removeNode(state, id) { + const found = findNode(state.nodes, id); + if (!found) return false; + removeMetadata(state, found.node); + found.nodes.splice(found.nodes.indexOf(found.node), 1); + return true; +} +function initFilePicker(config) { + const cfg = normalizeConfig(config); + const mount = resolveMount(cfg.mount); + injectStyles(); + buildMarkup(mount, cfg); + const root = mount.firstElementChild; + const elements = { + fileInputEl: root.querySelector(".fp-file-input"), + folderInputEl: root.querySelector(".fp-folder-input"), + statusEl: root.querySelector(".fp-status"), + tableBodyEl: root.querySelector(".fp-table-body"), + countEl: root.querySelector(".fp-count") + }; + const state = { nodes: [], fileKeys: /* @__PURE__ */ new Set(), busy: false }; + const listeners = []; + const listen = (target, event, handler) => { + target.addEventListener(event, handler); + listeners.push(() => target.removeEventListener(event, handler)); + }; + const redraw = () => render(state, elements, cfg); + const filesChange = onFilesChange(state, cfg, elements); + const folderChange = onFolderChange(state, cfg, elements); + listen(elements.fileInputEl, "change", async () => { + await filesChange(); + notify(cfg, state); + }); + listen(elements.folderInputEl, "change", async () => { + await folderChange(); + notify(cfg, state); + }); + listen(elements.tableBodyEl, "click", (event) => { + const toggle = event.target.closest("[data-toggle]"); + if (toggle) { + const found = findNode(state.nodes, toggle.dataset.toggle); + if (found) found.node.expanded = !found.node.expanded; + redraw(); + return; + } + const remove = event.target.closest("[data-remove]"); + if (remove) { + if (removeNode(state, remove.dataset.remove)) { + redraw(); + notify(cfg, state); + } + } + }); + if (root.querySelector("#fp-files-btn")) listen(root.querySelector("#fp-files-btn"), "click", () => elements.fileInputEl.click()); + if (root.querySelector("#fp-folder-btn")) listen(root.querySelector("#fp-folder-btn"), "click", () => elements.folderInputEl.click()); + if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }); + redraw(); + return { + pickFiles: () => elements.fileInputEl.click(), + pickFolder: () => elements.folderInputEl.click(), + addFiles: async (files) => { + await addFiles(state, cfg, files, elements); + notify(cfg, state); + }, + getFiles: () => [...flattenFiles(state.nodes)], + remove: (id) => { + if (removeNode(state, id)) { + redraw(); + notify(cfg, state); + } + }, + clear: () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }, + render: redraw, + destroy: () => { + listeners.forEach((removeListener) => removeListener()); + state.nodes = []; + state.fileKeys.clear(); + mount.replaceChildren(); + } + }; +} +export { + DEFAULTS, + initFilePicker, + putToVm, + uploadViaVM +}; diff --git a/dist/file-picker.iife.js b/dist/file-picker.iife.js new file mode 100644 index 0000000..0659d05 --- /dev/null +++ b/dist/file-picker.iife.js @@ -0,0 +1,1218 @@ +var FilePicker = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + + // upload/frontend/index.js + var index_exports = {}; + __export(index_exports, { + DEFAULTS: () => DEFAULTS, + initFilePicker: () => initFilePicker, + putToVm: () => putToVm, + uploadViaVM: () => uploadViaVM + }); + + // node_modules/fflate/esm/browser.js + var u8 = Uint8Array; + var u16 = Uint16Array; + var i32 = Int32Array; + var fleb = new u8([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, + /* unused */ + 0, + 0, + /* impossible */ + 0 + ]); + var fdeb = new u8([ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, + /* unused */ + 0, + 0 + ]); + var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); + var freb = function(eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = j - b[i] << 5 | i; + } + } + return { b, r }; + }; + var _a = freb(fleb, 2); + var fl = _a.b; + var revfl = _a.r; + fl[28] = 258, revfl[258] = 28; + var _b = freb(fdeb, 0); + var fd = _b.b; + var revfd = _b.r; + var rev = new u16(32768); + for (i = 0; i < 32768; ++i) { + x = (i & 43690) >> 1 | (i & 21845) << 1; + x = (x & 52428) >> 2 | (x & 13107) << 2; + x = (x & 61680) >> 4 | (x & 3855) << 4; + rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; + } + var x; + var i; + var hMap = (function(cd, mb, r) { + var s = cd.length; + var i = 0; + var l = new u16(mb); + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = le[i - 1] + l[i - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + if (cd[i]) { + var sv = i << 4 | cd[i]; + var r_1 = mb - cd[i]; + var v = le[cd[i] - 1]++ << r_1; + for (var m = v | (1 << r_1) - 1; v <= m; ++v) { + co[rev[v] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i]; + } + } + } + return co; + }); + var flt = new u8(288); + for (i = 0; i < 144; ++i) + flt[i] = 8; + var i; + for (i = 144; i < 256; ++i) + flt[i] = 9; + var i; + for (i = 256; i < 280; ++i) + flt[i] = 7; + var i; + for (i = 280; i < 288; ++i) + flt[i] = 8; + var i; + var fdt = new u8(32); + for (i = 0; i < 32; ++i) + fdt[i] = 5; + var i; + var flrm = /* @__PURE__ */ hMap(flt, 9, 1); + var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1); + var max = function(a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; + }; + var bits = function(d, p, m) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8) >> (p & 7) & m; + }; + var bits16 = function(d, p) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); + }; + var shft = function(p) { + return (p + 7) / 8 | 0; + }; + var slc = function(v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + return new u8(v.subarray(s, e)); + }; + var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + // determined by compression function + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" + // determined by unknown compression method + ]; + var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; + }; + var inflt = function(dat, st, buf, dict) { + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + var resize = noBuf || st.i != 2; + var noSt = st.i; + if (noBuf) + buf = new u8(sl * 3); + var cbuf = function(l2) { + var bl = buf.length; + if (l2 > bl) { + var nbuf = new u8(Math.max(bl * 2, l2)); + nbuf.set(buf); + buf = nbuf; + } + }; + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + var tbts = sl * 8; + do { + if (!lm) { + final = bits(dat, pos, 1); + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + l); + buf.set(dat.subarray(s, t), bt); + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + var ldt = new u8(tl); + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + var clb = max(clt), clbmsk = (1 << clb) - 1; + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl; ) { + var r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + var s = r >> 4; + if (s < 16) { + ldt[i++] = s; + } else { + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } else { + var add = sym - 254; + if (sym > 264) { + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + var d = dm[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1, pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); + }; + var et = /* @__PURE__ */ new u8(0); + var b2 = function(d, b) { + return d[b] | d[b + 1] << 8; + }; + var b4 = function(d, b) { + return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0; + }; + var b8 = function(d, b) { + return b4(d, b) + b4(d, b + 4) * 4294967296; + }; + function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); + } + var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder(); + var tds = 0; + try { + td.decode(et, { stream: true }); + tds = 1; + } catch (e) { + } + var dutf8 = function(d) { + for (var r = "", i = 0; ; ) { + var c = d[i++]; + var eb = (c > 127) + (c > 223) + (c > 239); + if (i + eb > d.length) + return { s: r, r: slc(d, i - 1) }; + if (!eb) + r += String.fromCharCode(c); + else if (eb == 3) { + c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023); + } else if (eb & 1) + r += String.fromCharCode((c & 31) << 6 | d[i++] & 63); + else + r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63); + } + }; + function strFromU8(dat, latin1) { + if (latin1) { + var r = ""; + for (var i = 0; i < dat.length; i += 16384) + r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384)); + return r; + } else if (td) { + return td.decode(dat); + } else { + var _a2 = dutf8(dat), s = _a2.s, r = _a2.r; + if (r.length) + err(8); + return s; + } + } + var slzh = function(d, b) { + return b + 30 + b2(d, b + 26) + b2(d, b + 28); + }; + var zh = function(d, b, z) { + var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl; + var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2]; + return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off]; + }; + var z64hs = function(d, b, l, z, sc, su, off) { + var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l; + var nf = nsc + nsu + noff; + if (z && nf) { + for (; b + 4 < e; b += 4 + b2(d, b + 2)) { + if (b2(d, b) == 1) { + return [ + nsc ? b8(d, b + 4 + 8 * nsu) : sc, + nsu ? b8(d, b + 4) : su, + noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off, + 1 + ]; + } + } + if (z < 2) + err(13); + } + return [sc, su, off, 0]; + }; + function unzipSync(data, opts) { + var files = {}; + var e = data.length - 22; + for (; b4(data, e) != 101010256; --e) { + if (!e || data.length - e > 65558) + err(13); + } + ; + var c = b2(data, e + 8); + if (!c) + return {}; + var o = b4(data, e + 16); + var z = b4(data, e - 20) == 117853008; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 101075792; + if (z) { + c = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + for (var i = 0; i < c; ++i) { + var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off); + o = no; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_2 + })) { + if (!c_2) + files[fn] = slc(data, b, b + sc); + else if (c_2 == 8) + files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) }); + else + err(14, "unknown compression type " + c_2); + } + } + return files; + } + + // upload/frontend/table/rebase_tree.js + function rebaseTree(root, prefix) { + root.path = `${prefix}/${root.path}`; + if (root.file) root.file = new File([root.file], root.file.name, { lastModified: root.file.lastModified }); + root.children.forEach((child) => rebaseTree(child, prefix)); + return root; + } + + // upload/frontend/zip/list_zip_files.js + var DEFAULT_LIMITS = { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + }; + function extensionAllowed(name, allowedExt) { + const lowerName = name.toLowerCase(); + return allowedExt.some((extension) => lowerName.endsWith(extension.toLowerCase())); + } + function makeFile(data, name) { + return new File([data], name.split("/").at(-1)); + } + function safeEntryParts(entryName) { + if (!entryName || entryName.startsWith("/") || entryName.includes("\\")) return null; + const parts = entryName.split("/"); + if (parts.some((part) => !part || part === "." || part === "..")) return null; + return parts; + } + function node(kind, name, path, children = [], file = null) { + return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" }; + } + function addPath(root, parts, fileNode) { + let current = root; + parts.forEach((part, index) => { + const last = index === parts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? fileNode : node("folder", part, `${current.path}/${part}`); + current.children.push(child); + } + current = child; + }); + } + async function listEntries(data, zipName, allowedExt, depth, limits, budget) { + if (depth > limits.maxDepth) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0433\u043B\u0443\u0431\u043E\u043A\u0430\u044F \u0432\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0441\u0442\u044C ZIP"); + const entries = unzipSync(data, { + filter: (entry) => { + budget.entries += 1; + if (budget.entries > limits.maxEntries) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E ZIP entries"); + const lowerName = entry.name.toLowerCase(); + if (!extensionAllowed(lowerName, allowedExt) && !lowerName.endsWith(".zip")) return false; + if (entry.originalSize > limits.maxEntryBytes) return false; + if (budget.totalBytes + entry.originalSize > limits.maxTotalBytes) { + throw new Error("\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u0441\u0443\u043C\u043C\u0430\u0440\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0445 ZIP entries"); + } + budget.totalBytes += entry.originalSize; + return true; + } + }); + const root = node("zip", zipName, zipName); + for (const [entryName, entryData] of Object.entries(entries)) { + if (entryName.endsWith("/")) continue; + const parts = safeEntryParts(entryName); + if (!parts) continue; + const normalizedName = parts.join("/"); + if (normalizedName.toLowerCase().endsWith(".zip")) { + const nested = await listEntries(entryData, entryName, allowedExt, depth + 1, limits, budget); + if (nested) { + rebaseTree(nested, zipName); + nested.name = parts.at(-1); + addPath(root, parts, nested); + } + } else if (extensionAllowed(normalizedName, allowedExt)) { + const path = `${zipName}/${normalizedName}`; + const file = makeFile(entryData, path); + addPath(root, parts, node("file", parts.at(-1), path, [], file)); + } + } + return root.children.length ? root : null; + } + async function listZipFiles(file, allowedExt, customLimits = {}) { + const data = new Uint8Array(await file.arrayBuffer()); + const limits = { ...DEFAULT_LIMITS, ...customLimits }; + return listEntries(data, file.name, allowedExt, 0, limits, { entries: 0, totalBytes: 0 }); + } + + // upload/frontend/table/add_file_with_dedup.js + function addFileWithDedup(state, fileNode) { + const accept = (node2) => { + if (node2.kind === "file") { + const key = `${node2.path}\0${node2.file.size}`; + if (state.fileKeys.has(key)) return null; + state.fileKeys.add(key); + return node2; + } + node2.children = node2.children.map(accept).filter(Boolean); + return node2.children.length ? node2 : null; + }; + const accepted = accept(fileNode); + if (!accepted || accepted.kind !== "file" && !accepted.children.length) return false; + if (accepted.kind !== "file") { + const existing = state.nodes.find((node2) => node2.kind !== "file" && node2.path === accepted.path); + if (existing) { + mergeChildren(existing, accepted); + return true; + } + } + state.nodes.push(accepted); + return true; + } + function mergeChildren(target, incoming) { + incoming.children.forEach((child) => { + if (child.kind === "file") { + const duplicate = target.children.some((existing2) => existing2.kind === "file" && existing2.path === child.path && existing2.file.size === child.file.size); + if (!duplicate) target.children.push(child); + return; + } + const existing = target.children.find((candidate) => candidate.kind !== "file" && candidate.path === child.path); + if (existing) mergeChildren(existing, child); + else target.children.push(child); + }); + } + + // upload/frontend/table/esc.js + function esc(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); + } + + // upload/frontend/table/fs.js + function fs(bytes) { + return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`; + } + + // upload/frontend/table/render.js + var DEFAULT_RENDER_CONFIG = { + labels: { + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${fs(bytes)}` + }, + layout: { columns: ["path", "size", "status"] } + }; + function renderConfig(cfg = {}) { + return { + labels: { + ...DEFAULT_RENDER_CONFIG.labels, + ...cfg.labels || {}, + columns: { + ...DEFAULT_RENDER_CONFIG.labels.columns, + ...cfg.labels?.columns || {} + } + }, + layout: { ...DEFAULT_RENDER_CONFIG.layout, ...cfg.layout || {} } + }; + } + function renderNode(node2, depth, cfg) { + const isGroup = node2.kind !== "file"; + const padding = depth * 4; + const name = esc(node2.name); + const action = isGroup ? `` : `${name}`; + const remove = ``; + const pathAttribute = node2.kind === "file" ? ` data-path="${esc(node2.path)}"` : ""; + const cells = { + path: action, + size: node2.kind === "file" ? fs(node2.file.size) : "", + status: node2.error ? esc(node2.error) : node2.kind === "file" ? esc(cfg.labels.statusReady) : "" + }; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + const row = `` + columns.map((column) => `${cells[column] || ""}`).join("") + `${remove}`; + if (!isGroup || !node2.expanded) return row; + return row + node2.children.map((child) => renderNode(child, depth + 1, cfg)).join(""); + } + function render(state, elements, cfg) { + const normalized = renderConfig(cfg); + const columns = normalized.layout.columns?.length ? normalized.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node2) => renderNode(node2, 0, normalized)).join("") : `${esc(normalized.labels.empty)}`; + const files = []; + const visit = (node2) => { + if (node2.kind === "file") files.push(node2); + else node2.children.forEach(visit); + }; + state.nodes.forEach(visit); + const totalBytes = files.reduce((sum, node2) => sum + node2.file.size, 0); + elements.countEl.textContent = normalized.labels.count(files.length, totalBytes); + } + function findNode(nodes, id) { + for (const node2 of nodes) { + if (node2.id === id) return { node: node2, nodes }; + const found = findNode(node2.children || [], id); + if (found) return found; + } + return null; + } + function flattenFiles(nodes, result = []) { + nodes.forEach((node2) => { + if (node2.kind === "file") result.push({ path: node2.path, name: node2.name, size: node2.file.size, file: node2.file }); + else flattenFiles(node2.children, result); + }); + return result; + } + + // upload/frontend/table/on_files_change.js + async function addFiles(state, cfg, files, elements) { + for (const file of Array.from(files)) { + if (!file.name.toLowerCase().endsWith(".zip")) { + if (!cfg.allowedExt.some((extension) => file.name.toLowerCase().endsWith(extension.toLowerCase()))) { + continue; + } + addFileWithDedup(state, { + id: crypto.randomUUID(), + kind: "file", + name: file.name, + path: file.name, + file, + children: [], + expanded: true + }); + continue; + } + try { + const zipTree = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zipTree) addFileWithDedup(state, zipTree); + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } + render(state, elements, cfg); + } + function onFilesChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + await addFiles(state, cfg, elements.fileInputEl.files, elements); + } finally { + elements.fileInputEl.value = ""; + state.busy = false; + } + }; + } + + // upload/frontend/table/on_folder_change.js + function onFolderChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + const roots = /* @__PURE__ */ new Map(); + for (const file of Array.from(elements.folderInputEl.files)) { + const parts = (file.webkitRelativePath || file.name).split("/"); + const relativePath = parts.slice(1).join("/") || file.name; + const lowerPath = relativePath.toLowerCase(); + const rootName = parts[0] || file.name; + if (!roots.has(rootName)) { + roots.set(rootName, { + id: crypto.randomUUID(), + kind: "folder", + name: rootName, + path: rootName, + children: [], + expanded: false + }); + } + const root = roots.get(rootName); + const addToFolder = (node2) => { + let current = root; + const nodeParts = node2.path.split("/").slice(1); + nodeParts.forEach((part, index) => { + const last = index === nodeParts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? node2 : { + id: crypto.randomUUID(), + kind: "folder", + name: part, + path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`, + children: [], + expanded: false + }; + current.children.push(child); + } + current = child; + }); + }; + if (lowerPath.endsWith(".zip")) { + try { + const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zip) { + rebaseTree(zip, rootName); + addToFolder(zip); + } + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) { + addToFolder({ + id: crypto.randomUUID(), + kind: "file", + name: parts.at(-1), + path: `${rootName}/${relativePath}`, + file, + children: [], + expanded: true + }); + } + } + roots.forEach((root) => addFileWithDedup(state, root)); + elements.folderInputEl.value = ""; + render(state, elements, cfg); + } finally { + state.busy = false; + } + }; + } + + // upload/frontend/upload/put_to_vm.js + function putToVm(file, url, options = {}) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + if (options.onXHR) { + options.onXHR(xhr); + } + const abortHandler = () => { + xhr.abort(); + reject(new DOMException("Upload aborted", "AbortError")); + }; + if (options.signal) { + if (options.signal.aborted) { + reject(new DOMException("Upload aborted", "AbortError")); + return; + } + options.signal.addEventListener("abort", abortHandler, { once: true }); + } + xhr.open("PUT", url); + xhr.timeout = options.timeoutMs || 3e5; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable && options.onProgress) { + const pct = Math.round(e.loaded / e.total * 100); + options.onProgress(pct, e.loaded, e.total); + } + }; + xhr.onload = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`\u0411\u0443\u0444\u0435\u0440: HTTP ${xhr.status} ${xhr.statusText}`)); + } + }; + xhr.onerror = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error("\u0421\u0435\u0442\u0435\u0432\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435 \u0432 \u0431\u0443\u0444\u0435\u0440")); + }; + xhr.ontimeout = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error(`\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u0431\u0443\u0444\u0435\u0440\u0430 (${Math.round(xhr.timeout / 1e3)}\u0441)`)); + }; + xhr.onabort = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new DOMException("Upload aborted", "AbortError")); + }; + xhr.send(file); + }); + } + + // upload/frontend/upload/upload_via_vm.js + function generateUuid() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === "x" ? r : r & 3 | 8; + return v.toString(16); + }); + } + function normalizeFileEntry(item) { + if (item instanceof File || item instanceof Blob) { + return { name: item.name || "unnamed", size: item.size, file: item, path: item.name || "" }; + } + if (item && item.file) { + return { + name: item.name || item.file.name || "unnamed", + size: item.size ?? item.file.size ?? 0, + file: item.file, + path: item.path || item.name || "" + }; + } + throw new Error("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0431\u044A\u0435\u043A\u0442 \u0444\u0430\u0439\u043B\u0430"); + } + async function uploadViaVM(files, options = {}) { + if (!options.vmUploadUrl) { + return { ok: false, error: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 options.vmUploadUrl \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D" }; + } + const normalizedFiles = Array.from(files).map(normalizeFileEntry); + const total = normalizedFiles.length; + if (total === 0) { + return { ok: true, session: options.session || "", count: 0 }; + } + const backendUrl = options.backendUploadUrl || (options.apiBase || "") + "/api/upload_refs"; + const vmBase = options.vmUploadUrl.endsWith("/") ? options.vmUploadUrl : `${options.vmUploadUrl}/`; + let currentSession = options.session || ""; + let totalAdded = 0; + for (let k = 0; k < total; k++) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + const { name, size, file } = normalizedFiles[k]; + const fileKey = `${generateUuid()}_${k}`; + const fileUrl = `${vmBase}${fileKey}`; + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0432 \u0431\u0443\u0444\u0435\u0440 (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u23F3 \u0412 \u0431\u0443\u0444\u0435\u0440..."); + } + const t0 = performance.now(); + try { + await putToVm(file, fileUrl, { + signal: options.signal, + onXHR: options.onXHR, + onProgress(pct, loaded, fileTotal) { + const elapsed = (performance.now() - t0) / 1e3; + const speed = elapsed > 0 ? loaded / elapsed : 0; + if (options.onFileStatus) { + options.onFileStatus(k, `\u23F3 ${pct}% (${fs(speed)}/s)`); + } + if (options.onProgress) { + options.onProgress({ index: k, total, pct, speed, loaded, fileTotal, name }); + } + } + }); + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u0431\u0443\u0444\u0435\u0440\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u0430\u0439\u043B\u0430 ${name} \u0432 \u0431\u0443\u0444\u0435\u0440: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + if (options.onStatus) { + options.onStatus(`\u041F\u0440\u0438\u0451\u043C \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u043C (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u26A1 \u0412 \u0441\u0435\u0441\u0441\u0438\u044E..."); + } + try { + const resp = await fetch(backendUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session: currentSession, + files: [{ name, size, url: fileUrl }] + }), + signal: options.signal + }); + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} ${resp.statusText}`); + } + const data = await resp.json(); + if (!data.ok) { + throw new Error(data.error || "\u0411\u044D\u043A\u0435\u043D\u0434 \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443"); + } + currentSession = data.session || currentSession; + totalAdded = data.count ?? totalAdded + 1; + if (options.onFileStatus) { + options.onFileStatus(k, "\u2713 \u0414\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D"); + } + if (options.onFileComplete) { + options.onFileComplete({ + fileIndex: k, + name, + size, + session: currentSession, + totalAdded + }); + } + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430 \u0444\u0430\u0439\u043B\u0430 ${name} \u0431\u044D\u043A\u0435\u043D\u0434\u043E\u043C: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + } + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E. \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E \u0444\u0430\u0439\u043B\u043E\u0432: ${total}`); + } + return { ok: true, session: currentSession, count: totalAdded }; + } + + // upload/frontend/index.js + var DEFAULTS = { + allowedExt: [], + labels: { + pickFiles: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B", + pickFolder: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443", + clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${bytes} B` + }, + layout: { columns: ["path", "size", "status"], controls: ["files", "folder", "clear"] }, + limits: { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + } + }; + function resolveMount(mount) { + const element = typeof mount === "string" ? document.querySelector(mount) : mount; + if (!element) throw new Error("File picker mount element \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"); + return element; + } + function normalizeConfig(config) { + return { + ...config, + allowedExt: config.allowedExt || DEFAULTS.allowedExt, + labels: { + ...DEFAULTS.labels, + ...config.labels || {}, + columns: { ...DEFAULTS.labels.columns, ...config.labels?.columns || {} } + }, + layout: { ...DEFAULTS.layout, ...config.layout || {} }, + limits: { ...DEFAULTS.limits, ...config.limits || {} } + }; + } + function injectStyles() { + if (document.querySelector("style[data-file-picker]")) return; + const style = document.createElement("style"); + style.dataset.filePicker = ""; + style.textContent = ` +.file-picker { color: #17211b; font-family: Georgia, 'Times New Roman', serif; } +.file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; } +.file-picker .fp-toolbar button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; } +.file-picker .fp-toolbar button.secondary { color: #1f5a3b; background: #cbdcc9; } +.file-picker .fp-toolbar button.quiet { color: #5d6a61; background: transparent; } +.file-picker .fp-table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; } +.file-picker .fp-table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; } +.file-picker .fp-table th, .file-picker .fp-table td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; } +.file-picker .fp-table th { color: #56745f; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; } +.file-picker .fp-status, .file-picker .fp-count { color: #5d6a61; font: 13px sans-serif; } +.file-picker .fp-count { min-height: 22px; } +.file-picker .tree-name { display: inline-flex; align-items: center; gap: 8px; min-width: 240px; padding-top: 0; padding-bottom: 0; border: 0; background: transparent; color: #17211b; font: inherit; text-align: left; } +.file-picker .tree-group { padding-left: 0; cursor: pointer; } +.file-picker .tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; } +.file-picker .remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; } +.file-picker .tree-row td:first-child { white-space: nowrap; } +@media (max-width: 600px) { .file-picker .fp-toolbar button { flex: 1 1 42%; } } +`; + document.head.append(style); + } + function buildMarkup(root, cfg) { + const controls = cfg.layout.controls || []; + const control = (name, id, label, className = "") => controls.includes(name) ? `` : ""; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(cfg.labels.columns); + const headings = columns.map((column) => `${esc(cfg.labels.columns[column] || column)}`).join(""); + root.innerHTML = ` +
+
+ + + ${control("files", "fp-files-btn", cfg.labels.pickFiles)} + ${control("folder", "fp-folder-btn", cfg.labels.pickFolder, "secondary")} + ${control("clear", "fp-clear-btn", cfg.labels.clear, "quiet")} +
+

+
${headings}
+

+
`; + } + function notify(cfg, state) { + if (typeof cfg.onChange === "function") cfg.onChange([...flattenFiles(state.nodes)]); + } + function removeMetadata(state, node2) { + if (node2.kind === "file") { + state.fileKeys.delete(`${node2.path}\0${node2.file.size}`); + return; + } + node2.children.forEach((child) => removeMetadata(state, child)); + } + function removeNode(state, id) { + const found = findNode(state.nodes, id); + if (!found) return false; + removeMetadata(state, found.node); + found.nodes.splice(found.nodes.indexOf(found.node), 1); + return true; + } + function initFilePicker(config) { + const cfg = normalizeConfig(config); + const mount = resolveMount(cfg.mount); + injectStyles(); + buildMarkup(mount, cfg); + const root = mount.firstElementChild; + const elements = { + fileInputEl: root.querySelector(".fp-file-input"), + folderInputEl: root.querySelector(".fp-folder-input"), + statusEl: root.querySelector(".fp-status"), + tableBodyEl: root.querySelector(".fp-table-body"), + countEl: root.querySelector(".fp-count") + }; + const state = { nodes: [], fileKeys: /* @__PURE__ */ new Set(), busy: false }; + const listeners = []; + const listen = (target, event, handler) => { + target.addEventListener(event, handler); + listeners.push(() => target.removeEventListener(event, handler)); + }; + const redraw = () => render(state, elements, cfg); + const filesChange = onFilesChange(state, cfg, elements); + const folderChange = onFolderChange(state, cfg, elements); + listen(elements.fileInputEl, "change", async () => { + await filesChange(); + notify(cfg, state); + }); + listen(elements.folderInputEl, "change", async () => { + await folderChange(); + notify(cfg, state); + }); + listen(elements.tableBodyEl, "click", (event) => { + const toggle = event.target.closest("[data-toggle]"); + if (toggle) { + const found = findNode(state.nodes, toggle.dataset.toggle); + if (found) found.node.expanded = !found.node.expanded; + redraw(); + return; + } + const remove = event.target.closest("[data-remove]"); + if (remove) { + if (removeNode(state, remove.dataset.remove)) { + redraw(); + notify(cfg, state); + } + } + }); + if (root.querySelector("#fp-files-btn")) listen(root.querySelector("#fp-files-btn"), "click", () => elements.fileInputEl.click()); + if (root.querySelector("#fp-folder-btn")) listen(root.querySelector("#fp-folder-btn"), "click", () => elements.folderInputEl.click()); + if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }); + redraw(); + return { + pickFiles: () => elements.fileInputEl.click(), + pickFolder: () => elements.folderInputEl.click(), + addFiles: async (files) => { + await addFiles(state, cfg, files, elements); + notify(cfg, state); + }, + getFiles: () => [...flattenFiles(state.nodes)], + remove: (id) => { + if (removeNode(state, id)) { + redraw(); + notify(cfg, state); + } + }, + clear: () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }, + render: redraw, + destroy: () => { + listeners.forEach((removeListener) => removeListener()); + state.nodes = []; + state.fileKeys.clear(); + mount.replaceChildren(); + } + }; + } + return __toCommonJS(index_exports); +})(); diff --git a/site/app.py b/site/app.py index 0f11763..8b99fc2 100644 --- a/site/app.py +++ b/site/app.py @@ -21,7 +21,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.77" +VERSION = "0.0.78" def setup_logging(): diff --git a/site/routes/main_bp.py b/site/routes/main_bp.py index 8cbde3a..a8e01f2 100644 --- a/site/routes/main_bp.py +++ b/site/routes/main_bp.py @@ -13,9 +13,9 @@ from flask import Blueprint, render_template, current_app, send_from_directory # ═══════════════════════════════════════════════════════════════════════════ # Корень модуля upload/frontend — для раздачи ES-модулей браузеру -_UPLOAD_FRONTEND_DIR = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), - "upload", "frontend") +_ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +_UPLOAD_FRONTEND_DIR = os.path.join(_ROOT_DIR, "upload", "frontend") +_DIST_DIR = os.path.join(_ROOT_DIR, "dist") main_bp = Blueprint("main", __name__) @@ -34,3 +34,15 @@ def index(): def upload_frontend(filename): """Раздаёт ES-модули переиспользуемого слоя загрузки (upload/frontend).""" return send_from_directory(_UPLOAD_FRONTEND_DIR, filename) + + +@main_bp.route("/file-picker/") +def file_picker_dist(filename): + """Отдаёт собранный бандл из каталога dist.""" + return send_from_directory(_DIST_DIR, filename) + + +@main_bp.route("/dist/") +def dist_files(filename): + """Отдаёт файлы из каталога dist.""" + return send_from_directory(_DIST_DIR, filename) diff --git a/site/static/dist/file-picker.esm.js b/site/static/dist/file-picker.esm.js new file mode 100644 index 0000000..d9f7ac1 --- /dev/null +++ b/site/static/dist/file-picker.esm.js @@ -0,0 +1,1194 @@ +// node_modules/fflate/esm/browser.js +var u8 = Uint8Array; +var u16 = Uint16Array; +var i32 = Int32Array; +var fleb = new u8([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, + /* unused */ + 0, + 0, + /* impossible */ + 0 +]); +var fdeb = new u8([ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, + /* unused */ + 0, + 0 +]); +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +var freb = function(eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = j - b[i] << 5 | i; + } + } + return { b, r }; +}; +var _a = freb(fleb, 2); +var fl = _a.b; +var revfl = _a.r; +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0); +var fd = _b.b; +var revfd = _b.r; +var rev = new u16(32768); +for (i = 0; i < 32768; ++i) { + x = (i & 43690) >> 1 | (i & 21845) << 1; + x = (x & 52428) >> 2 | (x & 13107) << 2; + x = (x & 61680) >> 4 | (x & 3855) << 4; + rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; +} +var x; +var i; +var hMap = (function(cd, mb, r) { + var s = cd.length; + var i = 0; + var l = new u16(mb); + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = le[i - 1] + l[i - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + if (cd[i]) { + var sv = i << 4 | cd[i]; + var r_1 = mb - cd[i]; + var v = le[cd[i] - 1]++ << r_1; + for (var m = v | (1 << r_1) - 1; v <= m; ++v) { + co[rev[v] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i]; + } + } + } + return co; +}); +var flt = new u8(288); +for (i = 0; i < 144; ++i) + flt[i] = 8; +var i; +for (i = 144; i < 256; ++i) + flt[i] = 9; +var i; +for (i = 256; i < 280; ++i) + flt[i] = 7; +var i; +for (i = 280; i < 288; ++i) + flt[i] = 8; +var i; +var fdt = new u8(32); +for (i = 0; i < 32; ++i) + fdt[i] = 5; +var i; +var flrm = /* @__PURE__ */ hMap(flt, 9, 1); +var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1); +var max = function(a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; +}; +var bits = function(d, p, m) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8) >> (p & 7) & m; +}; +var bits16 = function(d, p) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); +}; +var shft = function(p) { + return (p + 7) / 8 | 0; +}; +var slc = function(v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + return new u8(v.subarray(s, e)); +}; +var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + // determined by compression function + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" + // determined by unknown compression method +]; +var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; +}; +var inflt = function(dat, st, buf, dict) { + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + var resize = noBuf || st.i != 2; + var noSt = st.i; + if (noBuf) + buf = new u8(sl * 3); + var cbuf = function(l2) { + var bl = buf.length; + if (l2 > bl) { + var nbuf = new u8(Math.max(bl * 2, l2)); + nbuf.set(buf); + buf = nbuf; + } + }; + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + var tbts = sl * 8; + do { + if (!lm) { + final = bits(dat, pos, 1); + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + l); + buf.set(dat.subarray(s, t), bt); + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + var ldt = new u8(tl); + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + var clb = max(clt), clbmsk = (1 << clb) - 1; + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl; ) { + var r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + var s = r >> 4; + if (s < 16) { + ldt[i++] = s; + } else { + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } else { + var add = sym - 254; + if (sym > 264) { + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + var d = dm[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1, pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); +}; +var et = /* @__PURE__ */ new u8(0); +var b2 = function(d, b) { + return d[b] | d[b + 1] << 8; +}; +var b4 = function(d, b) { + return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0; +}; +var b8 = function(d, b) { + return b4(d, b) + b4(d, b + 4) * 4294967296; +}; +function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder(); +var tds = 0; +try { + td.decode(et, { stream: true }); + tds = 1; +} catch (e) { +} +var dutf8 = function(d) { + for (var r = "", i = 0; ; ) { + var c = d[i++]; + var eb = (c > 127) + (c > 223) + (c > 239); + if (i + eb > d.length) + return { s: r, r: slc(d, i - 1) }; + if (!eb) + r += String.fromCharCode(c); + else if (eb == 3) { + c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023); + } else if (eb & 1) + r += String.fromCharCode((c & 31) << 6 | d[i++] & 63); + else + r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63); + } +}; +function strFromU8(dat, latin1) { + if (latin1) { + var r = ""; + for (var i = 0; i < dat.length; i += 16384) + r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384)); + return r; + } else if (td) { + return td.decode(dat); + } else { + var _a2 = dutf8(dat), s = _a2.s, r = _a2.r; + if (r.length) + err(8); + return s; + } +} +var slzh = function(d, b) { + return b + 30 + b2(d, b + 26) + b2(d, b + 28); +}; +var zh = function(d, b, z) { + var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl; + var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2]; + return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off]; +}; +var z64hs = function(d, b, l, z, sc, su, off) { + var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l; + var nf = nsc + nsu + noff; + if (z && nf) { + for (; b + 4 < e; b += 4 + b2(d, b + 2)) { + if (b2(d, b) == 1) { + return [ + nsc ? b8(d, b + 4 + 8 * nsu) : sc, + nsu ? b8(d, b + 4) : su, + noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off, + 1 + ]; + } + } + if (z < 2) + err(13); + } + return [sc, su, off, 0]; +}; +function unzipSync(data, opts) { + var files = {}; + var e = data.length - 22; + for (; b4(data, e) != 101010256; --e) { + if (!e || data.length - e > 65558) + err(13); + } + ; + var c = b2(data, e + 8); + if (!c) + return {}; + var o = b4(data, e + 16); + var z = b4(data, e - 20) == 117853008; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 101075792; + if (z) { + c = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + for (var i = 0; i < c; ++i) { + var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off); + o = no; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_2 + })) { + if (!c_2) + files[fn] = slc(data, b, b + sc); + else if (c_2 == 8) + files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) }); + else + err(14, "unknown compression type " + c_2); + } + } + return files; +} + +// upload/frontend/table/rebase_tree.js +function rebaseTree(root, prefix) { + root.path = `${prefix}/${root.path}`; + if (root.file) root.file = new File([root.file], root.file.name, { lastModified: root.file.lastModified }); + root.children.forEach((child) => rebaseTree(child, prefix)); + return root; +} + +// upload/frontend/zip/list_zip_files.js +var DEFAULT_LIMITS = { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 +}; +function extensionAllowed(name, allowedExt) { + const lowerName = name.toLowerCase(); + return allowedExt.some((extension) => lowerName.endsWith(extension.toLowerCase())); +} +function makeFile(data, name) { + return new File([data], name.split("/").at(-1)); +} +function safeEntryParts(entryName) { + if (!entryName || entryName.startsWith("/") || entryName.includes("\\")) return null; + const parts = entryName.split("/"); + if (parts.some((part) => !part || part === "." || part === "..")) return null; + return parts; +} +function node(kind, name, path, children = [], file = null) { + return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" }; +} +function addPath(root, parts, fileNode) { + let current = root; + parts.forEach((part, index) => { + const last = index === parts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? fileNode : node("folder", part, `${current.path}/${part}`); + current.children.push(child); + } + current = child; + }); +} +async function listEntries(data, zipName, allowedExt, depth, limits, budget) { + if (depth > limits.maxDepth) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0433\u043B\u0443\u0431\u043E\u043A\u0430\u044F \u0432\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0441\u0442\u044C ZIP"); + const entries = unzipSync(data, { + filter: (entry) => { + budget.entries += 1; + if (budget.entries > limits.maxEntries) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E ZIP entries"); + const lowerName = entry.name.toLowerCase(); + if (!extensionAllowed(lowerName, allowedExt) && !lowerName.endsWith(".zip")) return false; + if (entry.originalSize > limits.maxEntryBytes) return false; + if (budget.totalBytes + entry.originalSize > limits.maxTotalBytes) { + throw new Error("\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u0441\u0443\u043C\u043C\u0430\u0440\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0445 ZIP entries"); + } + budget.totalBytes += entry.originalSize; + return true; + } + }); + const root = node("zip", zipName, zipName); + for (const [entryName, entryData] of Object.entries(entries)) { + if (entryName.endsWith("/")) continue; + const parts = safeEntryParts(entryName); + if (!parts) continue; + const normalizedName = parts.join("/"); + if (normalizedName.toLowerCase().endsWith(".zip")) { + const nested = await listEntries(entryData, entryName, allowedExt, depth + 1, limits, budget); + if (nested) { + rebaseTree(nested, zipName); + nested.name = parts.at(-1); + addPath(root, parts, nested); + } + } else if (extensionAllowed(normalizedName, allowedExt)) { + const path = `${zipName}/${normalizedName}`; + const file = makeFile(entryData, path); + addPath(root, parts, node("file", parts.at(-1), path, [], file)); + } + } + return root.children.length ? root : null; +} +async function listZipFiles(file, allowedExt, customLimits = {}) { + const data = new Uint8Array(await file.arrayBuffer()); + const limits = { ...DEFAULT_LIMITS, ...customLimits }; + return listEntries(data, file.name, allowedExt, 0, limits, { entries: 0, totalBytes: 0 }); +} + +// upload/frontend/table/add_file_with_dedup.js +function addFileWithDedup(state, fileNode) { + const accept = (node2) => { + if (node2.kind === "file") { + const key = `${node2.path}\0${node2.file.size}`; + if (state.fileKeys.has(key)) return null; + state.fileKeys.add(key); + return node2; + } + node2.children = node2.children.map(accept).filter(Boolean); + return node2.children.length ? node2 : null; + }; + const accepted = accept(fileNode); + if (!accepted || accepted.kind !== "file" && !accepted.children.length) return false; + if (accepted.kind !== "file") { + const existing = state.nodes.find((node2) => node2.kind !== "file" && node2.path === accepted.path); + if (existing) { + mergeChildren(existing, accepted); + return true; + } + } + state.nodes.push(accepted); + return true; +} +function mergeChildren(target, incoming) { + incoming.children.forEach((child) => { + if (child.kind === "file") { + const duplicate = target.children.some((existing2) => existing2.kind === "file" && existing2.path === child.path && existing2.file.size === child.file.size); + if (!duplicate) target.children.push(child); + return; + } + const existing = target.children.find((candidate) => candidate.kind !== "file" && candidate.path === child.path); + if (existing) mergeChildren(existing, child); + else target.children.push(child); + }); +} + +// upload/frontend/table/esc.js +function esc(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); +} + +// upload/frontend/table/fs.js +function fs(bytes) { + return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`; +} + +// upload/frontend/table/render.js +var DEFAULT_RENDER_CONFIG = { + labels: { + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${fs(bytes)}` + }, + layout: { columns: ["path", "size", "status"] } +}; +function renderConfig(cfg = {}) { + return { + labels: { + ...DEFAULT_RENDER_CONFIG.labels, + ...cfg.labels || {}, + columns: { + ...DEFAULT_RENDER_CONFIG.labels.columns, + ...cfg.labels?.columns || {} + } + }, + layout: { ...DEFAULT_RENDER_CONFIG.layout, ...cfg.layout || {} } + }; +} +function renderNode(node2, depth, cfg) { + const isGroup = node2.kind !== "file"; + const padding = depth * 4; + const name = esc(node2.name); + const action = isGroup ? `` : `${name}`; + const remove = ``; + const pathAttribute = node2.kind === "file" ? ` data-path="${esc(node2.path)}"` : ""; + const cells = { + path: action, + size: node2.kind === "file" ? fs(node2.file.size) : "", + status: node2.error ? esc(node2.error) : node2.kind === "file" ? esc(cfg.labels.statusReady) : "" + }; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + const row = `` + columns.map((column) => `${cells[column] || ""}`).join("") + `${remove}`; + if (!isGroup || !node2.expanded) return row; + return row + node2.children.map((child) => renderNode(child, depth + 1, cfg)).join(""); +} +function render(state, elements, cfg) { + const normalized = renderConfig(cfg); + const columns = normalized.layout.columns?.length ? normalized.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node2) => renderNode(node2, 0, normalized)).join("") : `${esc(normalized.labels.empty)}`; + const files = []; + const visit = (node2) => { + if (node2.kind === "file") files.push(node2); + else node2.children.forEach(visit); + }; + state.nodes.forEach(visit); + const totalBytes = files.reduce((sum, node2) => sum + node2.file.size, 0); + elements.countEl.textContent = normalized.labels.count(files.length, totalBytes); +} +function findNode(nodes, id) { + for (const node2 of nodes) { + if (node2.id === id) return { node: node2, nodes }; + const found = findNode(node2.children || [], id); + if (found) return found; + } + return null; +} +function flattenFiles(nodes, result = []) { + nodes.forEach((node2) => { + if (node2.kind === "file") result.push({ path: node2.path, name: node2.name, size: node2.file.size, file: node2.file }); + else flattenFiles(node2.children, result); + }); + return result; +} + +// upload/frontend/table/on_files_change.js +async function addFiles(state, cfg, files, elements) { + for (const file of Array.from(files)) { + if (!file.name.toLowerCase().endsWith(".zip")) { + if (!cfg.allowedExt.some((extension) => file.name.toLowerCase().endsWith(extension.toLowerCase()))) { + continue; + } + addFileWithDedup(state, { + id: crypto.randomUUID(), + kind: "file", + name: file.name, + path: file.name, + file, + children: [], + expanded: true + }); + continue; + } + try { + const zipTree = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zipTree) addFileWithDedup(state, zipTree); + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } + render(state, elements, cfg); +} +function onFilesChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + await addFiles(state, cfg, elements.fileInputEl.files, elements); + } finally { + elements.fileInputEl.value = ""; + state.busy = false; + } + }; +} + +// upload/frontend/table/on_folder_change.js +function onFolderChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + const roots = /* @__PURE__ */ new Map(); + for (const file of Array.from(elements.folderInputEl.files)) { + const parts = (file.webkitRelativePath || file.name).split("/"); + const relativePath = parts.slice(1).join("/") || file.name; + const lowerPath = relativePath.toLowerCase(); + const rootName = parts[0] || file.name; + if (!roots.has(rootName)) { + roots.set(rootName, { + id: crypto.randomUUID(), + kind: "folder", + name: rootName, + path: rootName, + children: [], + expanded: false + }); + } + const root = roots.get(rootName); + const addToFolder = (node2) => { + let current = root; + const nodeParts = node2.path.split("/").slice(1); + nodeParts.forEach((part, index) => { + const last = index === nodeParts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? node2 : { + id: crypto.randomUUID(), + kind: "folder", + name: part, + path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`, + children: [], + expanded: false + }; + current.children.push(child); + } + current = child; + }); + }; + if (lowerPath.endsWith(".zip")) { + try { + const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zip) { + rebaseTree(zip, rootName); + addToFolder(zip); + } + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) { + addToFolder({ + id: crypto.randomUUID(), + kind: "file", + name: parts.at(-1), + path: `${rootName}/${relativePath}`, + file, + children: [], + expanded: true + }); + } + } + roots.forEach((root) => addFileWithDedup(state, root)); + elements.folderInputEl.value = ""; + render(state, elements, cfg); + } finally { + state.busy = false; + } + }; +} + +// upload/frontend/upload/put_to_vm.js +function putToVm(file, url, options = {}) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + if (options.onXHR) { + options.onXHR(xhr); + } + const abortHandler = () => { + xhr.abort(); + reject(new DOMException("Upload aborted", "AbortError")); + }; + if (options.signal) { + if (options.signal.aborted) { + reject(new DOMException("Upload aborted", "AbortError")); + return; + } + options.signal.addEventListener("abort", abortHandler, { once: true }); + } + xhr.open("PUT", url); + xhr.timeout = options.timeoutMs || 3e5; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable && options.onProgress) { + const pct = Math.round(e.loaded / e.total * 100); + options.onProgress(pct, e.loaded, e.total); + } + }; + xhr.onload = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`\u0411\u0443\u0444\u0435\u0440: HTTP ${xhr.status} ${xhr.statusText}`)); + } + }; + xhr.onerror = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error("\u0421\u0435\u0442\u0435\u0432\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435 \u0432 \u0431\u0443\u0444\u0435\u0440")); + }; + xhr.ontimeout = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error(`\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u0431\u0443\u0444\u0435\u0440\u0430 (${Math.round(xhr.timeout / 1e3)}\u0441)`)); + }; + xhr.onabort = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new DOMException("Upload aborted", "AbortError")); + }; + xhr.send(file); + }); +} + +// upload/frontend/upload/upload_via_vm.js +function generateUuid() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === "x" ? r : r & 3 | 8; + return v.toString(16); + }); +} +function normalizeFileEntry(item) { + if (item instanceof File || item instanceof Blob) { + return { name: item.name || "unnamed", size: item.size, file: item, path: item.name || "" }; + } + if (item && item.file) { + return { + name: item.name || item.file.name || "unnamed", + size: item.size ?? item.file.size ?? 0, + file: item.file, + path: item.path || item.name || "" + }; + } + throw new Error("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0431\u044A\u0435\u043A\u0442 \u0444\u0430\u0439\u043B\u0430"); +} +async function uploadViaVM(files, options = {}) { + if (!options.vmUploadUrl) { + return { ok: false, error: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 options.vmUploadUrl \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D" }; + } + const normalizedFiles = Array.from(files).map(normalizeFileEntry); + const total = normalizedFiles.length; + if (total === 0) { + return { ok: true, session: options.session || "", count: 0 }; + } + const backendUrl = options.backendUploadUrl || (options.apiBase || "") + "/api/upload_refs"; + const vmBase = options.vmUploadUrl.endsWith("/") ? options.vmUploadUrl : `${options.vmUploadUrl}/`; + let currentSession = options.session || ""; + let totalAdded = 0; + for (let k = 0; k < total; k++) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + const { name, size, file } = normalizedFiles[k]; + const fileKey = `${generateUuid()}_${k}`; + const fileUrl = `${vmBase}${fileKey}`; + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0432 \u0431\u0443\u0444\u0435\u0440 (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u23F3 \u0412 \u0431\u0443\u0444\u0435\u0440..."); + } + const t0 = performance.now(); + try { + await putToVm(file, fileUrl, { + signal: options.signal, + onXHR: options.onXHR, + onProgress(pct, loaded, fileTotal) { + const elapsed = (performance.now() - t0) / 1e3; + const speed = elapsed > 0 ? loaded / elapsed : 0; + if (options.onFileStatus) { + options.onFileStatus(k, `\u23F3 ${pct}% (${fs(speed)}/s)`); + } + if (options.onProgress) { + options.onProgress({ index: k, total, pct, speed, loaded, fileTotal, name }); + } + } + }); + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u0431\u0443\u0444\u0435\u0440\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u0430\u0439\u043B\u0430 ${name} \u0432 \u0431\u0443\u0444\u0435\u0440: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + if (options.onStatus) { + options.onStatus(`\u041F\u0440\u0438\u0451\u043C \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u043C (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u26A1 \u0412 \u0441\u0435\u0441\u0441\u0438\u044E..."); + } + try { + const resp = await fetch(backendUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session: currentSession, + files: [{ name, size, url: fileUrl }] + }), + signal: options.signal + }); + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} ${resp.statusText}`); + } + const data = await resp.json(); + if (!data.ok) { + throw new Error(data.error || "\u0411\u044D\u043A\u0435\u043D\u0434 \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443"); + } + currentSession = data.session || currentSession; + totalAdded = data.count ?? totalAdded + 1; + if (options.onFileStatus) { + options.onFileStatus(k, "\u2713 \u0414\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D"); + } + if (options.onFileComplete) { + options.onFileComplete({ + fileIndex: k, + name, + size, + session: currentSession, + totalAdded + }); + } + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430 \u0444\u0430\u0439\u043B\u0430 ${name} \u0431\u044D\u043A\u0435\u043D\u0434\u043E\u043C: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + } + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E. \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E \u0444\u0430\u0439\u043B\u043E\u0432: ${total}`); + } + return { ok: true, session: currentSession, count: totalAdded }; +} + +// upload/frontend/index.js +var DEFAULTS = { + allowedExt: [], + labels: { + pickFiles: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B", + pickFolder: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443", + clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${bytes} B` + }, + layout: { columns: ["path", "size", "status"], controls: ["files", "folder", "clear"] }, + limits: { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + } +}; +function resolveMount(mount) { + const element = typeof mount === "string" ? document.querySelector(mount) : mount; + if (!element) throw new Error("File picker mount element \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"); + return element; +} +function normalizeConfig(config) { + return { + ...config, + allowedExt: config.allowedExt || DEFAULTS.allowedExt, + labels: { + ...DEFAULTS.labels, + ...config.labels || {}, + columns: { ...DEFAULTS.labels.columns, ...config.labels?.columns || {} } + }, + layout: { ...DEFAULTS.layout, ...config.layout || {} }, + limits: { ...DEFAULTS.limits, ...config.limits || {} } + }; +} +function injectStyles() { + if (document.querySelector("style[data-file-picker]")) return; + const style = document.createElement("style"); + style.dataset.filePicker = ""; + style.textContent = ` +.file-picker { color: #17211b; font-family: Georgia, 'Times New Roman', serif; } +.file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; } +.file-picker .fp-toolbar button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; } +.file-picker .fp-toolbar button.secondary { color: #1f5a3b; background: #cbdcc9; } +.file-picker .fp-toolbar button.quiet { color: #5d6a61; background: transparent; } +.file-picker .fp-table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; } +.file-picker .fp-table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; } +.file-picker .fp-table th, .file-picker .fp-table td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; } +.file-picker .fp-table th { color: #56745f; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; } +.file-picker .fp-status, .file-picker .fp-count { color: #5d6a61; font: 13px sans-serif; } +.file-picker .fp-count { min-height: 22px; } +.file-picker .tree-name { display: inline-flex; align-items: center; gap: 8px; min-width: 240px; padding-top: 0; padding-bottom: 0; border: 0; background: transparent; color: #17211b; font: inherit; text-align: left; } +.file-picker .tree-group { padding-left: 0; cursor: pointer; } +.file-picker .tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; } +.file-picker .remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; } +.file-picker .tree-row td:first-child { white-space: nowrap; } +@media (max-width: 600px) { .file-picker .fp-toolbar button { flex: 1 1 42%; } } +`; + document.head.append(style); +} +function buildMarkup(root, cfg) { + const controls = cfg.layout.controls || []; + const control = (name, id, label, className = "") => controls.includes(name) ? `` : ""; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(cfg.labels.columns); + const headings = columns.map((column) => `${esc(cfg.labels.columns[column] || column)}`).join(""); + root.innerHTML = ` +
+
+ + + ${control("files", "fp-files-btn", cfg.labels.pickFiles)} + ${control("folder", "fp-folder-btn", cfg.labels.pickFolder, "secondary")} + ${control("clear", "fp-clear-btn", cfg.labels.clear, "quiet")} +
+

+
${headings}
+

+
`; +} +function notify(cfg, state) { + if (typeof cfg.onChange === "function") cfg.onChange([...flattenFiles(state.nodes)]); +} +function removeMetadata(state, node2) { + if (node2.kind === "file") { + state.fileKeys.delete(`${node2.path}\0${node2.file.size}`); + return; + } + node2.children.forEach((child) => removeMetadata(state, child)); +} +function removeNode(state, id) { + const found = findNode(state.nodes, id); + if (!found) return false; + removeMetadata(state, found.node); + found.nodes.splice(found.nodes.indexOf(found.node), 1); + return true; +} +function initFilePicker(config) { + const cfg = normalizeConfig(config); + const mount = resolveMount(cfg.mount); + injectStyles(); + buildMarkup(mount, cfg); + const root = mount.firstElementChild; + const elements = { + fileInputEl: root.querySelector(".fp-file-input"), + folderInputEl: root.querySelector(".fp-folder-input"), + statusEl: root.querySelector(".fp-status"), + tableBodyEl: root.querySelector(".fp-table-body"), + countEl: root.querySelector(".fp-count") + }; + const state = { nodes: [], fileKeys: /* @__PURE__ */ new Set(), busy: false }; + const listeners = []; + const listen = (target, event, handler) => { + target.addEventListener(event, handler); + listeners.push(() => target.removeEventListener(event, handler)); + }; + const redraw = () => render(state, elements, cfg); + const filesChange = onFilesChange(state, cfg, elements); + const folderChange = onFolderChange(state, cfg, elements); + listen(elements.fileInputEl, "change", async () => { + await filesChange(); + notify(cfg, state); + }); + listen(elements.folderInputEl, "change", async () => { + await folderChange(); + notify(cfg, state); + }); + listen(elements.tableBodyEl, "click", (event) => { + const toggle = event.target.closest("[data-toggle]"); + if (toggle) { + const found = findNode(state.nodes, toggle.dataset.toggle); + if (found) found.node.expanded = !found.node.expanded; + redraw(); + return; + } + const remove = event.target.closest("[data-remove]"); + if (remove) { + if (removeNode(state, remove.dataset.remove)) { + redraw(); + notify(cfg, state); + } + } + }); + if (root.querySelector("#fp-files-btn")) listen(root.querySelector("#fp-files-btn"), "click", () => elements.fileInputEl.click()); + if (root.querySelector("#fp-folder-btn")) listen(root.querySelector("#fp-folder-btn"), "click", () => elements.folderInputEl.click()); + if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }); + redraw(); + return { + pickFiles: () => elements.fileInputEl.click(), + pickFolder: () => elements.folderInputEl.click(), + addFiles: async (files) => { + await addFiles(state, cfg, files, elements); + notify(cfg, state); + }, + getFiles: () => [...flattenFiles(state.nodes)], + remove: (id) => { + if (removeNode(state, id)) { + redraw(); + notify(cfg, state); + } + }, + clear: () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }, + render: redraw, + destroy: () => { + listeners.forEach((removeListener) => removeListener()); + state.nodes = []; + state.fileKeys.clear(); + mount.replaceChildren(); + } + }; +} +export { + DEFAULTS, + initFilePicker, + putToVm, + uploadViaVM +}; diff --git a/site/static/dist/file-picker.iife.js b/site/static/dist/file-picker.iife.js new file mode 100644 index 0000000..0659d05 --- /dev/null +++ b/site/static/dist/file-picker.iife.js @@ -0,0 +1,1218 @@ +var FilePicker = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + + // upload/frontend/index.js + var index_exports = {}; + __export(index_exports, { + DEFAULTS: () => DEFAULTS, + initFilePicker: () => initFilePicker, + putToVm: () => putToVm, + uploadViaVM: () => uploadViaVM + }); + + // node_modules/fflate/esm/browser.js + var u8 = Uint8Array; + var u16 = Uint16Array; + var i32 = Int32Array; + var fleb = new u8([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, + /* unused */ + 0, + 0, + /* impossible */ + 0 + ]); + var fdeb = new u8([ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, + /* unused */ + 0, + 0 + ]); + var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); + var freb = function(eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = j - b[i] << 5 | i; + } + } + return { b, r }; + }; + var _a = freb(fleb, 2); + var fl = _a.b; + var revfl = _a.r; + fl[28] = 258, revfl[258] = 28; + var _b = freb(fdeb, 0); + var fd = _b.b; + var revfd = _b.r; + var rev = new u16(32768); + for (i = 0; i < 32768; ++i) { + x = (i & 43690) >> 1 | (i & 21845) << 1; + x = (x & 52428) >> 2 | (x & 13107) << 2; + x = (x & 61680) >> 4 | (x & 3855) << 4; + rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; + } + var x; + var i; + var hMap = (function(cd, mb, r) { + var s = cd.length; + var i = 0; + var l = new u16(mb); + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = le[i - 1] + l[i - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + if (cd[i]) { + var sv = i << 4 | cd[i]; + var r_1 = mb - cd[i]; + var v = le[cd[i] - 1]++ << r_1; + for (var m = v | (1 << r_1) - 1; v <= m; ++v) { + co[rev[v] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i]; + } + } + } + return co; + }); + var flt = new u8(288); + for (i = 0; i < 144; ++i) + flt[i] = 8; + var i; + for (i = 144; i < 256; ++i) + flt[i] = 9; + var i; + for (i = 256; i < 280; ++i) + flt[i] = 7; + var i; + for (i = 280; i < 288; ++i) + flt[i] = 8; + var i; + var fdt = new u8(32); + for (i = 0; i < 32; ++i) + fdt[i] = 5; + var i; + var flrm = /* @__PURE__ */ hMap(flt, 9, 1); + var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1); + var max = function(a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; + }; + var bits = function(d, p, m) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8) >> (p & 7) & m; + }; + var bits16 = function(d, p) { + var o = p / 8 | 0; + return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); + }; + var shft = function(p) { + return (p + 7) / 8 | 0; + }; + var slc = function(v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + return new u8(v.subarray(s, e)); + }; + var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + // determined by compression function + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" + // determined by unknown compression method + ]; + var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; + }; + var inflt = function(dat, st, buf, dict) { + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + var resize = noBuf || st.i != 2; + var noSt = st.i; + if (noBuf) + buf = new u8(sl * 3); + var cbuf = function(l2) { + var bl = buf.length; + if (l2 > bl) { + var nbuf = new u8(Math.max(bl * 2, l2)); + nbuf.set(buf); + buf = nbuf; + } + }; + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + var tbts = sl * 8; + do { + if (!lm) { + final = bits(dat, pos, 1); + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + l); + buf.set(dat.subarray(s, t), bt); + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + var ldt = new u8(tl); + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + var clb = max(clt), clbmsk = (1 << clb) - 1; + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl; ) { + var r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + var s = r >> 4; + if (s < 16) { + ldt[i++] = s; + } else { + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } else { + var add = sym - 254; + if (sym > 264) { + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + var d = dm[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1, pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); + }; + var et = /* @__PURE__ */ new u8(0); + var b2 = function(d, b) { + return d[b] | d[b + 1] << 8; + }; + var b4 = function(d, b) { + return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0; + }; + var b8 = function(d, b) { + return b4(d, b) + b4(d, b + 4) * 4294967296; + }; + function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); + } + var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder(); + var tds = 0; + try { + td.decode(et, { stream: true }); + tds = 1; + } catch (e) { + } + var dutf8 = function(d) { + for (var r = "", i = 0; ; ) { + var c = d[i++]; + var eb = (c > 127) + (c > 223) + (c > 239); + if (i + eb > d.length) + return { s: r, r: slc(d, i - 1) }; + if (!eb) + r += String.fromCharCode(c); + else if (eb == 3) { + c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023); + } else if (eb & 1) + r += String.fromCharCode((c & 31) << 6 | d[i++] & 63); + else + r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63); + } + }; + function strFromU8(dat, latin1) { + if (latin1) { + var r = ""; + for (var i = 0; i < dat.length; i += 16384) + r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384)); + return r; + } else if (td) { + return td.decode(dat); + } else { + var _a2 = dutf8(dat), s = _a2.s, r = _a2.r; + if (r.length) + err(8); + return s; + } + } + var slzh = function(d, b) { + return b + 30 + b2(d, b + 26) + b2(d, b + 28); + }; + var zh = function(d, b, z) { + var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl; + var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2]; + return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off]; + }; + var z64hs = function(d, b, l, z, sc, su, off) { + var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l; + var nf = nsc + nsu + noff; + if (z && nf) { + for (; b + 4 < e; b += 4 + b2(d, b + 2)) { + if (b2(d, b) == 1) { + return [ + nsc ? b8(d, b + 4 + 8 * nsu) : sc, + nsu ? b8(d, b + 4) : su, + noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off, + 1 + ]; + } + } + if (z < 2) + err(13); + } + return [sc, su, off, 0]; + }; + function unzipSync(data, opts) { + var files = {}; + var e = data.length - 22; + for (; b4(data, e) != 101010256; --e) { + if (!e || data.length - e > 65558) + err(13); + } + ; + var c = b2(data, e + 8); + if (!c) + return {}; + var o = b4(data, e + 16); + var z = b4(data, e - 20) == 117853008; + if (z) { + var ze = b4(data, e - 12); + z = b4(data, ze) == 101075792; + if (z) { + c = b4(data, ze + 32); + o = b4(data, ze + 48); + } + } + var fltr = opts && opts.filter; + for (var i = 0; i < c; ++i) { + var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off); + o = no; + if (!fltr || fltr({ + name: fn, + size: sc, + originalSize: su, + compression: c_2 + })) { + if (!c_2) + files[fn] = slc(data, b, b + sc); + else if (c_2 == 8) + files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) }); + else + err(14, "unknown compression type " + c_2); + } + } + return files; + } + + // upload/frontend/table/rebase_tree.js + function rebaseTree(root, prefix) { + root.path = `${prefix}/${root.path}`; + if (root.file) root.file = new File([root.file], root.file.name, { lastModified: root.file.lastModified }); + root.children.forEach((child) => rebaseTree(child, prefix)); + return root; + } + + // upload/frontend/zip/list_zip_files.js + var DEFAULT_LIMITS = { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + }; + function extensionAllowed(name, allowedExt) { + const lowerName = name.toLowerCase(); + return allowedExt.some((extension) => lowerName.endsWith(extension.toLowerCase())); + } + function makeFile(data, name) { + return new File([data], name.split("/").at(-1)); + } + function safeEntryParts(entryName) { + if (!entryName || entryName.startsWith("/") || entryName.includes("\\")) return null; + const parts = entryName.split("/"); + if (parts.some((part) => !part || part === "." || part === "..")) return null; + return parts; + } + function node(kind, name, path, children = [], file = null) { + return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: kind === "file" }; + } + function addPath(root, parts, fileNode) { + let current = root; + parts.forEach((part, index) => { + const last = index === parts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? fileNode : node("folder", part, `${current.path}/${part}`); + current.children.push(child); + } + current = child; + }); + } + async function listEntries(data, zipName, allowedExt, depth, limits, budget) { + if (depth > limits.maxDepth) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0433\u043B\u0443\u0431\u043E\u043A\u0430\u044F \u0432\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0441\u0442\u044C ZIP"); + const entries = unzipSync(data, { + filter: (entry) => { + budget.entries += 1; + if (budget.entries > limits.maxEntries) throw new Error("\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E ZIP entries"); + const lowerName = entry.name.toLowerCase(); + if (!extensionAllowed(lowerName, allowedExt) && !lowerName.endsWith(".zip")) return false; + if (entry.originalSize > limits.maxEntryBytes) return false; + if (budget.totalBytes + entry.originalSize > limits.maxTotalBytes) { + throw new Error("\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u0441\u0443\u043C\u043C\u0430\u0440\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0445 ZIP entries"); + } + budget.totalBytes += entry.originalSize; + return true; + } + }); + const root = node("zip", zipName, zipName); + for (const [entryName, entryData] of Object.entries(entries)) { + if (entryName.endsWith("/")) continue; + const parts = safeEntryParts(entryName); + if (!parts) continue; + const normalizedName = parts.join("/"); + if (normalizedName.toLowerCase().endsWith(".zip")) { + const nested = await listEntries(entryData, entryName, allowedExt, depth + 1, limits, budget); + if (nested) { + rebaseTree(nested, zipName); + nested.name = parts.at(-1); + addPath(root, parts, nested); + } + } else if (extensionAllowed(normalizedName, allowedExt)) { + const path = `${zipName}/${normalizedName}`; + const file = makeFile(entryData, path); + addPath(root, parts, node("file", parts.at(-1), path, [], file)); + } + } + return root.children.length ? root : null; + } + async function listZipFiles(file, allowedExt, customLimits = {}) { + const data = new Uint8Array(await file.arrayBuffer()); + const limits = { ...DEFAULT_LIMITS, ...customLimits }; + return listEntries(data, file.name, allowedExt, 0, limits, { entries: 0, totalBytes: 0 }); + } + + // upload/frontend/table/add_file_with_dedup.js + function addFileWithDedup(state, fileNode) { + const accept = (node2) => { + if (node2.kind === "file") { + const key = `${node2.path}\0${node2.file.size}`; + if (state.fileKeys.has(key)) return null; + state.fileKeys.add(key); + return node2; + } + node2.children = node2.children.map(accept).filter(Boolean); + return node2.children.length ? node2 : null; + }; + const accepted = accept(fileNode); + if (!accepted || accepted.kind !== "file" && !accepted.children.length) return false; + if (accepted.kind !== "file") { + const existing = state.nodes.find((node2) => node2.kind !== "file" && node2.path === accepted.path); + if (existing) { + mergeChildren(existing, accepted); + return true; + } + } + state.nodes.push(accepted); + return true; + } + function mergeChildren(target, incoming) { + incoming.children.forEach((child) => { + if (child.kind === "file") { + const duplicate = target.children.some((existing2) => existing2.kind === "file" && existing2.path === child.path && existing2.file.size === child.file.size); + if (!duplicate) target.children.push(child); + return; + } + const existing = target.children.find((candidate) => candidate.kind !== "file" && candidate.path === child.path); + if (existing) mergeChildren(existing, child); + else target.children.push(child); + }); + } + + // upload/frontend/table/esc.js + function esc(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + })[character]); + } + + // upload/frontend/table/fs.js + function fs(bytes) { + return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`; + } + + // upload/frontend/table/render.js + var DEFAULT_RENDER_CONFIG = { + labels: { + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${fs(bytes)}` + }, + layout: { columns: ["path", "size", "status"] } + }; + function renderConfig(cfg = {}) { + return { + labels: { + ...DEFAULT_RENDER_CONFIG.labels, + ...cfg.labels || {}, + columns: { + ...DEFAULT_RENDER_CONFIG.labels.columns, + ...cfg.labels?.columns || {} + } + }, + layout: { ...DEFAULT_RENDER_CONFIG.layout, ...cfg.layout || {} } + }; + } + function renderNode(node2, depth, cfg) { + const isGroup = node2.kind !== "file"; + const padding = depth * 4; + const name = esc(node2.name); + const action = isGroup ? `` : `${name}`; + const remove = ``; + const pathAttribute = node2.kind === "file" ? ` data-path="${esc(node2.path)}"` : ""; + const cells = { + path: action, + size: node2.kind === "file" ? fs(node2.file.size) : "", + status: node2.error ? esc(node2.error) : node2.kind === "file" ? esc(cfg.labels.statusReady) : "" + }; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + const row = `` + columns.map((column) => `${cells[column] || ""}`).join("") + `${remove}`; + if (!isGroup || !node2.expanded) return row; + return row + node2.children.map((child) => renderNode(child, depth + 1, cfg)).join(""); + } + function render(state, elements, cfg) { + const normalized = renderConfig(cfg); + const columns = normalized.layout.columns?.length ? normalized.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); + elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node2) => renderNode(node2, 0, normalized)).join("") : `${esc(normalized.labels.empty)}`; + const files = []; + const visit = (node2) => { + if (node2.kind === "file") files.push(node2); + else node2.children.forEach(visit); + }; + state.nodes.forEach(visit); + const totalBytes = files.reduce((sum, node2) => sum + node2.file.size, 0); + elements.countEl.textContent = normalized.labels.count(files.length, totalBytes); + } + function findNode(nodes, id) { + for (const node2 of nodes) { + if (node2.id === id) return { node: node2, nodes }; + const found = findNode(node2.children || [], id); + if (found) return found; + } + return null; + } + function flattenFiles(nodes, result = []) { + nodes.forEach((node2) => { + if (node2.kind === "file") result.push({ path: node2.path, name: node2.name, size: node2.file.size, file: node2.file }); + else flattenFiles(node2.children, result); + }); + return result; + } + + // upload/frontend/table/on_files_change.js + async function addFiles(state, cfg, files, elements) { + for (const file of Array.from(files)) { + if (!file.name.toLowerCase().endsWith(".zip")) { + if (!cfg.allowedExt.some((extension) => file.name.toLowerCase().endsWith(extension.toLowerCase()))) { + continue; + } + addFileWithDedup(state, { + id: crypto.randomUUID(), + kind: "file", + name: file.name, + path: file.name, + file, + children: [], + expanded: true + }); + continue; + } + try { + const zipTree = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zipTree) addFileWithDedup(state, zipTree); + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } + render(state, elements, cfg); + } + function onFilesChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + await addFiles(state, cfg, elements.fileInputEl.files, elements); + } finally { + elements.fileInputEl.value = ""; + state.busy = false; + } + }; + } + + // upload/frontend/table/on_folder_change.js + function onFolderChange(state, cfg, elements) { + return async () => { + if (state.busy) return; + state.busy = true; + try { + const roots = /* @__PURE__ */ new Map(); + for (const file of Array.from(elements.folderInputEl.files)) { + const parts = (file.webkitRelativePath || file.name).split("/"); + const relativePath = parts.slice(1).join("/") || file.name; + const lowerPath = relativePath.toLowerCase(); + const rootName = parts[0] || file.name; + if (!roots.has(rootName)) { + roots.set(rootName, { + id: crypto.randomUUID(), + kind: "folder", + name: rootName, + path: rootName, + children: [], + expanded: false + }); + } + const root = roots.get(rootName); + const addToFolder = (node2) => { + let current = root; + const nodeParts = node2.path.split("/").slice(1); + nodeParts.forEach((part, index) => { + const last = index === nodeParts.length - 1; + let child = current.children.find((item) => item.name === part); + if (!child) { + child = last ? node2 : { + id: crypto.randomUUID(), + kind: "folder", + name: part, + path: `${rootName}/${nodeParts.slice(0, index + 1).join("/")}`, + children: [], + expanded: false + }; + current.children.push(child); + } + current = child; + }); + }; + if (lowerPath.endsWith(".zip")) { + try { + const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits); + if (zip) { + rebaseTree(zip, rootName); + addToFolder(zip); + } + } catch (error) { + if (typeof cfg.onError === "function") { + cfg.onError(error, file); + } else if (elements.statusEl) { + elements.statusEl.textContent = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0430\u0440\u0445\u0438\u0432\u0430 ${file.name}: ${error.message}`; + } + continue; + } + } else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) { + addToFolder({ + id: crypto.randomUUID(), + kind: "file", + name: parts.at(-1), + path: `${rootName}/${relativePath}`, + file, + children: [], + expanded: true + }); + } + } + roots.forEach((root) => addFileWithDedup(state, root)); + elements.folderInputEl.value = ""; + render(state, elements, cfg); + } finally { + state.busy = false; + } + }; + } + + // upload/frontend/upload/put_to_vm.js + function putToVm(file, url, options = {}) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + if (options.onXHR) { + options.onXHR(xhr); + } + const abortHandler = () => { + xhr.abort(); + reject(new DOMException("Upload aborted", "AbortError")); + }; + if (options.signal) { + if (options.signal.aborted) { + reject(new DOMException("Upload aborted", "AbortError")); + return; + } + options.signal.addEventListener("abort", abortHandler, { once: true }); + } + xhr.open("PUT", url); + xhr.timeout = options.timeoutMs || 3e5; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable && options.onProgress) { + const pct = Math.round(e.loaded / e.total * 100); + options.onProgress(pct, e.loaded, e.total); + } + }; + xhr.onload = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`\u0411\u0443\u0444\u0435\u0440: HTTP ${xhr.status} ${xhr.statusText}`)); + } + }; + xhr.onerror = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error("\u0421\u0435\u0442\u0435\u0432\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435 \u0432 \u0431\u0443\u0444\u0435\u0440")); + }; + xhr.ontimeout = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new Error(`\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u0431\u0443\u0444\u0435\u0440\u0430 (${Math.round(xhr.timeout / 1e3)}\u0441)`)); + }; + xhr.onabort = () => { + if (options.signal) { + options.signal.removeEventListener("abort", abortHandler); + } + reject(new DOMException("Upload aborted", "AbortError")); + }; + xhr.send(file); + }); + } + + // upload/frontend/upload/upload_via_vm.js + function generateUuid() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === "x" ? r : r & 3 | 8; + return v.toString(16); + }); + } + function normalizeFileEntry(item) { + if (item instanceof File || item instanceof Blob) { + return { name: item.name || "unnamed", size: item.size, file: item, path: item.name || "" }; + } + if (item && item.file) { + return { + name: item.name || item.file.name || "unnamed", + size: item.size ?? item.file.size ?? 0, + file: item.file, + path: item.path || item.name || "" + }; + } + throw new Error("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0431\u044A\u0435\u043A\u0442 \u0444\u0430\u0439\u043B\u0430"); + } + async function uploadViaVM(files, options = {}) { + if (!options.vmUploadUrl) { + return { ok: false, error: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 options.vmUploadUrl \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D" }; + } + const normalizedFiles = Array.from(files).map(normalizeFileEntry); + const total = normalizedFiles.length; + if (total === 0) { + return { ok: true, session: options.session || "", count: 0 }; + } + const backendUrl = options.backendUploadUrl || (options.apiBase || "") + "/api/upload_refs"; + const vmBase = options.vmUploadUrl.endsWith("/") ? options.vmUploadUrl : `${options.vmUploadUrl}/`; + let currentSession = options.session || ""; + let totalAdded = 0; + for (let k = 0; k < total; k++) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + const { name, size, file } = normalizedFiles[k]; + const fileKey = `${generateUuid()}_${k}`; + const fileUrl = `${vmBase}${fileKey}`; + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0432 \u0431\u0443\u0444\u0435\u0440 (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u23F3 \u0412 \u0431\u0443\u0444\u0435\u0440..."); + } + const t0 = performance.now(); + try { + await putToVm(file, fileUrl, { + signal: options.signal, + onXHR: options.onXHR, + onProgress(pct, loaded, fileTotal) { + const elapsed = (performance.now() - t0) / 1e3; + const speed = elapsed > 0 ? loaded / elapsed : 0; + if (options.onFileStatus) { + options.onFileStatus(k, `\u23F3 ${pct}% (${fs(speed)}/s)`); + } + if (options.onProgress) { + options.onProgress({ index: k, total, pct, speed, loaded, fileTotal, name }); + } + } + }); + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u0431\u0443\u0444\u0435\u0440\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u0430\u0439\u043B\u0430 ${name} \u0432 \u0431\u0443\u0444\u0435\u0440: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + if (options.onStatus) { + options.onStatus(`\u041F\u0440\u0438\u0451\u043C \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u043C (${k + 1}/${total}): ${name}`); + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u26A1 \u0412 \u0441\u0435\u0441\u0441\u0438\u044E..."); + } + try { + const resp = await fetch(backendUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session: currentSession, + files: [{ name, size, url: fileUrl }] + }), + signal: options.signal + }); + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} ${resp.statusText}`); + } + const data = await resp.json(); + if (!data.ok) { + throw new Error(data.error || "\u0411\u044D\u043A\u0435\u043D\u0434 \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443"); + } + currentSession = data.session || currentSession; + totalAdded = data.count ?? totalAdded + 1; + if (options.onFileStatus) { + options.onFileStatus(k, "\u2713 \u0414\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D"); + } + if (options.onFileComplete) { + options.onFileComplete({ + fileIndex: k, + name, + size, + session: currentSession, + totalAdded + }); + } + } catch (err2) { + if (options.signal?.aborted) { + return { ok: false, error: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430", aborted: true, session: currentSession }; + } + if (options.onFileStatus) { + options.onFileStatus(k, "\u2717 \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430"); + } + return { + ok: false, + error: `\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u0451\u043C\u0430 \u0444\u0430\u0439\u043B\u0430 ${name} \u0431\u044D\u043A\u0435\u043D\u0434\u043E\u043C: ${err2.message}`, + failedIndex: k, + session: currentSession + }; + } + } + if (options.onStatus) { + options.onStatus(`\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E. \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E \u0444\u0430\u0439\u043B\u043E\u0432: ${total}`); + } + return { ok: true, session: currentSession, count: totalAdded }; + } + + // upload/frontend/index.js + var DEFAULTS = { + allowedExt: [], + labels: { + pickFiles: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B", + pickFolder: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0430\u043F\u043A\u0443", + clear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C", + empty: "\u041D\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432", + statusReady: "\u0433\u043E\u0442\u043E\u0432", + remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C", + columns: { path: "\u041F\u0443\u0442\u044C", size: "\u0420\u0430\u0437\u043C\u0435\u0440", status: "\u0421\u0442\u0430\u0442\u0443\u0441" }, + count: (count, bytes) => `${count} \u0444\u0430\u0439\u043B\u043E\u0432 \xB7 ${bytes} B` + }, + layout: { columns: ["path", "size", "status"], controls: ["files", "folder", "clear"] }, + limits: { + maxEntries: 1e3, + maxTotalBytes: 100 * 1024 * 1024, + maxEntryBytes: 50 * 1024 * 1024, + maxDepth: 20 + } + }; + function resolveMount(mount) { + const element = typeof mount === "string" ? document.querySelector(mount) : mount; + if (!element) throw new Error("File picker mount element \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"); + return element; + } + function normalizeConfig(config) { + return { + ...config, + allowedExt: config.allowedExt || DEFAULTS.allowedExt, + labels: { + ...DEFAULTS.labels, + ...config.labels || {}, + columns: { ...DEFAULTS.labels.columns, ...config.labels?.columns || {} } + }, + layout: { ...DEFAULTS.layout, ...config.layout || {} }, + limits: { ...DEFAULTS.limits, ...config.limits || {} } + }; + } + function injectStyles() { + if (document.querySelector("style[data-file-picker]")) return; + const style = document.createElement("style"); + style.dataset.filePicker = ""; + style.textContent = ` +.file-picker { color: #17211b; font-family: Georgia, 'Times New Roman', serif; } +.file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; } +.file-picker .fp-toolbar button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; } +.file-picker .fp-toolbar button.secondary { color: #1f5a3b; background: #cbdcc9; } +.file-picker .fp-toolbar button.quiet { color: #5d6a61; background: transparent; } +.file-picker .fp-table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; } +.file-picker .fp-table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; } +.file-picker .fp-table th, .file-picker .fp-table td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; } +.file-picker .fp-table th { color: #56745f; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; } +.file-picker .fp-status, .file-picker .fp-count { color: #5d6a61; font: 13px sans-serif; } +.file-picker .fp-count { min-height: 22px; } +.file-picker .tree-name { display: inline-flex; align-items: center; gap: 8px; min-width: 240px; padding-top: 0; padding-bottom: 0; border: 0; background: transparent; color: #17211b; font: inherit; text-align: left; } +.file-picker .tree-group { padding-left: 0; cursor: pointer; } +.file-picker .tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; } +.file-picker .remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; } +.file-picker .tree-row td:first-child { white-space: nowrap; } +@media (max-width: 600px) { .file-picker .fp-toolbar button { flex: 1 1 42%; } } +`; + document.head.append(style); + } + function buildMarkup(root, cfg) { + const controls = cfg.layout.controls || []; + const control = (name, id, label, className = "") => controls.includes(name) ? `` : ""; + const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(cfg.labels.columns); + const headings = columns.map((column) => `${esc(cfg.labels.columns[column] || column)}`).join(""); + root.innerHTML = ` +
+
+ + + ${control("files", "fp-files-btn", cfg.labels.pickFiles)} + ${control("folder", "fp-folder-btn", cfg.labels.pickFolder, "secondary")} + ${control("clear", "fp-clear-btn", cfg.labels.clear, "quiet")} +
+

+
${headings}
+

+
`; + } + function notify(cfg, state) { + if (typeof cfg.onChange === "function") cfg.onChange([...flattenFiles(state.nodes)]); + } + function removeMetadata(state, node2) { + if (node2.kind === "file") { + state.fileKeys.delete(`${node2.path}\0${node2.file.size}`); + return; + } + node2.children.forEach((child) => removeMetadata(state, child)); + } + function removeNode(state, id) { + const found = findNode(state.nodes, id); + if (!found) return false; + removeMetadata(state, found.node); + found.nodes.splice(found.nodes.indexOf(found.node), 1); + return true; + } + function initFilePicker(config) { + const cfg = normalizeConfig(config); + const mount = resolveMount(cfg.mount); + injectStyles(); + buildMarkup(mount, cfg); + const root = mount.firstElementChild; + const elements = { + fileInputEl: root.querySelector(".fp-file-input"), + folderInputEl: root.querySelector(".fp-folder-input"), + statusEl: root.querySelector(".fp-status"), + tableBodyEl: root.querySelector(".fp-table-body"), + countEl: root.querySelector(".fp-count") + }; + const state = { nodes: [], fileKeys: /* @__PURE__ */ new Set(), busy: false }; + const listeners = []; + const listen = (target, event, handler) => { + target.addEventListener(event, handler); + listeners.push(() => target.removeEventListener(event, handler)); + }; + const redraw = () => render(state, elements, cfg); + const filesChange = onFilesChange(state, cfg, elements); + const folderChange = onFolderChange(state, cfg, elements); + listen(elements.fileInputEl, "change", async () => { + await filesChange(); + notify(cfg, state); + }); + listen(elements.folderInputEl, "change", async () => { + await folderChange(); + notify(cfg, state); + }); + listen(elements.tableBodyEl, "click", (event) => { + const toggle = event.target.closest("[data-toggle]"); + if (toggle) { + const found = findNode(state.nodes, toggle.dataset.toggle); + if (found) found.node.expanded = !found.node.expanded; + redraw(); + return; + } + const remove = event.target.closest("[data-remove]"); + if (remove) { + if (removeNode(state, remove.dataset.remove)) { + redraw(); + notify(cfg, state); + } + } + }); + if (root.querySelector("#fp-files-btn")) listen(root.querySelector("#fp-files-btn"), "click", () => elements.fileInputEl.click()); + if (root.querySelector("#fp-folder-btn")) listen(root.querySelector("#fp-folder-btn"), "click", () => elements.folderInputEl.click()); + if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }); + redraw(); + return { + pickFiles: () => elements.fileInputEl.click(), + pickFolder: () => elements.folderInputEl.click(), + addFiles: async (files) => { + await addFiles(state, cfg, files, elements); + notify(cfg, state); + }, + getFiles: () => [...flattenFiles(state.nodes)], + remove: (id) => { + if (removeNode(state, id)) { + redraw(); + notify(cfg, state); + } + }, + clear: () => { + state.nodes = []; + state.fileKeys.clear(); + redraw(); + notify(cfg, state); + }, + render: redraw, + destroy: () => { + listeners.forEach((removeListener) => removeListener()); + state.nodes = []; + state.fileKeys.clear(); + mount.replaceChildren(); + } + }; + } + return __toCommonJS(index_exports); +})(); diff --git a/site/templates/index.html b/site/templates/index.html index b04bb93..a43c5e9 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -49,38 +49,47 @@ } .card-body { padding: 12px; } .sub { font-size: 12px; color: var(--muted); margin-bottom: 10px; line-height: 1.5; } - .file-input-wrap { margin-bottom: 10px; } - .file-input-wrap input[type="file"] { width: 100%; font-size: 13px; } - .table-wrap { border: 1px solid var(--brand-gray); border-radius: 8px; overflow: hidden; } - table { width: 100%; border-collapse: collapse; font-size: 13px; } - th { + + /* FilePicker styling */ + .file-picker { font-family: inherit; color: var(--text); } + .file-picker .fp-toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; } + .file-picker .fp-toolbar button { + height: 32px; border-radius: 6px; padding: 0 12px; font-size: 13px; + font-family: inherit; cursor: pointer; border: 1px solid var(--brand-gray); + background: var(--card); color: var(--text); + display: inline-flex; align-items: center; gap: 5px; font-weight: 500; + } + .file-picker .fp-toolbar button#fp-files-btn { background: #eff6ff; border-color: var(--brand-primary); color: var(--brand-primary); } + .file-picker .fp-toolbar button#fp-folder-btn { background: var(--card); } + .file-picker .fp-toolbar button#fp-clear-btn { color: var(--muted); border-color: transparent; } + .file-picker .fp-toolbar button#fp-clear-btn:hover { color: var(--red); } + .file-picker .fp-table-wrap { border: 1px solid var(--brand-gray); border-radius: 8px; overflow: hidden; margin-bottom: 6px; } + .file-picker .fp-table { width: 100%; border-collapse: collapse; font-size: 13px; } + .file-picker .fp-table th { background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 8px; - border-right: 1px solid var(--brand-gray); text-align: left; - font-weight: 600; font-size: 10px; color: var(--muted); + border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); + text-align: left; font-weight: 600; font-size: 10px; color: var(--muted); } - td { - padding: 5px 8px; border-right: 1px solid var(--brand-gray); - border-bottom: 1px solid var(--brand-gray); + .file-picker .fp-table td { + padding: 5px 8px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); } - tr:hover td { background: rgba(37,99,235,.03); } - .name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .num-cell { text-align: right; white-space: nowrap; } - .empty-row td { color: var(--muted); text-align: center; padding: 20px; } - .row-over td { background: rgba(220,38,38,.10) !important; color:#c0392b; } - .row-over .num-cell { color:#c0392b; } - .row-over .name-cell { color:#c0392b; } - .row-current td { background: rgba(37,99,235,.12) !important; color:#1d4ed8; font-weight: 600; } - .grp-row td { - background: var(--brand-gray); font-size: 12px; font-weight: 700; - letter-spacing: .03em; text-transform: uppercase; color: var(--muted); - padding: 4px 8px; border: none; + .file-picker .tree-row:hover td { background: rgba(37,99,235,.03); } + .file-picker .tree-row.row-current td { background: rgba(37,99,235,.12) !important; color: #1d4ed8; font-weight: 600; } + .file-picker .tree-name { + display: inline-flex; align-items: center; gap: 6px; border: 0; background: transparent; + color: inherit; font: inherit; text-align: left; cursor: pointer; } - .remove-btn { + .file-picker .tree-chevron { display: inline-block; width: 12px; color: var(--muted); font-size: 9px; } + .file-picker .fp-status { font-size: 12px; color: var(--muted); margin: 4px 0; } + .file-picker .fp-count { display: none; } + .file-picker .remove-btn { cursor: pointer; color: #f87171; background: none; border: none; padding: 2px 4px; font-size: 14px; line-height: 1; } - .remove-btn:hover { color: var(--red); } - body.busy .remove-btn { display: none; } + .file-picker .remove-btn:hover { color: var(--red); } + body.busy .file-picker button { pointer-events: none; opacity: 0.5; } + body.busy .file-picker .remove-btn { display: none; } + .footer-bar { margin-top: 10px; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--muted); @@ -146,29 +155,12 @@

Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.
CSV с таблицей замен скачивается отдельно.
- Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх лимитов помечаются красным (статус «пропущен») и не участвуют в обфускации. Пустые, повреждённые файлы и сканы без текстового слоя получают статус «не извлечён» — они тоже не попадают в результат.
+ Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх лимитов помечаются статусом «пропущен» и не участвуют в обфускации. Пустые, повреждённые файлы и сканы без текстового слоя получают статус «не извлечён» — они тоже не попадают в результат.
⏱ Время обработки каждого файла — ориентировочное (обновляется по факту).

-
- - - -
-
- - - - - - - - - - - - -
ИмяРазмерСтатус
Нет выбранных файлов
-
+ +
+ - +