From cd827588a169cd49143bc98d92a96097221b465b Mon Sep 17 00:00:00 2001 From: PascalMarchesseault <97350299+PascalMarchesseault@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:18:58 -0500 Subject: [PATCH] CreatePost - Upload files, thumbnail, external link --- .../Contents/Handlers/CreateContent.cs | 129 ++++++++---------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/src/Web/Features/Contents/Handlers/CreateContent.cs b/src/Web/Features/Contents/Handlers/CreateContent.cs index a8ca937..5905ff6 100644 --- a/src/Web/Features/Contents/Handlers/CreateContent.cs +++ b/src/Web/Features/Contents/Handlers/CreateContent.cs @@ -4,6 +4,7 @@ using Hutopy.Web.Common.BlobStorage; using Hutopy.Web.Common.Security; using Hutopy.Web.Features.Contents.Data; using Hutopy.Web.Features.Contents.Handlers.Models; +using Microsoft.EntityFrameworkCore; namespace Hutopy.Web.Features.Contents.Handlers; @@ -14,7 +15,7 @@ public record PostContentRequest( string Title, string Description, IFormFileCollection? Files, - IFormFile? Thumbnail, // Nouveau champ pour le thumbnail + IFormFile? Thumbnail, string[]? ExternalUrls); [PublicAPI] @@ -39,11 +40,15 @@ public sealed class PostContentRequestValidator : Validator .NotEmpty().WithMessage("You should specify a valid/not empty Description"); RuleForEach(r => r.ExternalUrls) - .NotEmpty().WithMessage("External URL cannot be empty") - .Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute)).WithMessage("External URL is not valid"); + .Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute) && + (url.StartsWith("http://") || url.StartsWith("https://"))) + .WithMessage("External URL must be a valid HTTP/HTTPS URL"); 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"); + + } } @@ -56,23 +61,22 @@ public sealed class PostContent( { Post("/api/contents"); Options(o => o.WithTags("Contents")); - AllowFileUploads(); + AllowFileUploads(); } - public override async Task HandleAsync( - PostContentRequest req, - CancellationToken ct) + public override async Task HandleAsync(PostContentRequest req, CancellationToken ct) { var urls = new ConcurrentBag(); string? thumbnailUrl = null; - // Traitement des fichiers uploadés - if (req.Files is not null) + using var transaction = await context.Database.BeginTransactionAsync(ct); + + try { - await Parallel.ForEachAsync( - req.Files, - ct, - async (file, ict) => + + if (req.Files is not null) + { + await Parallel.ForEachAsync(req.Files, ct, async (file, ict) => { try { @@ -81,32 +85,35 @@ public sealed class PostContent( } 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( - new Content + + if (req.ExternalUrls is not null) + { + 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, CreatedBy = User.GetUserId(), @@ -114,33 +121,19 @@ public sealed class PostContent( Description = req.Description, Urls = urls.IsEmpty ? null : urls.ToArray(), ThumbnailUrl = thumbnailUrl, - }, - ct); + }, ct); - await context.SaveChangesAsync(ct); + await context.SaveChangesAsync(ct); + await transaction.CommitAsync(ct); - // Récupérer le contenu pour le retour - var content = await context - .Contents - .Select(c => new ContentModel - { - Id = c.Id, - CreatedBy = c.CreatedBy, - 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); + await SendOkAsync(new { Message = "Content published successfully!" }, ct); + } + catch (Exception ex) + { + await transaction.RollbackAsync(ct); + Logger.LogError("Transaction failed: {Message}", ex.Message); + throw; + } } private async Task SaveFileAsync( @@ -148,21 +141,19 @@ public sealed class PostContent( Guid contentId, IFormFile file, 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 + ? $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/thumbnail-{file.FileName}" + : $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}"; - // Téléverse le fichier - var url = await blobStorage.UploadFileAsync( + + return await blobStorage.UploadFileAsync( ContainerNames.Creators, blobName, file.OpenReadStream(), file.ContentType, ct: ct); - - return url; } }