From bbcc7a8a33ed3537b499a628a42b3e50abca14b1 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Wed, 31 Jul 2024 17:38:58 -0400 Subject: [PATCH] Adds multiple media urls for content --- Directory.Packages.props | 2 +- .../Constants/ContainerNames.cs | 1 + .../Constants/SubDirectoryNames.cs | 2 +- .../Interfaces/IAzureBlobStorageService.cs | 4 +- .../Users/Commands/UploadBannerPicture.cs | 9 +- .../Users/Commands/UploadProfilePicture.cs | 11 +- .../Users/Commands/UploadWebsiteIcon.cs | 29 +++-- .../AzureBlob/AzureBlobStorageService.cs | 80 ++++++------ src/Infrastructure/Infrastructure.csproj | 2 +- .../ApplicationDbContextModelSnapshot.cs | 10 +- src/Web/Endpoints/UpdateMyUser.cs | 6 +- src/Web/Features/Contents/Data/Content.cs | 6 +- .../Contents/Handlers/CreateContent.cs | 114 ++++++++++++++++++ .../Handlers/GetCreatorByAlias.cs | 7 +- .../Features/Contents/Handlers/PostContent.cs | 39 ------ ..._AddMultipleMediaUrlsToContent.Designer.cs | 59 +++++++++ ...725022229_AddMultipleMediaUrlsToContent.cs | 42 +++++++ .../ContentDbContextModelSnapshot.cs | 6 +- src/Web/Web.csproj | 1 + 19 files changed, 319 insertions(+), 111 deletions(-) create mode 100644 src/Web/Features/Contents/Handlers/CreateContent.cs rename src/Web/Features/{Creators => Contents}/Handlers/GetCreatorByAlias.cs (78%) delete mode 100644 src/Web/Features/Contents/Handlers/PostContent.cs create mode 100644 src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs create mode 100644 src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index cad324b..42adba5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/src/Application/AzureBlobStorage/Constants/ContainerNames.cs b/src/Application/AzureBlobStorage/Constants/ContainerNames.cs index 5bab776..1f49c7e 100644 --- a/src/Application/AzureBlobStorage/Constants/ContainerNames.cs +++ b/src/Application/AzureBlobStorage/Constants/ContainerNames.cs @@ -3,4 +3,5 @@ public static class ContainerNames { public static string Users = "users"; + public static string Creators = "creators"; } diff --git a/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs b/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs index 6ee43ea..7590b74 100644 --- a/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs +++ b/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs @@ -3,5 +3,5 @@ public static class SubDirectoryNames { public static string Profile = "profile"; - public static string Posts = "posts"; + public static string Contents = "contents"; } diff --git a/src/Application/Common/Interfaces/IAzureBlobStorageService.cs b/src/Application/Common/Interfaces/IAzureBlobStorageService.cs index a397ce0..e0aa991 100644 --- a/src/Application/Common/Interfaces/IAzureBlobStorageService.cs +++ b/src/Application/Common/Interfaces/IAzureBlobStorageService.cs @@ -2,6 +2,6 @@ public interface IAzureBlobStorageService { - Task UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType); - Task DownloadFileAsync(string containerName, string blobName); + Task UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream, string contentType, CancellationToken ct = default); + Task DownloadFileAsync(string containerName, string blobName, CancellationToken ct = default); } diff --git a/src/Application/Users/Commands/UploadBannerPicture.cs b/src/Application/Users/Commands/UploadBannerPicture.cs index be7590f..63573fb 100644 --- a/src/Application/Users/Commands/UploadBannerPicture.cs +++ b/src/Application/Users/Commands/UploadBannerPicture.cs @@ -10,7 +10,7 @@ namespace Hutopy.Application.Users.Commands; /// public class UploadBannerPictureCommand : IRequest { - public required Stream BannerPicture { get; init; } + public required MemoryStream BannerPicture { get; init; } public string BannerPictureUrl { get; init; } = string.Empty; } @@ -32,7 +32,12 @@ public class UploadBannerPictureCommandHandler(IHttpContextAccessor contextAcces var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}"; - var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture, contentType); + var url = await azureBlobStorageService.UploadFileAsync( + ContainerNames.Users, + blobName, + request.BannerPicture, + contentType, + cancellationToken); await identityService.UpdateCurrentUserBannerPictureUrlAsync(url); diff --git a/src/Application/Users/Commands/UploadProfilePicture.cs b/src/Application/Users/Commands/UploadProfilePicture.cs index 2e017d2..431e014 100644 --- a/src/Application/Users/Commands/UploadProfilePicture.cs +++ b/src/Application/Users/Commands/UploadProfilePicture.cs @@ -10,13 +10,13 @@ namespace Hutopy.Application.Users.Commands; /// public class UploadProfilePictureCommand : IRequest { - public required Stream ProfilePicture { get; init; } + public required MemoryStream ProfilePicture { get; init; } public string ProfilePictureUrl { get; init; } = string.Empty; } public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAccessor, IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler { - public async Task Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken) + public async Task Handle(UploadProfilePictureCommand request, CancellationToken ct) { // If an url to the picture is provided, use it right away and don't upload anything. if (!string.IsNullOrEmpty(request.ProfilePictureUrl)) @@ -32,7 +32,12 @@ public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAcce var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}"; - var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture, contentType); + var url = await azureBlobStorageService.UploadFileAsync( + ContainerNames.Users, + blobName, + request.ProfilePicture, + contentType, + ct); await identityService.UpdateCurrentUserProfilePictureUrlAsync(url); diff --git a/src/Application/Users/Commands/UploadWebsiteIcon.cs b/src/Application/Users/Commands/UploadWebsiteIcon.cs index eaa8cd6..2e74eda 100644 --- a/src/Application/Users/Commands/UploadWebsiteIcon.cs +++ b/src/Application/Users/Commands/UploadWebsiteIcon.cs @@ -10,14 +10,17 @@ namespace Hutopy.Application.Users.Commands; /// public class UploadWebsiteIconCommand : IRequest { - public required Stream WebsiteIcon { get; init; } - + public required MemoryStream WebsiteIcon { get; init; } + public string WebsitePictureUrl { get; init; } = string.Empty; } -public class UploadWebsiteIconCommandHandler(IHttpContextAccessor contextAccessor, IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +public class UploadWebsiteIconCommandHandler( + IHttpContextAccessor contextAccessor, + IIdentityService identityService, + IAzureBlobStorageService azureBlobStorageService) : IRequestHandler { - public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken) + public async Task Handle(UploadWebsiteIconCommand request, CancellationToken ct) { // If an url to the picture is provided, use it right away and don't upload anything. if (!string.IsNullOrEmpty(request.WebsitePictureUrl)) @@ -25,19 +28,23 @@ public class UploadWebsiteIconCommandHandler(IHttpContextAccessor contextAccesso await identityService.UpdateCurrentUserWebsiteIconUrlAsync(request.WebsitePictureUrl); return Results.Ok(request.WebsitePictureUrl); } - + var contentType = contextAccessor.EnsureContentType(); - + var identityUser = await identityService.GetCurrentUserAsync(); var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); - + var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.WebsiteIcon}"; - - var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.WebsiteIcon, contentType); + + var url = await azureBlobStorageService.UploadFileAsync( + ContainerNames.Users, + blobName, + request.WebsiteIcon, + contentType, + ct); await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url); - + return Results.Ok(request.WebsitePictureUrl); } } - diff --git a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs index 8f12630..7887cef 100644 --- a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs +++ b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs @@ -11,7 +11,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService { private readonly BlobServiceClient _blobServiceClient; private readonly ILogger _logger; - private readonly long _maxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes + private readonly long _maxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes public AzureBlobStorageService(IConfiguration configuration, ILogger logger) { @@ -25,63 +25,73 @@ public class AzureBlobStorageService : IAzureBlobStorageService /// /// The blob name (path within the container, include the file name). /// The name of the container where the file is stored. - /// The stream. + /// The memory stream containing the image. /// The content type. + /// The cancellation token /// - public async Task UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType) + public async Task UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream, + string contentType, CancellationToken ct = default) { // Read the file stream into a memory stream to determine the length // WATCH FOR MEMORY USAGE USING THE MEMORY STREAM. - var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream); memoryStream.Position = 0; // Check if the file size exceeds the maximum upload size if (memoryStream.Length > _maxUploadSize) { - _logger.LogInformation($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes."); - throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes."); + _logger.LogError( + $"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes."); + throw new InvalidOperationException( + $"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes."); } - + // Validate content type if (!ContentTypes.IsAllowed(contentType, memoryStream)) { - _logger.LogInformation($"Blob storage: Unsupported file type {contentType}. Only PNG and JPEG are allowed."); + _logger.LogError( + $"Blob storage: Unsupported file type {contentType}. Only PNG and JPEG are allowed."); throw new InvalidOperationException("Unsupported file type. Only PNG and JPEG are allowed."); } - + try { // Get a reference to a container var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); // Create the container if it does not exist - await containerClient.CreateIfNotExistsAsync(); + await containerClient.CreateIfNotExistsAsync( + PublicAccessType.Blob, + cancellationToken: ct); // Get a reference to a blob var blobClient = containerClient.GetBlobClient(blobName); // Define the BlobHttpHeaders to include the content type - var blobHttpHeaders = new BlobHttpHeaders - { - ContentType = contentType - }; + var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType }; // Upload the file - var response = await blobClient.UploadAsync(memoryStream, new BlobUploadOptions - { - HttpHeaders = blobHttpHeaders - }); + var response = await blobClient.UploadAsync( + memoryStream, + new BlobUploadOptions { HttpHeaders = blobHttpHeaders }, + ct); var fileUri = blobClient.Uri.ToString(); - + _logger.LogInformation( - $"Blob storage: Status [ {response.GetRawResponse().Status.ToString()} ] " + - $"Uploaded [ {blobName} ] to the container [ {containerName} ] " + - $"with contentType [ {contentType} ] " + - $"with a length of [ {memoryStream.Length} bytes ]" + - $"with the uri [ {fileUri} ]" - ); + """ + Blob storage: Status [ {ResponseStatus} ] + Uploaded [ {BlobName} ] to the container [ {ContainerName} ] + with contentType [ {ContentType} ] + with a length of [ {StreamLength} bytes ] + with the uri [ {FileUri} ] + """, + response.GetRawResponse().Status.ToString(), + blobName, + containerName, + contentType, + memoryStream.Length, + fileUri + ); // Return the URI of the uploaded blob return fileUri; @@ -89,7 +99,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService catch (RequestFailedException ex) { _logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}"); - throw new Exception("Error uploading file to Azure Blob Storage", ex); + throw; } catch (Exception ex) { @@ -99,25 +109,27 @@ public class AzureBlobStorageService : IAzureBlobStorageService } /// - /// Download a file to microsoft azure blob storage. + /// Download a file to microsoft's azure blob storage. /// /// The blob name (path within the container). /// The name of the container where the file is stored. (users) + /// The cancellation token for the request /// - public async Task DownloadFileAsync(string containerName, string blobName) + public async Task DownloadFileAsync(string containerName, string blobName, + CancellationToken ct = default) { try { // Get a reference to a container var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); - + // Get a reference to a blob var blobClient = containerClient.GetBlobClient(blobName); // Download the blob to a stream - BlobDownloadInfo download = await blobClient.DownloadAsync(); + BlobDownloadInfo download = await blobClient.DownloadAsync(ct); MemoryStream memoryStream = new(); - await download.Content.CopyToAsync(memoryStream); + await download.Content.CopyToAsync(memoryStream, ct); memoryStream.Position = 0; // Ensure the stream is at the beginning return memoryStream; @@ -125,12 +137,12 @@ public class AzureBlobStorageService : IAzureBlobStorageService catch (RequestFailedException ex) { _logger.LogError($"Azure Storage request failed: {ex.Message}"); - throw new Exception("Error downloading file from Azure Blob Storage", ex); + throw; } catch (Exception ex) { _logger.LogError($"An error occurred: {ex.Message}"); - throw new ApplicationException("Error downloading file from Azure Blob Storage.", ex); + throw; } } } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index dd3cb64..36b3844 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -4,7 +4,7 @@ Hutopy.Infrastructure - + diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index be95cf4..8b4b9ec 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -66,7 +66,7 @@ namespace Hutopy.Infrastructure.Migrations b.HasKey("Id"); - b.ToTable("FutureCreators"); + b.ToTable("FutureCreators", (string)null); }); modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => @@ -141,7 +141,7 @@ namespace Hutopy.Infrastructure.Migrations b.HasIndex("ApplicationUserId"); - b.ToTable("UserTransactions"); + b.ToTable("UserTransactions", (string)null); }); modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => @@ -395,7 +395,7 @@ namespace Hutopy.Infrastructure.Migrations modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => { - b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.ProfileColors#Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => { b1.Property("ApplicationUserId") .HasColumnType("nvarchar(450)"); @@ -424,7 +424,7 @@ namespace Hutopy.Infrastructure.Migrations .HasForeignKey("ApplicationUserId"); }); - b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => + b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.SocialNetworks#Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => { b1.Property("ApplicationUserId") .HasColumnType("nvarchar(450)"); @@ -469,7 +469,7 @@ namespace Hutopy.Infrastructure.Migrations .HasForeignKey("ApplicationUserId"); }); - b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.StoredDataUrls#Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => { b1.Property("ApplicationUserId") .HasColumnType("nvarchar(450)"); diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs index b26303a..7399116 100644 --- a/src/Web/Endpoints/UpdateMyUser.cs +++ b/src/Web/Endpoints/UpdateMyUser.cs @@ -19,19 +19,19 @@ public class UpdateMyUser : EndpointGroupBase return await sender.Send(command); } - private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream stream, string url = "") + private static async Task UpdateCurrentUserProfilePicture(ISender sender, MemoryStream stream, string url = "") { var command = new UploadProfilePictureCommand { ProfilePicture = stream, ProfilePictureUrl = url}; return await sender.Send(command); } - private static async Task UpdateCurrentUserBannerPicture(ISender sender, Stream stream, string url = "") + private static async Task UpdateCurrentUserBannerPicture(ISender sender, MemoryStream stream, string url = "") { var command = new UploadBannerPictureCommand { BannerPicture = stream, BannerPictureUrl = url}; return await sender.Send(command); } - private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream, string url = "") + private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, MemoryStream stream, string url = "") { var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url}; return await sender.Send(command); diff --git a/src/Web/Features/Contents/Data/Content.cs b/src/Web/Features/Contents/Data/Content.cs index c5c3dec..08660bb 100644 --- a/src/Web/Features/Contents/Data/Content.cs +++ b/src/Web/Features/Contents/Data/Content.cs @@ -6,7 +6,7 @@ public class Content public Guid CreatedBy { get; init; } public DateTimeOffset CreatedAt { get; init; } - public string? Title { get; init; } - public string? Description { get; init; } - public string? Uri { get; init; } + public string Title { get; set; } + public string Description { get; set; } + public string[]? Urls { get; init; } } diff --git a/src/Web/Features/Contents/Handlers/CreateContent.cs b/src/Web/Features/Contents/Handlers/CreateContent.cs new file mode 100644 index 0000000..9f7c94a --- /dev/null +++ b/src/Web/Features/Contents/Handlers/CreateContent.cs @@ -0,0 +1,114 @@ +using System.Collections.Concurrent; +using FastEndpoints; +using FluentValidation; +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Web.Common; +using Hutopy.Web.Features.Contents.Data; + +namespace Hutopy.Web.Features.Contents.Handlers; + +public record PostContentRequest( + Guid Id, + Guid CreatorId, + string Title, + string Description, + IFormFileCollection Files); + +public sealed class PostContentRequestValidator : Validator +{ + public PostContentRequestValidator() + { + RuleFor(r => r.Id) + .NotNull().WithMessage("You should specify the Id") + .NotEmpty().WithMessage("You should specify a valid/not empty Id"); + + RuleFor(r => r.CreatorId) + .NotNull().WithMessage("You should specify the CreatorId") + .NotEmpty().WithMessage("You should specify a valid/not empty CreatorId"); + + RuleFor(r => r.Title) + .NotNull().WithMessage("You should specify the Title") + .NotEmpty().WithMessage("You should specify a valid/not empty Title"); + + RuleFor(r => r.Description) + .NotNull().WithMessage("You should specify the Description") + .NotEmpty().WithMessage("You should specify a valid/not empty Description"); + } +} + +public sealed class PostContent( + IAzureBlobStorageService blobStorage, + ContentDbContext context) + : Endpoint +{ + public override void Configure() + { + Post("/api/contents"); + Options(o => o.WithTags("Contents")); + AllowFileUploads(); + } + + public override async Task HandleAsync( + PostContentRequest req, + CancellationToken ct) + { + var urls = new ConcurrentBag(); + + await Parallel.ForEachAsync( + req.Files, + ct, + async (file, ict) => + { + try + { + var contentUrl = await SaveFileAsync( + req.CreatorId, + req.Id, + file, + ict); + + urls.Add(contentUrl); + } + catch (Exception ex) + { + Logger.LogError("{ErrorMessage}", ex.Message); + } + }); + + await context.Contents.AddAsync( + new() + { + Id = req.Id, + CreatedBy = User.GetUserId(), + Title = req.Title, + Description = req.Description, + Urls = urls.ToArray() + }, + ct); + + await context.SaveChangesAsync(ct); + + await SendOkAsync(ct); + } + + private async Task SaveFileAsync( + Guid creatorId, + Guid contentId, + IFormFile file, + CancellationToken ct = default) + { + var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream, ct); + + // TODO: I would like us to use ContainerNames.Creators but it seems we are missing configurations @jbourdon + var url = await blobStorage.UploadFileAsync( + ContainerNames.Users, + $"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}", + memoryStream, + file.ContentType, + ct: ct); + + return url; + } +} diff --git a/src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs b/src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs similarity index 78% rename from src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs rename to src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs index 0c798e9..d5277e2 100644 --- a/src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs +++ b/src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs @@ -3,11 +3,11 @@ using FluentValidation; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; -namespace Hutopy.Web.Features.Creators.Handlers; +namespace Hutopy.Web.Features.Contents.Handlers; public sealed class GetCreatorByAliasRequest { - public string CreatorAlias { get; set; } + public string CreatorAlias { get; init; } } public sealed class GetCreatorByAliasRequestValidator @@ -16,7 +16,8 @@ public sealed class GetCreatorByAliasRequestValidator public GetCreatorByAliasRequestValidator() { RuleFor(r => r.CreatorAlias) - .NotNull().WithMessage("You must specify a creator-alias"); + .NotNull().WithMessage("You should specify the CreatorAlias") + .NotEmpty().WithMessage("You should specify a valid/not empty CreatorAlias"); } } diff --git a/src/Web/Features/Contents/Handlers/PostContent.cs b/src/Web/Features/Contents/Handlers/PostContent.cs deleted file mode 100644 index 71bebd5..0000000 --- a/src/Web/Features/Contents/Handlers/PostContent.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FastEndpoints; -using Hutopy.Web.Common; -using Hutopy.Web.Features.Contents.Data; - -namespace Hutopy.Web.Features.Contents.Handlers; - -public record struct PostContentRequest( - string? Title, - string? Description, - string? Uri); - -public class PostContent( - ContentDbContext context) - : Endpoint -{ - public override void Configure() - { - Post("/api/contents"); - Options( o => o.WithTags("Contents")); - } - - public override async Task HandleAsync( - PostContentRequest req, - CancellationToken ct) - { - await context.Contents.AddAsync( - new Content - { - Id = GuidHelper.GenerateUuidV7(), - CreatedBy = User.GetUserId(), - Title = req.Title, - Description = req.Description, - Uri = req.Uri - }, - ct); - - await context.SaveChangesAsync(ct); - } -} diff --git a/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs new file mode 100644 index 0000000..6971bc3 --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs @@ -0,0 +1,59 @@ +// +using System; +using Hutopy.Web.Features.Contents.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Migrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20240725022229_AddMultipleMediaUrlsToContent")] + partial class AddMultipleMediaUrlsToContent + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Content") + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Urls") + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.ToTable("Contents", "Content"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs new file mode 100644 index 0000000..74051db --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Migrations +{ + /// + public partial class AddMultipleMediaUrlsToContent : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Uri", + schema: "Content", + table: "Contents"); + + migrationBuilder.AddColumn( + name: "Urls", + schema: "Content", + table: "Contents", + type: "text[]", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Urls", + schema: "Content", + table: "Contents"); + + migrationBuilder.AddColumn( + name: "Uri", + schema: "Content", + table: "Contents", + type: "text", + nullable: true); + } + } +} diff --git a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs index e01f3a9..0bff6dd 100644 --- a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs +++ b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs @@ -23,7 +23,7 @@ namespace Hutopy.Web.Contents.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b => + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -43,8 +43,8 @@ namespace Hutopy.Web.Contents.Migrations b.Property("Title") .HasColumnType("text"); - b.Property("Uri") - .HasColumnType("text"); + b.Property("Urls") + .HasColumnType("text[]"); b.HasKey("Id"); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index ead4845..ba72aba 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -12,6 +12,7 @@ +