141 lines
6.4 KiB
C#
141 lines
6.4 KiB
C#
using Microsoft.Extensions.Options;
|
||
|
||
namespace TeleWave.Infrastructure.Media;
|
||
|
||
/// <summary>
|
||
/// Единая точка резолва путей хранилища + защита от path traversal. Любой путь, собранный из
|
||
/// внешних данных (имя загруженного файла, имя из inbox/), проверяется на нахождение внутри корня.
|
||
/// </summary>
|
||
public sealed class MediaPathResolver
|
||
{
|
||
private readonly string _root;
|
||
|
||
public MediaPathResolver(IOptions<StorageOptions> options)
|
||
{
|
||
_root = Path.GetFullPath(options.Value.RootPath);
|
||
InboxDir = Path.Combine(_root, "inbox");
|
||
UploadsDir = Path.Combine(_root, "uploads");
|
||
OriginalsDir = Path.Combine(_root, "originals");
|
||
AssetsDir = Path.Combine(_root, "assets");
|
||
BumpersDir = Path.Combine(_root, "bumpers");
|
||
MetadataDir = Path.Combine(_root, "metadata");
|
||
ImagesDir = Path.Combine(_root, "images");
|
||
}
|
||
|
||
public string InboxDir { get; }
|
||
public string UploadsDir { get; }
|
||
public string OriginalsDir { get; }
|
||
public string AssetsDir { get; }
|
||
|
||
/// <summary>Сырые файлы шаблонов заставок (фон/музыка) по каналам — не режутся на HLS.</summary>
|
||
public string BumpersDir { get; }
|
||
|
||
/// <summary>Картинки метаданных (постеры/кадры), скачанные локально.</summary>
|
||
public string MetadataDir { get; }
|
||
|
||
/// <summary>Общий реестр изображений (галерея): файлы images/{imageId}{ext}.</summary>
|
||
public string ImagesDir { get; }
|
||
|
||
public void EnsureDirectories()
|
||
{
|
||
Directory.CreateDirectory(InboxDir);
|
||
Directory.CreateDirectory(UploadsDir);
|
||
Directory.CreateDirectory(OriginalsDir);
|
||
Directory.CreateDirectory(AssetsDir);
|
||
Directory.CreateDirectory(BumpersDir);
|
||
Directory.CreateDirectory(MetadataDir);
|
||
Directory.CreateDirectory(ImagesDir);
|
||
}
|
||
|
||
public string MetadataShowDir(Guid showId) =>
|
||
EnsureWithinRoot(Path.Combine(MetadataDir, "shows", showId.ToString("N")));
|
||
|
||
/// <summary>Абсолютный путь к файлу постера шоу (extension — с точкой).</summary>
|
||
public string MetadataShowPosterPath(Guid showId, string extension) =>
|
||
EnsureWithinRoot(
|
||
Path.Combine(MetadataDir, "shows", showId.ToString("N"), "poster" + extension)
|
||
);
|
||
|
||
/// <summary>Относительный путь постера от корня (для хранения в БД и отдачи).</summary>
|
||
public string MetadataShowPosterRelative(Guid showId, string extension) =>
|
||
$"metadata/shows/{showId:N}/poster{extension}";
|
||
|
||
public string MetadataEpisodeDir(Guid episodeId) =>
|
||
EnsureWithinRoot(Path.Combine(MetadataDir, "episodes", episodeId.ToString("N")));
|
||
|
||
public string MetadataEpisodeStillPath(Guid episodeId, string extension) =>
|
||
EnsureWithinRoot(
|
||
Path.Combine(MetadataDir, "episodes", episodeId.ToString("N"), "still" + extension)
|
||
);
|
||
|
||
public string MetadataEpisodeStillRelative(Guid episodeId, string extension) =>
|
||
$"metadata/episodes/{episodeId:N}/still{extension}";
|
||
|
||
/// <summary>Абсолютный путь по относительному (с защитой от traversal) или null, если вне корня.</summary>
|
||
public string? ResolveRelative(string relativePath)
|
||
{
|
||
try
|
||
{
|
||
return EnsureWithinRoot(Path.Combine(_root, relativePath));
|
||
}
|
||
catch (UnauthorizedAccessException)
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public string BumperTemplateDir(Guid templateId) =>
|
||
EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N")));
|
||
|
||
/// <summary>Путь к файлу блока заставки (kind — «audio»/«background», extension — с точкой).</summary>
|
||
public string BumperTemplateFilePath(Guid templateId, string kind, string extension) =>
|
||
EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N"), kind + extension));
|
||
|
||
/// <summary>Путь к файлу изображения общего реестра (extension — с точкой).</summary>
|
||
public string ImagePath(Guid imageId, string extension) =>
|
||
EnsureWithinRoot(Path.Combine(ImagesDir, imageId.ToString("N") + extension));
|
||
|
||
public string OriginalPath(Guid assetId, string extension) =>
|
||
EnsureWithinRoot(Path.Combine(OriginalsDir, assetId.ToString("N") + extension));
|
||
|
||
public string AssetDir(Guid assetId) =>
|
||
EnsureWithinRoot(Path.Combine(AssetsDir, assetId.ToString("N")));
|
||
|
||
public string AssetRelativePath(Guid assetId) => $"assets/{assetId:N}";
|
||
|
||
/// <summary>Путь к файлу сегмента внутри каталога ассета (имя файла проверяется на traversal).</summary>
|
||
public string SegmentPath(Guid assetId, string fileName)
|
||
{
|
||
var assetDir = AssetDir(assetId);
|
||
return EnsureWithin(assetDir, Path.Combine(assetDir, fileName));
|
||
}
|
||
|
||
/// <summary>Резолвит имя файла внутри uploads/ (токен загрузки), проверяя выход за пределы каталога.</summary>
|
||
public string UploadPath(string token) =>
|
||
EnsureWithin(UploadsDir, Path.Combine(UploadsDir, token));
|
||
|
||
/// <summary>Резолвит имя файла внутри inbox/, проверяя выход за пределы каталога.</summary>
|
||
public string InboxPath(string fileName) =>
|
||
EnsureWithin(InboxDir, Path.Combine(InboxDir, fileName));
|
||
|
||
private string EnsureWithinRoot(string candidate) => EnsureWithin(_root, candidate);
|
||
|
||
private static string EnsureWithin(string baseDir, string candidate)
|
||
{
|
||
var full = Path.GetFullPath(candidate);
|
||
var normalizedBase = baseDir.EndsWith(Path.DirectorySeparatorChar)
|
||
? baseDir
|
||
: baseDir + Path.DirectorySeparatorChar;
|
||
|
||
if (
|
||
!full.StartsWith(normalizedBase, StringComparison.Ordinal)
|
||
&& !string.Equals(full, baseDir, StringComparison.Ordinal)
|
||
)
|
||
throw new UnauthorizedAccessException(
|
||
$"Путь '{candidate}' выходит за пределы каталога хранилища."
|
||
);
|
||
|
||
return full;
|
||
}
|
||
}
|