# 2026-09-05 02:31 — ffmpeg/ghostscript в образ, водяной знак PDF, снятие лимитов админа **Сделано:** - **Найдена причина «Ошибка загрузки» видео:** в контейнере не было ни `ffmpeg`/`ffprobe`, ни `gs` (контейнер поднимался из голого `node:20-slim`). В `getVideoDuration()` не было обработчика `'error'` → `spawn ENOENT` уходил в `uncaughtException` → `process.exit(1)`, то есть **загрузка видео роняла весь сервер**. Лимиты размера были ни при чём. - **Контейнер переведён на собственный образ:** создан `/opt/sites/wadevelop/Dockerfile` (`FROM node:20-slim` + `ffmpeg` + `ghostscript`), в `/opt/sites/docker-compose.yml` сервис `wadevelop` переключён с `image: node:20-slim` на `build: ./wadevelop` + `image: wadevelop-node:latest`. Образ собран, контейнер пересоздан. Проверено на бою: `gs 10.00.0`, ffmpeg с libx264/libvpx/libmp3lame/aac, тестовые mp4 и gif собираются. - **PDF: водяной знак не работал вообще** — в `public/pdf.html` в `sendRequest()` отсутствовала ветка `case 'watermark'`, запрос падал в `default` («Неизвестная операция: watermark»). Добавлена ветка, параметры сделаны опциональными, кириллица — через встраиваемый TTF (`@pdf-lib/fontkit` + Manrope), т.к. `StandardFonts.Helvetica` кодирует только WinAnsi. - **Админ освобождён от всех ограничений** — единая фабрика лимитов `lib/limits.js` (`skip: isAdmin`), сняты лимиты размера/количества в PDF и размера в video (включая клиентскую отсечку). - **Закрыт IDOR на скачивании результатов** во всех четырёх download-роутах через новый реестр владельцев `lib/owner.js` (SQLite, та же БД что и очередь) + случайные суффиксы в именах файлов. - **Всё выкачено на прод и проверено:** сайт 200, `/health` 200, стартовый лог чистый — спам `ERR_ERL_KEY_GEN_IPV6` (4 шт. при каждом старте) исчез; реестр владельцев проверен прогоном на проде (свой `true`, чужой `false`, неизвестный `false`). **Изменённые / созданные файлы:** - [lib/owner.js](../../lib/owner.js) — **новый**: реестр владельцев файлов-результатов (`file_owners` в `jobs.db`), `claim/isOwner/displayName`, авточистка старше 24 ч - [lib/limits.js](../../lib/limits.js) — **новый**: `createLimiter()` — общая фабрика rate-limit, `skip: isAdmin`, ключ через `ipKeyGenerator` (чинит `ERR_ERL_KEY_GEN_IPV6`) - [routes/video.js](../../routes/video.js) — обработчики `'error'` для ffmpeg/ffprobe (crash-guard), `FFMPEG_MISSING` → 503, таймаут `VIDEO_TIMEOUT_MS` (10 → 30 мин), `-ss` до `-i` и `-t` вместо `-to`, прогресс от длительности результата, валидация `startTime`/`endTime`, owner-check в `/download`, снят лимит размера для админа - [routes/pdf.js](../../routes/pdf.js) — `runGhostscript()` + `gsUnavailable()` (503 вместо `spawn gs ENOENT`) для preview/compress/toImages, водяной знак с TTF и опциональными параметрами, owner-check в `/download`, `crypto.randomBytes` в именах, лимиты размера/количества сняты для админа - [routes/compress.js](../../routes/compress.js) — случайные суффиксы в именах архива и single-файла, `owner.claim`, owner-check в `/compress/download`, лимитер через общую фабрику - [server.js](../../server.js) — owner-check в `/download/:filename` - [routes/api.js](../../routes/api.js), [routes/auth.js](../../routes/auth.js), [routes/favicon.js](../../routes/favicon.js), [routes/redirects.js](../../routes/redirects.js), [routes/parser.js](../../routes/parser.js), [routes/httpclient.js](../../routes/httpclient.js) — переведены на `createLimiter` (в auth — `skipAdmin: false`) - [public/pdf.html](../../public/pdf.html) — ветка `case 'watermark'` в `sendRequest()`, убрана обязательность текста, метка «(необязательно)» - [public/video.html](../../public/video.html) — детект админа вынесен на загрузку страницы (раньше `/auth/me` спрашивался уже после проверки размера — первый большой файл всегда отклонялся), `MAX_SIZE = Infinity` для админа - [package.json](../../package.json) — добавлен `@pdf-lib/fontkit` - На VPS: `/opt/sites/wadevelop/Dockerfile` (новый), `/opt/sites/docker-compose.yml` (сервис на `build`) **Решения:** - **Бинарники через Dockerfile, а не `apt` в живом контейнере** — `docker exec apt-get install` слетел бы при первом же пересоздании контейнера. - **Кириллица в водяном знаке через `@pdf-lib/fontkit` + `public/vendor/fonts/manrope-600.ttf`**, с откатом на Helvetica + транслитерацию, если пакет/шрифт недоступны — чтобы код работал в любой среде, а не падал 500-й. - **Владелец файла хранится в SQLite, а не в памяти** — переживает рестарт; отсутствие записи трактуется как «чужой» (записи живут 24 ч против 30 мин у самих файлов). - **Лимит входа `/auth/login` для админа НЕ снят** — на этапе логина сессии ещё нет, отличить админа технически не от чего, и это защита самого аккаунта от подбора. - Правки делались от локальной копии: перед деплоем сверены md5 всех 13 затрагиваемых файлов — **VPS был идентичен git HEAD** (`a3fb5a2`), расхождений нет. - Заливка — через PowerShell, а не Bash: Git Bash конвертирует POSIX-пути (`/opt/...` → `C:/Program Files/Git/opt/...`) и ломает `sftp_put.py`. **Открытые вопросы / следующие шаги:** - [ ] **Проверить под залогиненной сессией:** водяной знак PDF (кириллица, дефолты) и конвертацию/ сжатие видео с прогрессом — у меня нет учётной записи для UI-проверки. - [ ] **Очередь не ограничивает параллельность** — `queue.getPending()` нигде не вызывается, каждый `/video/convert` спавнит ffmpeg немедленно. Пока ffmpeg отсутствовал, это не проявлялось; теперь 3–4 одновременные конвертации положат VPS. - [ ] **`/mnt/webdata/storage` живёт внутри контейнера, а не в томе** — пересоздание контейнера (как сегодня) обнуляет `jobs.db` и папки результатов. Нужен том в `docker-compose.yml`. - [ ] `routes/api.js` `/ai/explain` бьётся в `192.168.31.100:11434` (домашний PC) и `127.0.0.1:8080` — с VPS недоступно, объяснялка regex и security-аудит на проде мертвы. - [ ] `routes/logs.js` (68 строк) нигде не подключён — мёртвый файл, ссылок на `/logs` в HTML нет. - [ ] `routes/api.js` — `req.on('close')` для `AbortController` регистрируется **после** дочитывания стрима: отмена при отвале клиента не работает никогда. - [ ] `server.js:44-47` — публичные `/api/*` смонтированы как `app.get('/api/settings', apiRouter)`; путь внутри роутера не совпадает, запрос проваливается дальше и обрабатывается вторым монтированием `app.use('/api', ...)`. Работает случайно, `publicLimiter` срабатывает не там. - [ ] Хвосты прошлого аудита: DNS-rebinding pinning (HIGH-14), обновление 36 уязвимых npm-зависимостей, сведение VPS-прод и rpi4-двойника в один git. - [ ] Локальные правки **не закоммичены** — рабочее дерево грязное (14 изменённых + 2 новых файла). **Как продолжить (для чистой сессии):** - Контекст: **прод = VPS 195.140.146.84**, контейнер `wadevelop`, код `/opt/sites/www/wadevelop.ru/`, теперь собирается из `/opt/sites/wadevelop/Dockerfile`. rpi4 — двойник, НЕ деплоить. - SSH: `python3 C:/Users/tream/.claude/ssh_cmd.py root@195.140.146.84 ""`. Заливка: `python3 C:/Users/tream/.claude/sftp_put.py root@195.140.146.84 ` — **запускать через PowerShell**, Bash ломает POSIX-пути. - Пересборка образа: `cd /opt/sites && docker compose build wadevelop && docker compose up -d wadevelop`. Обычный деплой без смены зависимостей: заливка файлов + `docker restart wadevelop`. - Проверка: `curl -s -o /dev/null -w "%{http_code}" https://wadevelop.ru/` (200); `docker logs wadevelop --tail 20`; `docker exec wadevelop which ffmpeg ffprobe gs`. - Откат: бэкапы `*.bak-20260905` в `/opt/sites/www/wadevelop.ru/` (13 файлов) и `/opt/sites/docker-compose.yml.bak-20260905`. **Ссылки:** - Прод: https://wadevelop.ru — VPS 195.140.146.84 (FirstVDS, treamz.fvds.ru), контейнер `wadevelop` - Предыдущая сессия: [2026-07-24 23:48 — аудит безопасности](2026-07-24-2348.md) - Git: локаль на коммите `a3fb5a2` + незакоммиченные правки этой сессии