Предусловия в file storage для хранения записей звонков
Чек-лист для разработчика file storage (talent.microservices.filestorage): что нужно завести, чтобы
messenger мог сохранять записи звонков/конференций (jibri → ingest → file storage). Пока этих справочных
данных нет — ingest-эндпоинт записи завершится ошибкой, и CallRecordingEntity не дойдёт до Available.
Дом сущности записи и пайплайн — call-recording.md, recording-pipeline.md. Здесь — только предусловия в чужом сервисе.
Зачем
Загрузка файла в file storage требует, чтобы существовали:
- категория файлов (
FileCategoryEntity) с кодомCALL_RECORDINGS— байты записей нельзя класть в общуюMEDIA/CHAT_ATTACHMENTS, у записей своя политика хранения/доступа и свой путь в бакете; - типы контента (
FileContentTypeEntity) для видео/аудио контейнеров записей (video/mp4,video/webm,video/x-matroska, на будущееaudio/mpeg) — сейчас в справочнике только архивы/документы/картинки (id 1..19), видео нет, иUploadFileRequest.FileContentTypeIdне на что указать.
Категории уникальны по Code, типы контента — по Id (smallint) и по паре MIME/расширение. Новые записи
идут строго следующими свободными идентификаторами, без «дыр» и переиспользования занятых id.
Порядок шагов ниже — рабочий: константы → сиды → миграция → проверка.
Шаг 1. Код категории CALL_RECORDINGS
Файл: Talent.FileStorage.Constants/FileCategoryCodeConstants.cs (актуальный источник кодов). Добавить
свойство рядом с существующими (Media, ChatAttachments, ...):
/// <summary>
/// Записи звонков и конференций (Jitsi/jibri)
/// </summary>
public static string CallRecordings { get; } = "CALL_RECORDINGS";
Второй файл Talent.FileStorage.Api.HttpClients/FileCategoryCodeConstants.cs помечен [Obsolete] («Используйте
Talent.FileStorage.Constants...») — дублировать в него не нужно; новый код использует только версию из
Constants. Если по каким-то причинам deprecated-копия ещё в ходу у потребителей, добавьте туда такое же
свойство, но целевое состояние — единый источник в Constants.
Шаг 2. Сид категории FileCategoryEntity
Файл: Talent.FileStorage.DataBase/Seeders/FileCategoryEntitySeeder.cs. Добавить элемент в массив Entities
по образцу существующих (например, CHAT_ATTACHMENTS):
new FileCategoryEntity()
{
Id = Guid.Parse("A1E7C4D2-0F3B-4C9A-9E11-7C2D5B8F0A64"), // новый уникальный Guid, сгенерировать свой
Name = "Записи звонков",
Code = "CALL_RECORDINGS",
CreatedAt = EntitySeederConstants.DefaultCreatedAt,
Description = "Записи звонков и конференций мессенджера (Jitsi/jibri)",
DefaultBucketId = BucketEntitySeeder.DefaultId, // тот же засиженный бакет 7BEDBFF3-...-1C3762851946
Path = "CallRecordings",
},
Обязательные поля берутся из FileCategoryEntity (Talent.FileStorage.DataBase/Entities/FileCategoryEntity.cs):
Id, Name, Code, Path, DefaultBucketId, CreatedAt (+ опциональный Description). Path определяет
префикс объектов записей внутри бакета. DefaultBucketId — существующий бакет BucketEntitySeeder.DefaultId
(Talent.FileStorage.DataBase/Seeders/BucketEntitySeeder.cs); отдельный бакет под записи заводить не обязательно.
Новый Guid выше — placeholder: сгенерируйте собственный и зафиксируйте одно и то же значение в сиде и в
миграции (Шаг 4), иначе идемпотентность сида/миграции сломается.
Шаг 3. Типы контента для видео/аудио
Занятые id — 1..19 (Gif = 19). Следующие свободные — начиная с 20.
3.1. Enum группы
Файл: Talent.FileStorage.DataBase/Enums/FileContentTypeGroup.cs. Группы Video нет — добавить в конец
(значения сохраняются как int, поэтому только в хвост, без сдвига существующих):
public enum FileContentTypeGroup
{
None, // 0
Images, // 1
Archives, // 2
Documents, // 3
Application, // 4
Video // 5 ← новое
// Audio // 6 ← опционально, если .mp3 выделять в отдельную группу
}
audio/mpeg (.mp3) семантически — аудио; допустимо положить его в Video (медиа-контейнер записи) либо завести
Audio = 6. Рекомендация: Video для всех четырёх сейчас, Audio — когда появится реальный audio-only сценарий.
3.2. Константы типов контента
Файл: Talent.FileStorage.Constants/FileContentTypeConstants.cs. Добавить свойства после Gif (Id = 19) и
включить их в приватный массив AllContentTypes (иначе GetContentTypeInfoBy* их не найдёт):
/// <summary> video/mp4 </summary>
public static FileContentTypeInfo Mp4 { get; } = new() { Id = 20, MimeType = "video/mp4", FileTypeExtensions = ".mp4" };
/// <summary> video/webm </summary>
public static FileContentTypeInfo Webm { get; } = new() { Id = 21, MimeType = "video/webm", FileTypeExtensions = ".webm" };
/// <summary> video/x-matroska </summary>
public static FileContentTypeInfo Mkv { get; } = new() { Id = 22, MimeType = "video/x-matroska", FileTypeExtensions = ".mkv" };
/// <summary> audio/mpeg (на будущее — audio-only записи) </summary>
public static FileContentTypeInfo Mp3 { get; } = new() { Id = 23, MimeType = "audio/mpeg", FileTypeExtensions = ".mp3" };
private static readonly FileContentTypeInfo[] AllContentTypes = new[]
{
Default, Pdf, /* ... */ Webp, Gif,
Mp4, Webm, Mkv, Mp3, // ← добавить
};
3.3. Сид типов контента
Файл: Talent.FileStorage.DataBase/Seeders/FileContentTypeEntitySeeder.cs. Добавить записи в массив Entities
после Gif (Id = 19), группа Video:
new FileContentTypeEntity()
{
Id = 20,
FileTypeName = "video/mp4",
FileTypeExtensions = ".mp4",
CreatedAt = EntitySeederConstants.DefaultCreatedAt,
Group = FileContentTypeGroup.Video
},
new FileContentTypeEntity()
{
Id = 21,
FileTypeName = "video/webm",
FileTypeExtensions = ".webm",
CreatedAt = EntitySeederConstants.DefaultCreatedAt,
Group = FileContentTypeGroup.Video
},
new FileContentTypeEntity()
{
Id = 22,
FileTypeName = "video/x-matroska",
FileTypeExtensions = ".mkv",
CreatedAt = EntitySeederConstants.DefaultCreatedAt,
Group = FileContentTypeGroup.Video
},
new FileContentTypeEntity()
{
Id = 23,
FileTypeName = "audio/mpeg",
FileTypeExtensions = ".mp3",
CreatedAt = EntitySeederConstants.DefaultCreatedAt,
Group = FileContentTypeGroup.Video // либо FileContentTypeGroup.Audio
},
FileTypeName — это MIME-тип (см. существующие записи: "image/png" и т.п.), Group пишется как int.
Шаг 4. EF-миграция
После правки сидов сгенерировать миграцию (схема БД — TalentFileStorage):
dotnet ef migrations add AddCallRecordingsCategoryAndVideoContentTypes \
--project Talent.FileStorage.DataBase \
--startup-project Talent.FileStorage.Migrator
(startup-проект — тот, что применяет миграции: Talent.FileStorage.Migrator; при иной конфигурации — Talent.FileStorage.Api.)
Что попадёт в Up() (образцы — Migrations/20240816080839_AddedNewCategories.cs для категории и
Migrations/20251119085927_AddedFileContentTypeEntity.cs для типов контента):
InsertDataвTalentFileStorage.FileCategoryEntity— одна строкаCALL_RECORDINGS(колонкиId, Name, Code, Path, Description, DefaultBucketId, CreatedAt);InsertDataвTalentFileStorage.FileContentTypeEntity— четыре строки (id 20..23,Group = 5);- обратный
Down()— соответствующиеDeleteDataпоId(Guid категории и id 20..23).
Проверить, что сгенерированный InsertData использует тот же Guid категории, что и сид (Шаг 2). Применение —
штатным прогоном мигратора при деплое (Talent.FileStorage.Migrator), после существующих миграций; порядок
определяется таймстампом в имени файла миграции.
Шаг 5 (рекомендация). Стрим-загрузка крупных записей
Клиент SDK IFilesService.UploadFile (Talent.FileStorage.Api.HttpClients/Services/Files/IFilesService.cs)
принимает UploadFileRequest.FileContent типа MemoryStream — то есть весь файл держится в RAM. Записи
конференций — это десятки/сотни МБ; при нескольких параллельных выгрузках это риск OOM на стороне messenger.
Два варианта (не взаимоисключающие):
messengerстримит напрямую в multipart-эндпоинт.POST /api/files/upload-file/form(Talent.FileStorage.Api/Features/FilesController.cs, методUploadFileFromForm) принимаетIFormFileи сконфигурирован под стрим/ограниченную память:[RequestFormLimits(BufferBody = false, MemoryBufferThreshold = 128MB, MultipartBodyLengthLimit = 1GB)],[RequestSizeLimit(1GB)],[DisableRequestTimeout]. То естьmessengerпроксирует байты записи multipart-потоком, минуяMemoryStreamSDK-клиента. Плюс — работает уже сейчас; минус — не типобезопасный SDK-контракт, ручной multipart.- file storage добавляет
Stream-перегрузку. Ввести вIFilesServiceметод, принимающийStream(а неMemoryStream), и пробрасывать поток до провайдера без полной буферизации в память. Плюс — чистый SDK-контракт для потребителей; минус — работа на стороне file storage.
Риск, если не делать ничего: под нагрузкой (несколько одновременно завершившихся записей) буферизация в
MemoryStream даёт всплеск RAM пропорционально суммарному размеру файлов.
Проверка: как этим пользуется ingest в messenger
Внутренний эндпоинт messenger POST /internal/call-recordings/{sessionId}/ingest
(../api/internal/) после приёма байтов от jibri вызывает загрузку в file storage:
var result = await filesService.UploadFile(
new IFilesService.UploadFileRequest
{
TenantId = tenantId,
CategoryCode = FileCategoryCodeConstants.CallRecordings, // "CALL_RECORDINGS" (Шаг 1)
FileName = $"call-{sessionId}-{DateTime.UtcNow:yyyyMMddHHmmss}.mp4",
FileContentTypeId = FileContentTypeConstants.Mp4.Id, // 20 (Шаг 3); .webm → 21, .mkv → 22
FileContent = content, // MemoryStream (или см. Шаг 5)
Comment = null,
IsDraft = false,
},
cancellationToken);
CategoryCodeобязан существовать (Шаги 1–2),FileContentTypeId— существовать в справочнике (Шаг 3), иначе загрузка вернётResult.Failи запись не станетAvailable.- Успех →
UploadFileRequestResult.FileId. ЭтотFileIdmessengerсохраняет вCallRecordingEntity(Uploading → Available, вместе сSizeBytes/DurationSeconds/Format). - Объектный ключ в хранилище —
{CategoryId}/{FileId}(в бакете категорииCALL_RECORDINGS). - Скачивание позже — CDN presigned GET (
IFilesService.GetDownloadFileUrl→FileDownloadLink = {CdnUrl}/{FilePath}), ссылка живёт ~1 час; отдаётся клиентам через recordings-эндпоинтыmessenger(GET /recordings/{id}/download).
Порядок скачивания и download-роуты — см. ../api/recordings/; формат ключа/пресайна — jitsi-deployment.md и доменный call-recording.md.
Обратные ссылки
Автоссылки: где используется этот документ.