Adds multiple media urls for content
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.11.0" />
|
||||
<PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" />
|
||||
<PackageVersion Include="Azure.Storage.Blobs" Version="12.21.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="FastEndpoints" Version="5.26.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
public static class ContainerNames
|
||||
{
|
||||
public static string Users = "users";
|
||||
public static string Creators = "creators";
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
public static class SubDirectoryNames
|
||||
{
|
||||
public static string Profile = "profile";
|
||||
public static string Posts = "posts";
|
||||
public static string Contents = "contents";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
public interface IAzureBlobStorageService
|
||||
{
|
||||
Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType);
|
||||
Task<MemoryStream> DownloadFileAsync(string containerName, string blobName);
|
||||
Task<string> UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream, string contentType, CancellationToken ct = default);
|
||||
Task<MemoryStream> DownloadFileAsync(string containerName, string blobName, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Hutopy.Application.Users.Commands;
|
||||
/// </summary>
|
||||
public class UploadBannerPictureCommand : IRequest<IResult>
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ namespace Hutopy.Application.Users.Commands;
|
||||
/// </summary>
|
||||
public class UploadProfilePictureCommand : IRequest<IResult>
|
||||
{
|
||||
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<UploadProfilePictureCommand, IResult>
|
||||
{
|
||||
public async Task<IResult> Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken)
|
||||
public async Task<IResult> 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);
|
||||
|
||||
|
||||
@@ -10,14 +10,17 @@ namespace Hutopy.Application.Users.Commands;
|
||||
/// </summary>
|
||||
public class UploadWebsiteIconCommand : IRequest<IResult>
|
||||
{
|
||||
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<UploadWebsiteIconCommand, IResult>
|
||||
public class UploadWebsiteIconCommandHandler(
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IIdentityService identityService,
|
||||
IAzureBlobStorageService azureBlobStorageService) : IRequestHandler<UploadWebsiteIconCommand, IResult>
|
||||
{
|
||||
public async Task<IResult> Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken)
|
||||
public async Task<IResult> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
||||
{
|
||||
private readonly BlobServiceClient _blobServiceClient;
|
||||
private readonly ILogger<AzureBlobStorageService> _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<AzureBlobStorageService> logger)
|
||||
{
|
||||
@@ -25,63 +25,73 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
||||
/// </summary>
|
||||
/// <param name="blobName">The blob name (path within the container, include the file name).</param>
|
||||
/// <param name="containerName">The name of the container where the file is stored.</param>
|
||||
/// <param name="fileStream">The stream.</param>
|
||||
/// <param name="memoryStream">The memory stream containing the image.</param>
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType)
|
||||
public async Task<string> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a file to microsoft azure blob storage.
|
||||
/// Download a file to microsoft's azure blob storage.
|
||||
/// </summary>
|
||||
/// <param name="blobName">The blob name (path within the container).</param>
|
||||
/// <param name="containerName">The name of the container where the file is stored. (users)</param>
|
||||
/// <param name="ct">The cancellation token for the request</param>
|
||||
/// <returns></returns>
|
||||
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName)
|
||||
public async Task<MemoryStream> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
|
||||
@@ -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<string>("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<string>("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<string>("ApplicationUserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
@@ -19,19 +19,19 @@ public class UpdateMyUser : EndpointGroupBase
|
||||
return await sender.Send(command);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateCurrentUserProfilePicture(ISender sender, Stream stream, string url = "")
|
||||
private static async Task<IResult> UpdateCurrentUserProfilePicture(ISender sender, MemoryStream stream, string url = "")
|
||||
{
|
||||
var command = new UploadProfilePictureCommand { ProfilePicture = stream, ProfilePictureUrl = url};
|
||||
return await sender.Send(command);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateCurrentUserBannerPicture(ISender sender, Stream stream, string url = "")
|
||||
private static async Task<IResult> UpdateCurrentUserBannerPicture(ISender sender, MemoryStream stream, string url = "")
|
||||
{
|
||||
var command = new UploadBannerPictureCommand { BannerPicture = stream, BannerPictureUrl = url};
|
||||
return await sender.Send(command);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream, string url = "")
|
||||
private static async Task<IResult> UpdateCurrentUserWebsiteIcon(ISender sender, MemoryStream stream, string url = "")
|
||||
{
|
||||
var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url};
|
||||
return await sender.Send(command);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
114
src/Web/Features/Contents/Handlers/CreateContent.cs
Normal file
114
src/Web/Features/Contents/Handlers/CreateContent.cs
Normal file
@@ -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<PostContentRequest>
|
||||
{
|
||||
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<PostContentRequest>
|
||||
{
|
||||
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<string>();
|
||||
|
||||
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<string> 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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PostContentRequest>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
59
src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs
generated
Normal file
59
src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string[]>("Urls")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Contents", "Content");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMultipleMediaUrlsToContent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Uri",
|
||||
schema: "Content",
|
||||
table: "Contents");
|
||||
|
||||
migrationBuilder.AddColumn<string[]>(
|
||||
name: "Urls",
|
||||
schema: "Content",
|
||||
table: "Contents",
|
||||
type: "text[]",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Urls",
|
||||
schema: "Content",
|
||||
table: "Contents");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Uri",
|
||||
schema: "Content",
|
||||
table: "Contents",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -43,8 +43,8 @@ namespace Hutopy.Web.Contents.Migrations
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("text");
|
||||
b.Property<string[]>("Urls")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="FastEndpoints" />
|
||||
|
||||
Reference in New Issue
Block a user