Перейти к основному содержимому

Загрузки панели в S3

Файлы, которые пользователи загружают в редактор (фото, видео, документы узлов), можно хранить в S3-совместимом бакете вместо папки uploads/ на сервере. Тогда панель и боты не зависят от диска конкретной машины: проект можно перенести на другой сервер, в Kubernetes или на Railway.

Как это устроено​

  • Адрес файла не меняется: /uploads/<projectId>/<дата>/<файл>. Он записан в узлах проектов и в media_files, переписывать их не нужно.
  • Ключ объекта в бакете совпадает с путём внутри uploads/: 3/2026-08-19/photo.jpg.
  • Для зарегистрированного адреса панель выбирает источник по записи media_files, включая исходное хранилище, даже если активное хранилище изменилось. Неоднозначный адрес не выдаётся. Незарегистрированные локальные файлы продолжают раздаваться статически. Зарегистрированные файлы с признаком публичности доступны без сессии; для приватных файлов требуется доступ к проекту. Незарегистрированные локальные файлы сохраняют прежнюю публичную статическую раздачу.
  • Чтение из S3 доступно только для зарегистрированного адреса. Сборки ботов, бэкапы и произвольные ключи бакетов через /uploads не открываются.
  • /api/media/s3-proxy/<configId>/<ключ> отдаёт объект приватного бакета владельцу или коллаборатору проекта. Без личности — 401. Строка media_files ищется по хранилищу и ключу: нет строки — 404, нет доступа к проекту — 403. Совпадение configId с UPLOADS_STORAGE_ID само по себе доступ не даёт. Ответ с Cache-Control: private.
  • Новые загрузки в хранилище загрузок тоже получают адрес /uploads/....
  • Удаление файла в панели удаляет объект из бакета и оставшуюся локальную копию.

Доступ бота к оригиналам и экспорт​

Для получения оригиналов из панели задайте постоянный секрет MEDIA_RUNTIME_SECRET на сервере панели и MEDIA_RUNTIME_BASE_URL с адресом панели, доступным процессу бота. В Docker и на удалённом исполнителе localhost обычно указывает на сам контейнер или исполнитель. Для удалённого исполнителя используйте HTTPS; HTTP разрешён только при явном MEDIA_RUNTIME_ALLOW_INSECURE_REMOTE=true для доверенного защищённого канала. Бот получает отдельный токен проекта, а не ключи S3. Не меняйте секрет без планирования перезапуска ботов: ранее выданные токены перестанут действовать.

Для автономного запуска доступен ZIP через POST /api/projects/:id/export?format=zip с авторизацией проекта. Архив содержит bot.py, оригиналы в uploads/ и manifest.json; при недоступности файла экспорт завершается ошибкой. Общий размер оригиналов ограничен 128 МиБ. Обычный экспорт кода без format=zip не включает файлы.

Для серверного чтения локальных файлов на Windows используется PowerShell и проверка открытого файлового дескриптора; скрипт windows-media-read.ps1 должен находиться рядом с модулем при развёртывании. Перед выпуском проверьте доступность всех нужных файлов и не удаляйте локальные копии до проверки целевого режима. Запуск бота не подтверждает фактическую отправку медиа.

Поля бакета и UPLOADS_STORAGE_ID задаются в админке, раздел «Хранилища» (настройки рантайма). Пока значение не сохранено, читается переменная окружения. Сохранение обновляет строку s3-default.

Настройка​

  1. Создайте бакет (Railway Bucket, Cloudflare R2, MinIO, SeaweedFS).
  2. Добавьте хранилище в панель — «Хранилища» в интерфейсе или API:
curl -X POST https://<панель>/api/storage-configs \
-H 'Content-Type: application/json' -b <cookie сессии> \
-d '{"id":"s3-uploads","name":"S3: загрузки","backend":"s3",
"config":{"bucket":"tbb-uploads","region":"auto","endpointUrl":"https://...","forcePathStyle":false},
"s3AccessKeyId":"...","s3SecretAccessKey":"..."}'
curl -X POST https://<панель>/api/storage-configs/s3-uploads/test -b <cookie сессии>

Для ключей нужен STORAGE_ENCRYPTION_KEY.

  1. Пропишите в .env и перезапустите панель:
UPLOADS_STORAGE_ID=s3-uploads

Перенос существующих файлов​

npm run storage:migrate-uploads -- --dry-run # что будет залито
npm run storage:migrate-uploads # залить и переключить media_files на S3
npm run storage:migrate-uploads -- --delete-local # удалить локальные копии (см. ограничение)
ФлагНазначение
--target <id>Хранилище (по умолчанию UPLOADS_STORAGE_ID)
--dir <папка>Папка загрузок (по умолчанию uploads)
--dry-runТолько показать план
--delete-localУдалять файл с диска после проверки размера в бакете
  • Переносится вся папка, включая файлы без записи в media_files (на них могут ссылаться узлы).
  • Файлы в корне uploads/ (README) и скрытые файлы пропускаются.
  • Повторный запуск безопасен: файлы с тем же размером в бакете пропускаются.
  • После заливки размер проверяется; при несовпадении файл считается ошибкой и не удаляется.
  • Скрипт завершается с кодом 1, если были ошибки.

Перед переносом на проде снимите бэкап базы (DB_BACKUPS.md).

Чтобы новые загрузки шли в S3​

Сделайте хранилище активным: PATCH /api/storage-configs/s3-uploads с {"isActive": true}. Откат — сделать активным local-default; адреса в проектах при этом не меняются.

Локальная разработка на SeaweedFS​

./weed server -dir=data -s3 -s3.port=8333 -s3.config=s3.json \
-master.volumeSizeLimitMB=256 -volume.max=64 -ip=127.0.0.1
echo "s3.bucket.create -name tbb-uploads" | ./weed shell -master=127.0.0.1:9333

Каждому бакету нужны свои тома. При малом -volume.max (например, 8) запись в новый бакет падает с failed to find writable volumes, а S3 отвечает «We encountered an internal error».

Хранилище для SeaweedFS: endpointUrl: http://127.0.0.1:8333, forcePathStyle: true, region: us-east-1.

Кэш удаляет старые файлы, когда их больше не удерживает задача отправки. Если предел занят используемыми файлами, бот сообщает об ошибке. По умолчанию предел составляет 512 МиБ на процесс; штатное завершение удаляет временный каталог. После аварийной остановки оставшиеся каталоги кэша можно удалить после остановки соответствующих ботов. Для проверки доступности выбранного S3 нужна возможность HeadBucket; результат успешной проверки кэшируется на пять секунд.

Для картинки счёта Telegram задайте публичный HTTPS-адрес панели в MEDIA_PUBLIC_BASE_URL: сервер выдаёт ссылку на один файл сроком на пять минут. Поддерживаются JPEG и PNG до 10 МиБ. Автономному экспорту без публичного сервера такой адрес недоступен.