Refactor Dockerfile and documentation for non-root user setup. Update CLAUDE.md and README.md to clarify media storage permissions and user requirements. Modify appsettings for local development database configuration. Enhance MediaEndpoints by removing unused regex and improving file resolution logic. Update BumperPreview to use SHA-256 for asset ID generation, ensuring better security practices.
This commit is contained in:
@@ -84,9 +84,12 @@
|
||||
- Один образ: Api раздаёт REST (`/api`) и статику SPA из `wwwroot` (fallback на `index.html`).
|
||||
Один origin, база API — относительный `/api`.
|
||||
- Multi-stage Dockerfile: node (фронт) → dotnet sdk (publish + копирование в `wwwroot`) → aspnet
|
||||
runtime. Никакого gosu/root-drop и Data Protection key-ring — секретов на диске пока нет
|
||||
(JWT-ключ — обычная конфигурация, не шифруемый секрет at-rest); если появятся зашифрованные
|
||||
данные (например API-ключи внешних сервисов) — добавлять Data Protection по образцу PnvPanel.
|
||||
runtime. Контейнер работает под непривилегированным пользователем образа (`USER $APP_UID`,
|
||||
uid/gid 1654) — но **статически**, без gosu/entrypoint-скриптов и подгонки uid на старте: права
|
||||
на bind-mount хранилища выставляет оператор на хосте (см. `docs/server-storage-setup.md` § 5).
|
||||
Data Protection key-ring не заводим — секретов на диске пока нет (JWT-ключ — обычная конфигурация,
|
||||
не шифруемый секрет at-rest); появятся зашифрованные данные (например API-ключи внешних
|
||||
сервисов) — добавлять Data Protection по образцу PnvPanel.
|
||||
- docker-compose: только `app` — PostgreSQL живёт вне compose (внешний сервер/хост, адрес и
|
||||
креды — в `ConnectionStrings__Default` из `.env`; база и пользователь на нём создаются
|
||||
заранее вручную, приложение их не сидит). Миграции применяются авто на старте
|
||||
|
||||
+8
-1
@@ -5,7 +5,9 @@ FROM node:22-alpine AS frontend
|
||||
WORKDIR /app/frontend
|
||||
RUN corepack enable
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN corepack prepare pnpm@11.9.0 --activate && pnpm install --frozen-lockfile
|
||||
# --ignore-scripts: ни один пакет фронта не требует postinstall, поэтому lifecycle-скрипты зависимостей
|
||||
# в сборке не выполняются — компрометация любого пакета в дереве не даёт исполнения кода на этапе install.
|
||||
RUN corepack prepare pnpm@11.9.0 --activate && pnpm install --frozen-lockfile --ignore-scripts
|
||||
COPY frontend/ ./
|
||||
RUN pnpm build
|
||||
|
||||
@@ -34,6 +36,11 @@ ENV ASPNETCORE_ENVIRONMENT=Production \
|
||||
ASPNETCORE_HTTP_PORTS=8080
|
||||
EXPOSE 8080
|
||||
COPY --from=backend /app/publish ./
|
||||
# Работаем под непривилегированным пользователем образа aspnet (app, uid/gid 1654 — $APP_UID задан
|
||||
# в самом образе). Порт 8080 непривилегированный, установка пакетов уже позади, приложению нужна
|
||||
# только запись в /media. ВАЖНО: bind-mount пробрасывает права хоста как есть — каталог хранилища
|
||||
# должен принадлежать uid 1654, иначе контейнер не сможет писать (см. docs/server-storage-setup.md § 5).
|
||||
USER $APP_UID
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
ENTRYPOINT ["dotnet", "TeleWave.Api.dll"]
|
||||
|
||||
@@ -42,6 +42,23 @@ docker compose up -d --build
|
||||
# → http://localhost:8085 (админ — логин/пароль из .env, AdminSeed__Username/Password)
|
||||
```
|
||||
|
||||
**Права на медиахранилище.** Контейнер работает под непривилегированным пользователем (uid/gid
|
||||
`1654`), а bind-mount пробрасывает права хоста как есть — каталог хранилища должен принадлежать
|
||||
этому uid, иначе приложение стартует, но любая запись в `/media` упадёт с `Permission denied`.
|
||||
Полная процедура (включая группу, через которую вы сами кладёте файлы в `inbox/` и `manual/`) —
|
||||
[`docs/server-storage-setup.md`](docs/server-storage-setup.md), шаг 5:
|
||||
|
||||
```bash
|
||||
sudo groupadd -g 1654 telewave
|
||||
sudo usermod -aG telewave "$USER" # затем перелогиниться
|
||||
sudo chown -R 1654:1654 /srv/telewave/media
|
||||
sudo chmod -R 750 /srv/telewave/media
|
||||
sudo chmod 2775 /srv/telewave/media/inbox /srv/telewave/media/manual
|
||||
```
|
||||
|
||||
> Обновляетесь с версии, где контейнер работал под root? Это и есть вся миграция: остановите
|
||||
> контейнер (`docker compose down`), выполните команды выше, поднимайте новый образ.
|
||||
|
||||
## Локальная разработка
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using LiteCqrs;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Api.Common;
|
||||
@@ -19,8 +18,6 @@ namespace TeleWave.Api.Endpoints;
|
||||
|
||||
public static class MediaEndpoints
|
||||
{
|
||||
private static readonly Regex SegmentFileName = new(@"^seg\d{1,6}\.ts$", RegexOptions.Compiled);
|
||||
|
||||
public static IEndpointRouteBuilder MapMediaEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var admin = app.MapGroup("/api/admin/media")
|
||||
@@ -177,16 +174,7 @@ public static class MediaEndpoints
|
||||
/// <summary>Плейлист ассета: переписываем ffmpeg-index.m3u8, направляя сегменты на admin-роут.</summary>
|
||||
private static IResult PreviewPlaylist(Guid id, MediaPathResolver paths)
|
||||
{
|
||||
string indexPath;
|
||||
try
|
||||
{
|
||||
indexPath = paths.SegmentPath(id, "index.m3u8");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
if (!File.Exists(indexPath))
|
||||
if (SegmentFiles.TryResolveExisting(paths, id, "index.m3u8") is not { } indexPath)
|
||||
return Results.NotFound();
|
||||
|
||||
var baseUrl = $"/api/admin/media/{id}/preview/";
|
||||
@@ -206,19 +194,10 @@ public static class MediaEndpoints
|
||||
|
||||
private static IResult PreviewSegment(Guid id, string file, MediaPathResolver paths)
|
||||
{
|
||||
if (!SegmentFileName.IsMatch(file))
|
||||
if (!SegmentFiles.IsSegmentName(file))
|
||||
return Results.NotFound();
|
||||
|
||||
string path;
|
||||
try
|
||||
{
|
||||
path = paths.SegmentPath(id, file);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
if (!File.Exists(path))
|
||||
if (SegmentFiles.TryResolveExisting(paths, id, file) is not { } path)
|
||||
return Results.NotFound();
|
||||
|
||||
return Results.File(path, "video/mp2t", enableRangeProcessing: true);
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
// Локальная БД разработчика. В appsettings.json строки подключения нет намеренно: этот файл
|
||||
// едет в образ, и креды в нём (даже заведомо игрушечные) — это и находка сканера, и приглашение
|
||||
// однажды поправить их «на месте» вместо ConnectionStrings__Default из окружения.
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=telewave;Username=telewave;Password=telewave"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=telewave;Username=telewave;Password=telewave"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "TeleWave",
|
||||
"Audience": "TeleWave",
|
||||
|
||||
@@ -3,10 +3,16 @@ using System.Security.Cryptography;
|
||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||
|
||||
/// <summary>
|
||||
/// Детерминированный id ассета-превью для блока заставки: один и тот же на каждый повторный рендер,
|
||||
/// Детерминированный id ассета-превью для подблока заставки: один и тот же на каждый повторный рендер,
|
||||
/// поэтому предпросмотр перезаписывает единственный каталог assets/{id}, а не плодит новые.
|
||||
/// </summary>
|
||||
public static class BumperPreview
|
||||
{
|
||||
public static Guid AssetId(Guid templateId) => new(MD5.HashData(templateId.ToByteArray()));
|
||||
/// <summary>
|
||||
/// Свёртка id подблока в стабильный id превью. Не защита — просто способ получить из одного GUID
|
||||
/// другой, воспроизводимо; берём SHA-256 и первые 16 байт, чтобы в коде не оставалось вызовов
|
||||
/// сломанных хеш-функций, которые потом приходится каждый раз объяснять сканерам.
|
||||
/// </summary>
|
||||
public static Guid AssetId(Guid variantId) =>
|
||||
new(SHA256.HashData(variantId.ToByteArray()).AsSpan(0, 16));
|
||||
}
|
||||
|
||||
@@ -7,20 +7,28 @@ namespace TeleWave.Application.Library;
|
||||
/// вытаскиваются из имён по мере надобности (сводка в списке шоу, метка в расписании админки).</summary>
|
||||
public static class EpisodeName
|
||||
{
|
||||
/// <summary>Потолок на один Match. Шаблоны ниже линейные и в катастрофический бэктрекинг не уходят,
|
||||
/// но имя файла приходит извне (загрузка, inbox), а разбор идёт в цикле по всей библиотеке —
|
||||
/// страховка стоит ноль, а её отсутствие превращает любую будущую правку шаблона в риск.</summary>
|
||||
private static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
private static readonly Regex SxxEyy = new(
|
||||
@"[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})",
|
||||
RegexOptions.Compiled
|
||||
RegexOptions.Compiled,
|
||||
MatchTimeout
|
||||
);
|
||||
|
||||
private static readonly Regex NxNN = new(
|
||||
@"(?:^|[^\d])(\d{1,2})x(\d{1,3})",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase,
|
||||
MatchTimeout
|
||||
);
|
||||
|
||||
// Ведущий номер серии: «01. Название», «02 - Название», «03_Название» — сезон считаем первым.
|
||||
private static readonly Regex LeadingNumber = new(
|
||||
@"^\s*(\d{1,3})[\s._)\]-]",
|
||||
RegexOptions.Compiled
|
||||
RegexOptions.Compiled,
|
||||
MatchTimeout
|
||||
);
|
||||
|
||||
public static (int Season, int Episode)? Parse(string? name)
|
||||
|
||||
@@ -8,7 +8,7 @@ Runbook: разметка, форматирование и монтирован
|
||||
> разделов), но каждый шаг ниже содержит проверку — не пропускайте их.
|
||||
|
||||
Итог: раздел `sdb1` (ext4) смонтирован в `/srv/telewave/media`, внутри созданы рабочие каталоги,
|
||||
запись из контейнера (сейчас работает под root) возможна.
|
||||
запись из контейнера (работает под непривилегированным uid 1654) возможна.
|
||||
|
||||
---
|
||||
|
||||
@@ -106,12 +106,28 @@ findmnt /srv/telewave/media
|
||||
sudo mkdir -p /srv/telewave/media/{inbox,manual,uploads,originals,assets}
|
||||
```
|
||||
|
||||
**Владелец.** Контейнер сейчас работает под `root` (в Dockerfile нет `USER`), а bind-mount
|
||||
пробрасывает права хоста внутрь как есть. Поэтому достаточно оставить владельцем root:
|
||||
**Владелец.** Контейнер работает под непривилегированным пользователем `app` образа aspnet —
|
||||
**uid/gid 1654** (`USER $APP_UID` в Dockerfile). Bind-mount пробрасывает права хоста внутрь как
|
||||
есть, никакого маппинга uid не происходит: контейнер увидит ровно те номера, что стоят на хосте.
|
||||
Значит, владельцем хранилища должен быть 1654.
|
||||
|
||||
Имени `app` на хосте нет — заводим группу с этим gid, чтобы права были читаемыми в `ls` и чтобы
|
||||
в неё можно было добавить себя:
|
||||
|
||||
```bash
|
||||
sudo chown -R root:root /srv/telewave/media
|
||||
sudo chmod -R 755 /srv/telewave/media
|
||||
sudo groupadd -g 1654 telewave # «уже существует» — не ошибка, идём дальше
|
||||
sudo usermod -aG telewave "$USER" # чтобы класть файлы в inbox/manual под собой
|
||||
```
|
||||
|
||||
> Членство в группе применяется **только к новым сессиям**: перелогиньтесь (или `newgrp telewave`),
|
||||
> иначе следующая команда отработает, а записать файл вы всё равно не сможете. Проверка — `id`
|
||||
> должен показать `telewave` в списке групп.
|
||||
|
||||
Теперь владелец и базовые права:
|
||||
|
||||
```bash
|
||||
sudo chown -R 1654:1654 /srv/telewave/media
|
||||
sudo chmod -R 750 /srv/telewave/media
|
||||
```
|
||||
|
||||
Два каталога наполняете вы, а не приложение:
|
||||
@@ -122,17 +138,20 @@ sudo chmod -R 755 /srv/telewave/media
|
||||
(Медиа → «Из папки manual»), выбираются галочками и сразу привязываются к шоу. Импортированные
|
||||
файлы уходят из каталога так же, как из `inbox/`.
|
||||
|
||||
Чтобы класть в них файлы вручную (SFTP/rsync) под своим пользователем, откройте на запись именно
|
||||
эти каталоги вашей группе:
|
||||
Их открываем группе на запись, плюс setgid (`2` в начале режима) — чтобы файлы, положенные вами по
|
||||
SFTP/rsync, наследовали группу `telewave`, а не вашу личную, и приложение их видело:
|
||||
|
||||
```bash
|
||||
sudo chown root:$(id -gn) /srv/telewave/media/inbox /srv/telewave/media/manual
|
||||
sudo chmod 775 /srv/telewave/media/inbox /srv/telewave/media/manual
|
||||
sudo chmod 2775 /srv/telewave/media/inbox /srv/telewave/media/manual
|
||||
```
|
||||
|
||||
> При переходе контейнера на non-root пользователя (если позже добавим `USER` в Dockerfile —
|
||||
> в aspnet-образе это обычно uid `1654`), сменить владельца на этот uid:
|
||||
> `sudo chown -R 1654:1654 /srv/telewave/media` (кроме `inbox` и `manual`, оставленных вам).
|
||||
Забрать файл из каталога приложение сможет в любом случае: удаление зависит от прав на **каталог**
|
||||
(владелец — 1654), а не на сам файл, поэтому чужой umask у ваших загрузок импорту не мешает.
|
||||
|
||||
**Обновление существующей установки.** Если хранилище было заведено раньше, когда контейнер работал
|
||||
под root, — те же команды и есть вся миграция: выполните их на остановленном контейнере
|
||||
(`docker compose down`), затем поднимайте новый образ. До смены владельца приложение стартует, но
|
||||
любая запись в `/media` будет падать с `Permission denied`.
|
||||
|
||||
---
|
||||
|
||||
@@ -185,14 +204,24 @@ Docker — это важно, потому что файлы в `inbox/` кла
|
||||
|
||||
## 7. Проверка записи из контейнера
|
||||
|
||||
После добавления тома и пересборки образа убедиться, что контейнер пишет на диск:
|
||||
После добавления тома и пересборки образа убедиться, что контейнер работает под нужным uid и пишет
|
||||
на диск:
|
||||
|
||||
```bash
|
||||
docker compose exec app id # ожидается uid=1654 gid=1654
|
||||
docker compose exec app sh -c 'touch /media/.wtest && ls -l /media/.wtest && rm /media/.wtest'
|
||||
```
|
||||
|
||||
Команда должна отработать без ошибок доступа. На этом подготовка хоста завершена — дальнейшее
|
||||
(создание `MediaAsset`, нарезка ffmpeg) делает уже само приложение.
|
||||
И что вы сами можете класть файлы вручную (под своим пользователем, не через sudo):
|
||||
|
||||
```bash
|
||||
touch /srv/telewave/media/inbox/.wtest && rm /srv/telewave/media/inbox/.wtest
|
||||
```
|
||||
|
||||
Обе команды должны отработать без ошибок доступа. `Permission denied` в первой — не сделан
|
||||
`chown -R 1654:1654` из шага 5; во второй — вы не в группе `telewave` либо не перелогинились после
|
||||
`usermod`. На этом подготовка хоста завершена — дальнейшее (создание `MediaAsset`, нарезка ffmpeg)
|
||||
делает уже само приложение.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user