Add storage management features and UI components
ci / build-backend (push) Successful in 1m32s
ci / build-frontend (push) Successful in 53s
ci / tests (push) Successful in 3m50s
ci / sonar (push) Successful in 4m7s

Implemented new storage endpoints in the API and updated the dependency injection to include storage-related services. Enhanced the frontend by adding storage routes and links in the admin layout, along with new types and localization for storage management. Updated documentation to reflect the new storage features and their usage in the admin interface.
This commit is contained in:
Leonid Pershin
2026-07-27 22:23:48 +03:00
parent 779f322f62
commit 8ae8eebc12
19 changed files with 1013 additions and 0 deletions
@@ -0,0 +1,72 @@
namespace TeleWave.Application.Common.Interfaces;
/// <summary>
/// Части хранилища, которые считаются отдельно. Это не «папки», а ответ на вопрос «чем занят диск»:
/// сегменты программ и сегменты заставок лежат в одном каталоге, но растут по разным причинам,
/// и администратору важно видеть их порознь.
/// </summary>
public enum StorageArea
{
/// <summary>HLS-сегменты программ — обычно это почти весь объём.</summary>
Assets = 0,
/// <summary>HLS-сегменты отрендеренных заставок.</summary>
BumperAssets = 1,
/// <summary>Исходники, оставленные после нарезки (<c>Storage:KeepOriginals</c>).</summary>
Originals = 2,
/// <summary>Автоматический inbox — то, что ещё не разобрано сканером.</summary>
Inbox = 3,
/// <summary>Ручной inbox — то, что админ забирает в шоу сам.</summary>
ManualInbox = 4,
/// <summary>Перевалочный каталог заливок: всё, что здесь залежалось, — след оборванной загрузки.</summary>
Uploads = 5,
/// <summary>Звук и фоны блоков заставок (сырые файлы, не нарезка).</summary>
BumperSources = 6,
/// <summary>Общий реестр изображений: постеры, кадры, логотипы.</summary>
Images = 7,
/// <summary>Всё прочее под корнем хранилища — обычно ноль, а если нет, то это и надо увидеть.</summary>
Other = 8,
}
/// <param name="Bytes">Объём области, байт.</param>
/// <param name="Files">Число файлов — по нему видно, что «пусто» и «много мелочи» это разные беды.</param>
public sealed record StorageAreaUsage(StorageArea Area, long Bytes, int Files);
/// <param name="RootPath">Корень хранилища — на сервере их может быть несколько, и надо видеть, о каком речь.</param>
/// <param name="VolumeTotalBytes">Размер тома целиком; 0 — файловая система не отдала метрику.</param>
/// <param name="VolumeFreeBytes">Свободно на томе. Свободное место делится со всем, что есть на диске.</param>
/// <param name="MinFreeSpaceBytes">Порог, ниже которого загрузка отклоняется; едет вместе с цифрами тома, чтобы их было с чем сравнить.</param>
/// <param name="ComputedAt">Когда посчитано: обход дерева кэшируется, и возраст цифры надо показывать.</param>
public sealed record StorageUsage(
string RootPath,
long VolumeTotalBytes,
long VolumeFreeBytes,
long StorageBytes,
int StorageFiles,
long MinFreeSpaceBytes,
IReadOnlyList<StorageAreaUsage> Areas,
DateTimeOffset ComputedAt
);
/// <summary>
/// Порт подсчёта занятого места. Размеров в БД нет и не будет: правда о диске лежит на диске,
/// а любое дублирование в таблицах разошлось бы с ней при первой же ручной правке файлов.
///
/// Обход дерева стоит дорого (сегментов — сотни тысяч), поэтому результат кэшируется реализацией,
/// а <paramref name="refresh"/> — это явная кнопка «пересчитать» в админке.
/// </summary>
public interface IStorageInspector
{
Task<StorageUsage> InspectAsync(
IReadOnlySet<Guid> generatedAssetIds,
bool refresh,
CancellationToken cancellationToken
);
}
@@ -0,0 +1,25 @@
using LiteCqrs;
using TeleWave.Application.Common.Interfaces;
namespace TeleWave.Application.Storage;
/// <summary>
/// Чем занят диск. <paramref name="Refresh"/> — явный пересчёт: обход дерева долгий, поэтому
/// обычный запрос отдаёт кэш и его возраст.
/// </summary>
public sealed record GetStorageStatsQuery(bool Refresh = false) : IQuery<StorageStatsDto>;
public sealed record StorageAreaDto(StorageArea Area, long Bytes, int Files);
/// <param name="RootPath">Корень хранилища — на сервере их может быть несколько, и надо видеть, о каком речь.</param>
/// <param name="MinFreeSpaceBytes">Порог, ниже которого загрузка отклоняется (<c>Storage:MinFreeSpaceBytes</c>).</param>
public sealed record StorageStatsDto(
string RootPath,
long VolumeTotalBytes,
long VolumeFreeBytes,
long StorageBytes,
int StorageFiles,
long MinFreeSpaceBytes,
IReadOnlyList<StorageAreaDto> Areas,
DateTimeOffset ComputedAt
);
@@ -0,0 +1,42 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Media;
namespace TeleWave.Application.Storage;
public sealed class GetStorageStatsQueryHandler(
IAppDbContext dbContext,
IStorageInspector inspector
) : IQueryHandler<GetStorageStatsQuery, StorageStatsDto>
{
public async Task<StorageStatsDto> Handle(
GetStorageStatsQuery query,
CancellationToken cancellationToken
)
{
// Сегменты программ и заставок лежат в одном каталоге; отличить их можно только по базе.
var generated = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => a.Source == MediaSource.Generated)
.Select(a => a.Id)
.ToListAsync(cancellationToken);
var usage = await inspector.InspectAsync(
generated.ToHashSet(),
query.Refresh,
cancellationToken
);
return new StorageStatsDto(
usage.RootPath,
usage.VolumeTotalBytes,
usage.VolumeFreeBytes,
usage.StorageBytes,
usage.StorageFiles,
usage.MinFreeSpaceBytes,
usage.Areas.Select(a => new StorageAreaDto(a.Area, a.Bytes, a.Files)).ToList(),
usage.ComputedAt
);
}
}