Remove downloads management features: delete DownloadsEndpoints and related API logic, remove associated frontend components and routes, and update documentation to reflect the removal of the downloads directory from the application structure.
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media.Downloads;
|
||||
using TeleWave.Application.Media.Register;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
namespace TeleWave.Api.Endpoints;
|
||||
|
||||
public static class DownloadsEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapDownloadsEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var admin = app.MapGroup("/api/admin/downloads")
|
||||
.WithTags("Admin.Downloads")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", List).Produces<IReadOnlyList<DownloadFileDto>>();
|
||||
admin.MapPost("/import", Import).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> List(ISender sender, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await sender.Send(new ListDownloadsQuery(), cancellationToken);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>Импорт выбранного файла из downloads/: регистрирует ассет (копией) и ставит в обработку.</summary>
|
||||
private static async Task<IResult> Import(
|
||||
ImportDownloadBody body,
|
||||
ISender sender,
|
||||
IMediaProcessingQueue queue,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var fileName = Path.GetFileName(body.RelativePath);
|
||||
var result = await sender.Send(
|
||||
new RegisterMediaAssetCommand(body.RelativePath, MediaSource.Download, fileName),
|
||||
cancellationToken
|
||||
);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToHttpResult();
|
||||
|
||||
queue.Enqueue(result.Value);
|
||||
return Results.Created($"/api/admin/media/{result.Value}", new CreatedIdResponse(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ImportDownloadBody(string RelativePath);
|
||||
@@ -113,7 +113,6 @@ app.MapAuthEndpoints();
|
||||
app.MapRoleEndpoints();
|
||||
app.MapAdminUserEndpoints();
|
||||
app.MapMediaEndpoints();
|
||||
app.MapDownloadsEndpoints();
|
||||
app.MapShowEndpoints();
|
||||
app.MapChannelEndpoints();
|
||||
app.MapStreamingEndpoints();
|
||||
|
||||
@@ -2,9 +2,6 @@ using TeleWave.Domain.Media;
|
||||
|
||||
namespace TeleWave.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>Готовый к импорту файл в каталоге downloads/ (скачан торрент-клиентом).</summary>
|
||||
public sealed record DownloadEntry(string RelativePath, string Name, long SizeBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Порт файлового хранилища медиа. Все относительные пути резолвятся строго внутри корня
|
||||
/// (<c>Storage:RootPath</c>) — защита от path traversal лежит на реализации.
|
||||
@@ -14,9 +11,6 @@ public interface IMediaStorage
|
||||
/// <summary>Свободное место на томе хранилища, байт.</summary>
|
||||
long GetAvailableFreeSpaceBytes();
|
||||
|
||||
/// <summary>Список видеофайлов в downloads/ (рекурсивно), пригодных к импорту.</summary>
|
||||
IReadOnlyList<DownloadEntry> ListDownloads();
|
||||
|
||||
/// <summary>
|
||||
/// Стримит загружаемый контент во временный файл в <c>uploads/</c> без буферизации в память.
|
||||
/// Возвращает непрозрачный токен (имя временного файла) для последующего <see cref="PromoteToOriginalAsync"/>.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace TeleWave.Application.Media.Downloads;
|
||||
|
||||
public sealed record DownloadFileDto(string RelativePath, string Name, long SizeBytes);
|
||||
@@ -1,5 +0,0 @@
|
||||
using LiteCqrs;
|
||||
|
||||
namespace TeleWave.Application.Media.Downloads;
|
||||
|
||||
public sealed record ListDownloadsQuery : IQuery<IReadOnlyList<DownloadFileDto>>;
|
||||
@@ -1,20 +0,0 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Application.Media.Downloads;
|
||||
|
||||
public sealed class ListDownloadsQueryHandler(IMediaStorage storage)
|
||||
: IQueryHandler<ListDownloadsQuery, IReadOnlyList<DownloadFileDto>>
|
||||
{
|
||||
public Task<IReadOnlyList<DownloadFileDto>> Handle(
|
||||
ListDownloadsQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
IReadOnlyList<DownloadFileDto> files = storage
|
||||
.ListDownloads()
|
||||
.Select(d => new DownloadFileDto(d.RelativePath, d.Name, d.SizeBytes))
|
||||
.ToList();
|
||||
return Task.FromResult(files);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,4 @@ public enum MediaSource
|
||||
|
||||
/// <summary>Положен вручную в inbox/ и подобран сканером.</summary>
|
||||
Inbox,
|
||||
|
||||
/// <summary>Скачан торрент-клиентом в downloads/ и импортирован вручную (копированием).</summary>
|
||||
Download,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media;
|
||||
using TeleWave.Domain.Media;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
@@ -22,41 +21,6 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<DownloadEntry> ListDownloads()
|
||||
{
|
||||
if (!Directory.Exists(paths.DownloadsDir))
|
||||
return [];
|
||||
|
||||
var result = new List<DownloadEntry>();
|
||||
foreach (var file in Directory.EnumerateFiles(paths.DownloadsDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var name = Path.GetFileName(file);
|
||||
if (!MediaFormats.IsAllowed(name))
|
||||
continue;
|
||||
// Пропускаем недокачанное: маркеры qBittorrent (.!qB) и каталог незавершённых.
|
||||
if (name.EndsWith(".!qb", StringComparison.OrdinalIgnoreCase)
|
||||
|| file.Contains($"{Path.DirectorySeparatorChar}incomplete{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
long size;
|
||||
try
|
||||
{
|
||||
size = new FileInfo(file).Length;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (size <= 0)
|
||||
continue;
|
||||
|
||||
var relative = Path.GetRelativePath(paths.DownloadsDir, file).Replace('\\', '/');
|
||||
result.Add(new DownloadEntry(relative, name, size));
|
||||
}
|
||||
|
||||
return result.OrderBy(e => e.RelativePath, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
public async Task<string> SaveUploadAsync(
|
||||
Stream content,
|
||||
string extension,
|
||||
@@ -94,24 +58,16 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var sourcePath = source switch
|
||||
{
|
||||
MediaSource.Inbox => paths.InboxPath(sourceToken),
|
||||
MediaSource.Download => paths.DownloadPath(sourceToken),
|
||||
_ => paths.UploadPath(sourceToken),
|
||||
};
|
||||
var sourcePath = source == MediaSource.Inbox
|
||||
? paths.InboxPath(sourceToken)
|
||||
: paths.UploadPath(sourceToken);
|
||||
|
||||
if (!File.Exists(sourcePath))
|
||||
throw new FileNotFoundException("Исходный файл не найден в хранилище.", sourcePath);
|
||||
|
||||
Directory.CreateDirectory(paths.OriginalsDir);
|
||||
var destination = paths.OriginalPath(assetId, extension);
|
||||
|
||||
// Download копируем (торрент продолжает сидировать); остальное переносим.
|
||||
if (source == MediaSource.Download)
|
||||
File.Copy(sourcePath, destination, overwrite: true);
|
||||
else
|
||||
File.Move(sourcePath, destination, overwrite: true);
|
||||
File.Move(sourcePath, destination, overwrite: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,12 @@ public sealed class MediaPathResolver
|
||||
UploadsDir = Path.Combine(_root, "uploads");
|
||||
OriginalsDir = Path.Combine(_root, "originals");
|
||||
AssetsDir = Path.Combine(_root, "assets");
|
||||
DownloadsDir = Path.Combine(_root, "downloads");
|
||||
}
|
||||
|
||||
public string InboxDir { get; }
|
||||
public string UploadsDir { get; }
|
||||
public string OriginalsDir { get; }
|
||||
public string AssetsDir { get; }
|
||||
public string DownloadsDir { get; }
|
||||
|
||||
public void EnsureDirectories()
|
||||
{
|
||||
@@ -32,7 +30,6 @@ public sealed class MediaPathResolver
|
||||
Directory.CreateDirectory(UploadsDir);
|
||||
Directory.CreateDirectory(OriginalsDir);
|
||||
Directory.CreateDirectory(AssetsDir);
|
||||
Directory.CreateDirectory(DownloadsDir);
|
||||
}
|
||||
|
||||
public string OriginalPath(Guid assetId, string extension) =>
|
||||
@@ -58,10 +55,6 @@ public sealed class MediaPathResolver
|
||||
public string InboxPath(string fileName) =>
|
||||
EnsureWithin(InboxDir, Path.Combine(InboxDir, fileName));
|
||||
|
||||
/// <summary>Резолвит относительный путь внутри downloads/ (может содержать подкаталоги сезона).</summary>
|
||||
public string DownloadPath(string relativePath) =>
|
||||
EnsureWithin(DownloadsDir, Path.Combine(DownloadsDir, relativePath));
|
||||
|
||||
private string EnsureWithinRoot(string candidate) => EnsureWithin(_root, candidate);
|
||||
|
||||
private static string EnsureWithin(string baseDir, string candidate)
|
||||
|
||||
Reference in New Issue
Block a user