Adds multiple media urls for content
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
||||||
<PackageVersion Include="Azure.Identity" Version="1.11.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="coverlet.collector" Version="6.0.0" />
|
||||||
<PackageVersion Include="FastEndpoints" Version="5.26.0" />
|
<PackageVersion Include="FastEndpoints" Version="5.26.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
public static class ContainerNames
|
public static class ContainerNames
|
||||||
{
|
{
|
||||||
public static string Users = "users";
|
public static string Users = "users";
|
||||||
|
public static string Creators = "creators";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
public static class SubDirectoryNames
|
public static class SubDirectoryNames
|
||||||
{
|
{
|
||||||
public static string Profile = "profile";
|
public static string Profile = "profile";
|
||||||
public static string Posts = "posts";
|
public static string Contents = "contents";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
public interface IAzureBlobStorageService
|
public interface IAzureBlobStorageService
|
||||||
{
|
{
|
||||||
Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType);
|
Task<string> UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream, string contentType, CancellationToken ct = default);
|
||||||
Task<MemoryStream> DownloadFileAsync(string containerName, string blobName);
|
Task<MemoryStream> DownloadFileAsync(string containerName, string blobName, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace Hutopy.Application.Users.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UploadBannerPictureCommand : IRequest<IResult>
|
public class UploadBannerPictureCommand : IRequest<IResult>
|
||||||
{
|
{
|
||||||
public required Stream BannerPicture { get; init; }
|
public required MemoryStream BannerPicture { get; init; }
|
||||||
public string BannerPictureUrl { get; init; } = string.Empty;
|
public string BannerPictureUrl { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,12 @@ public class UploadBannerPictureCommandHandler(IHttpContextAccessor contextAcces
|
|||||||
|
|
||||||
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}";
|
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);
|
await identityService.UpdateCurrentUserBannerPictureUrlAsync(url);
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ namespace Hutopy.Application.Users.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UploadProfilePictureCommand : IRequest<IResult>
|
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 string ProfilePictureUrl { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAccessor, IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler<UploadProfilePictureCommand, IResult>
|
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 an url to the picture is provided, use it right away and don't upload anything.
|
||||||
if (!string.IsNullOrEmpty(request.ProfilePictureUrl))
|
if (!string.IsNullOrEmpty(request.ProfilePictureUrl))
|
||||||
@@ -32,7 +32,12 @@ public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAcce
|
|||||||
|
|
||||||
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}";
|
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);
|
await identityService.UpdateCurrentUserProfilePictureUrlAsync(url);
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ namespace Hutopy.Application.Users.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UploadWebsiteIconCommand : IRequest<IResult>
|
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 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 an url to the picture is provided, use it right away and don't upload anything.
|
||||||
if (!string.IsNullOrEmpty(request.WebsitePictureUrl))
|
if (!string.IsNullOrEmpty(request.WebsitePictureUrl))
|
||||||
@@ -25,19 +28,23 @@ public class UploadWebsiteIconCommandHandler(IHttpContextAccessor contextAccesso
|
|||||||
await identityService.UpdateCurrentUserWebsiteIconUrlAsync(request.WebsitePictureUrl);
|
await identityService.UpdateCurrentUserWebsiteIconUrlAsync(request.WebsitePictureUrl);
|
||||||
return Results.Ok(request.WebsitePictureUrl);
|
return Results.Ok(request.WebsitePictureUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentType = contextAccessor.EnsureContentType();
|
var contentType = contextAccessor.EnsureContentType();
|
||||||
|
|
||||||
var identityUser = await identityService.GetCurrentUserAsync();
|
var identityUser = await identityService.GetCurrentUserAsync();
|
||||||
var currentUserId = new Guid(identityUser?.Id ?? "").ToString();
|
var currentUserId = new Guid(identityUser?.Id ?? "").ToString();
|
||||||
|
|
||||||
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.WebsiteIcon}";
|
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);
|
await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url);
|
||||||
|
|
||||||
return Results.Ok(request.WebsitePictureUrl);
|
return Results.Ok(request.WebsitePictureUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
|||||||
{
|
{
|
||||||
private readonly BlobServiceClient _blobServiceClient;
|
private readonly BlobServiceClient _blobServiceClient;
|
||||||
private readonly ILogger<AzureBlobStorageService> _logger;
|
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)
|
public AzureBlobStorageService(IConfiguration configuration, ILogger<AzureBlobStorageService> logger)
|
||||||
{
|
{
|
||||||
@@ -25,63 +25,73 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blobName">The blob name (path within the container, include the file name).</param>
|
/// <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="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="contentType">The content type.</param>
|
||||||
|
/// <param name="ct">The cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <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
|
// Read the file stream into a memory stream to determine the length
|
||||||
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
||||||
var memoryStream = new MemoryStream();
|
|
||||||
await fileStream.CopyToAsync(memoryStream);
|
|
||||||
memoryStream.Position = 0;
|
memoryStream.Position = 0;
|
||||||
|
|
||||||
// Check if the file size exceeds the maximum upload size
|
// Check if the file size exceeds the maximum upload size
|
||||||
if (memoryStream.Length > _maxUploadSize)
|
if (memoryStream.Length > _maxUploadSize)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
|
_logger.LogError(
|
||||||
throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
|
$"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
|
// Validate content type
|
||||||
if (!ContentTypes.IsAllowed(contentType, memoryStream))
|
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.");
|
throw new InvalidOperationException("Unsupported file type. Only PNG and JPEG are allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get a reference to a container
|
// Get a reference to a container
|
||||||
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||||
|
|
||||||
// Create the container if it does not exist
|
// Create the container if it does not exist
|
||||||
await containerClient.CreateIfNotExistsAsync();
|
await containerClient.CreateIfNotExistsAsync(
|
||||||
|
PublicAccessType.Blob,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
// Get a reference to a blob
|
// Get a reference to a blob
|
||||||
var blobClient = containerClient.GetBlobClient(blobName);
|
var blobClient = containerClient.GetBlobClient(blobName);
|
||||||
|
|
||||||
// Define the BlobHttpHeaders to include the content type
|
// Define the BlobHttpHeaders to include the content type
|
||||||
var blobHttpHeaders = new BlobHttpHeaders
|
var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType };
|
||||||
{
|
|
||||||
ContentType = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
// Upload the file
|
// Upload the file
|
||||||
var response = await blobClient.UploadAsync(memoryStream, new BlobUploadOptions
|
var response = await blobClient.UploadAsync(
|
||||||
{
|
memoryStream,
|
||||||
HttpHeaders = blobHttpHeaders
|
new BlobUploadOptions { HttpHeaders = blobHttpHeaders },
|
||||||
});
|
ct);
|
||||||
|
|
||||||
var fileUri = blobClient.Uri.ToString();
|
var fileUri = blobClient.Uri.ToString();
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
$"Blob storage: Status [ {response.GetRawResponse().Status.ToString()} ] " +
|
"""
|
||||||
$"Uploaded [ {blobName} ] to the container [ {containerName} ] " +
|
Blob storage: Status [ {ResponseStatus} ]
|
||||||
$"with contentType [ {contentType} ] " +
|
Uploaded [ {BlobName} ] to the container [ {ContainerName} ]
|
||||||
$"with a length of [ {memoryStream.Length} bytes ]" +
|
with contentType [ {ContentType} ]
|
||||||
$"with the uri [ {fileUri} ]"
|
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 the URI of the uploaded blob
|
||||||
return fileUri;
|
return fileUri;
|
||||||
@@ -89,7 +99,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
|||||||
catch (RequestFailedException ex)
|
catch (RequestFailedException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}");
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -99,25 +109,27 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Download a file to microsoft azure blob storage.
|
/// Download a file to microsoft's azure blob storage.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blobName">The blob name (path within the container).</param>
|
/// <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="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>
|
/// <returns></returns>
|
||||||
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName)
|
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get a reference to a container
|
// Get a reference to a container
|
||||||
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||||
|
|
||||||
// Get a reference to a blob
|
// Get a reference to a blob
|
||||||
var blobClient = containerClient.GetBlobClient(blobName);
|
var blobClient = containerClient.GetBlobClient(blobName);
|
||||||
|
|
||||||
// Download the blob to a stream
|
// Download the blob to a stream
|
||||||
BlobDownloadInfo download = await blobClient.DownloadAsync();
|
BlobDownloadInfo download = await blobClient.DownloadAsync(ct);
|
||||||
MemoryStream memoryStream = new();
|
MemoryStream memoryStream = new();
|
||||||
await download.Content.CopyToAsync(memoryStream);
|
await download.Content.CopyToAsync(memoryStream, ct);
|
||||||
memoryStream.Position = 0; // Ensure the stream is at the beginning
|
memoryStream.Position = 0; // Ensure the stream is at the beginning
|
||||||
|
|
||||||
return memoryStream;
|
return memoryStream;
|
||||||
@@ -125,12 +137,12 @@ public class AzureBlobStorageService : IAzureBlobStorageService
|
|||||||
catch (RequestFailedException ex)
|
catch (RequestFailedException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Azure Storage request failed: {ex.Message}");
|
_logger.LogError($"Azure Storage request failed: {ex.Message}");
|
||||||
throw new Exception("Error downloading file from Azure Blob Storage", ex);
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError($"An error occurred: {ex.Message}");
|
_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>
|
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<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.Authentication.Google" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("FutureCreators");
|
b.ToTable("FutureCreators", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
|
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
|
||||||
@@ -141,7 +141,7 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ApplicationUserId");
|
b.HasIndex("ApplicationUserId");
|
||||||
|
|
||||||
b.ToTable("UserTransactions");
|
b.ToTable("UserTransactions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||||
@@ -395,7 +395,7 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
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")
|
b1.Property<string>("ApplicationUserId")
|
||||||
.HasColumnType("nvarchar(450)");
|
.HasColumnType("nvarchar(450)");
|
||||||
@@ -424,7 +424,7 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
.HasForeignKey("ApplicationUserId");
|
.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")
|
b1.Property<string>("ApplicationUserId")
|
||||||
.HasColumnType("nvarchar(450)");
|
.HasColumnType("nvarchar(450)");
|
||||||
@@ -469,7 +469,7 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
.HasForeignKey("ApplicationUserId");
|
.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")
|
b1.Property<string>("ApplicationUserId")
|
||||||
.HasColumnType("nvarchar(450)");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ public class UpdateMyUser : EndpointGroupBase
|
|||||||
return await sender.Send(command);
|
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};
|
var command = new UploadProfilePictureCommand { ProfilePicture = stream, ProfilePictureUrl = url};
|
||||||
return await sender.Send(command);
|
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};
|
var command = new UploadBannerPictureCommand { BannerPicture = stream, BannerPictureUrl = url};
|
||||||
return await sender.Send(command);
|
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};
|
var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url};
|
||||||
return await sender.Send(command);
|
return await sender.Send(command);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public class Content
|
|||||||
public Guid CreatedBy { get; init; }
|
public Guid CreatedBy { get; init; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|
||||||
public string? Title { get; init; }
|
public string Title { get; set; }
|
||||||
public string? Description { get; init; }
|
public string Description { get; set; }
|
||||||
public string? Uri { get; init; }
|
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.Interfaces;
|
||||||
using Hutopy.Application.Common.Models;
|
using Hutopy.Application.Common.Models;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Creators.Handlers;
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
public sealed class GetCreatorByAliasRequest
|
public sealed class GetCreatorByAliasRequest
|
||||||
{
|
{
|
||||||
public string CreatorAlias { get; set; }
|
public string CreatorAlias { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class GetCreatorByAliasRequestValidator
|
public sealed class GetCreatorByAliasRequestValidator
|
||||||
@@ -16,7 +16,8 @@ public sealed class GetCreatorByAliasRequestValidator
|
|||||||
public GetCreatorByAliasRequestValidator()
|
public GetCreatorByAliasRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(r => r.CreatorAlias)
|
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);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -43,8 +43,8 @@ namespace Hutopy.Web.Contents.Migrations
|
|||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
b.Property<string[]>("Urls")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.Storage.Blobs" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
<PackageReference Include="FastEndpoints" />
|
<PackageReference Include="FastEndpoints" />
|
||||||
|
|||||||
Reference in New Issue
Block a user