CreatePost - Upload files, thumbnail, external link

This commit is contained in:
PascalMarchesseault
2024-11-24 20:18:58 -05:00
parent ebf0c0da96
commit cd827588a1

View File

@@ -4,6 +4,7 @@ using Hutopy.Web.Common.BlobStorage;
using Hutopy.Web.Common.Security; using Hutopy.Web.Common.Security;
using Hutopy.Web.Features.Contents.Data; using Hutopy.Web.Features.Contents.Data;
using Hutopy.Web.Features.Contents.Handlers.Models; using Hutopy.Web.Features.Contents.Handlers.Models;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Features.Contents.Handlers; namespace Hutopy.Web.Features.Contents.Handlers;
@@ -14,7 +15,7 @@ public record PostContentRequest(
string Title, string Title,
string Description, string Description,
IFormFileCollection? Files, IFormFileCollection? Files,
IFormFile? Thumbnail, // Nouveau champ pour le thumbnail IFormFile? Thumbnail,
string[]? ExternalUrls); string[]? ExternalUrls);
[PublicAPI] [PublicAPI]
@@ -39,11 +40,15 @@ public sealed class PostContentRequestValidator : Validator<PostContentRequest>
.NotEmpty().WithMessage("You should specify a valid/not empty Description"); .NotEmpty().WithMessage("You should specify a valid/not empty Description");
RuleForEach(r => r.ExternalUrls) RuleForEach(r => r.ExternalUrls)
.NotEmpty().WithMessage("External URL cannot be empty") .Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute) &&
.Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute)).WithMessage("External URL is not valid"); (url.StartsWith("http://") || url.StartsWith("https://")))
.WithMessage("External URL must be a valid HTTP/HTTPS URL");
RuleFor(r => r.Thumbnail) RuleFor(r => r.Thumbnail)
.Must(file => file == null || file.Length > 0).WithMessage("Thumbnail file is invalid."); .Must(file => file == null || file.ContentType.StartsWith("image/"))
.WithMessage("Thumbnail must be an image");
} }
} }
@@ -59,20 +64,19 @@ public sealed class PostContent(
AllowFileUploads(); AllowFileUploads();
} }
public override async Task HandleAsync( public override async Task HandleAsync(PostContentRequest req, CancellationToken ct)
PostContentRequest req,
CancellationToken ct)
{ {
var urls = new ConcurrentBag<string>(); var urls = new ConcurrentBag<string>();
string? thumbnailUrl = null; string? thumbnailUrl = null;
// Traitement des fichiers uploadés using var transaction = await context.Database.BeginTransactionAsync(ct);
if (req.Files is not null)
try
{ {
await Parallel.ForEachAsync(
req.Files, if (req.Files is not null)
ct, {
async (file, ict) => await Parallel.ForEachAsync(req.Files, ct, async (file, ict) =>
{ {
try try
{ {
@@ -81,32 +85,35 @@ public sealed class PostContent(
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("{ErrorMessage}", ex.Message); Logger.LogError("Failed to upload file {FileName}: {Message}", file.FileName, ex.Message);
} }
}); });
}
// Téléversement du thumbnail
if (req.Thumbnail is not null)
{
try
{
thumbnailUrl = await SaveFileAsync(
req.CreatorId,
req.Id,
req.Thumbnail,
ct,
isThumbnail: true); // Utilisation d'un chemin spécifique pour le thumbnail
} }
catch (Exception ex)
{
Logger.LogError("Error uploading thumbnail: {ErrorMessage}", ex.Message);
}
}
// Ajout à la base de données
await context.Contents.AddAsync( if (req.ExternalUrls is not null)
new Content {
foreach (var externalUrl in req.ExternalUrls.Where(url => !string.IsNullOrWhiteSpace(url)))
{
urls.Add(externalUrl);
}
}
if (req.Thumbnail is not null)
{
try
{
thumbnailUrl = await SaveFileAsync(req.CreatorId, req.Id, req.Thumbnail, ct, isThumbnail: true);
}
catch (Exception ex)
{
Logger.LogError("Error uploading thumbnail: {Message}", ex.Message);
}
}
await context.Contents.AddAsync(new Content
{ {
Id = req.Id, Id = req.Id,
CreatedBy = User.GetUserId(), CreatedBy = User.GetUserId(),
@@ -114,33 +121,19 @@ public sealed class PostContent(
Description = req.Description, Description = req.Description,
Urls = urls.IsEmpty ? null : urls.ToArray(), Urls = urls.IsEmpty ? null : urls.ToArray(),
ThumbnailUrl = thumbnailUrl, ThumbnailUrl = thumbnailUrl,
}, }, ct);
ct);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
// Récupérer le contenu pour le retour await SendOkAsync(new { Message = "Content published successfully!" }, ct);
var content = await context }
.Contents catch (Exception ex)
.Select(c => new ContentModel {
{ await transaction.RollbackAsync(ct);
Id = c.Id, Logger.LogError("Transaction failed: {Message}", ex.Message);
CreatedBy = c.CreatedBy, throw;
CreatedByName = c.Creator!.Name, }
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
DeletedAt = c.DeletedAt,
Title = c.Title,
Description = c.Description,
Urls = c.Urls,
ThumbnailUrl = c.ThumbnailUrl,
})
.SingleOrDefaultAsync(
c => c.Id == req.Id,
cancellationToken: ct);
await SendOkAsync(content, ct);
} }
private async Task<string> SaveFileAsync( private async Task<string> SaveFileAsync(
@@ -148,21 +141,19 @@ public sealed class PostContent(
Guid contentId, Guid contentId,
IFormFile file, IFormFile file,
CancellationToken ct = default, CancellationToken ct = default,
bool isThumbnail = false) // Nouveau paramètre pour indiquer si c'est un thumbnail bool isThumbnail = false)
{ {
// Détermine le chemin du blob
var blobName = isThumbnail
? $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/thumbnail-{file.FileName}" // Chemin pour le thumbnail
: $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}"; // Chemin pour les fichiers normaux
// Téléverse le fichier var blobName = isThumbnail
var url = await blobStorage.UploadFileAsync( ? $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/thumbnail-{file.FileName}"
: $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}";
return await blobStorage.UploadFileAsync(
ContainerNames.Creators, ContainerNames.Creators,
blobName, blobName,
file.OpenReadStream(), file.OpenReadStream(),
file.ContentType, file.ContentType,
ct: ct); ct: ct);
return url;
} }
} }