From 86c9fb51faa164d1319e5b75865771ee32feeb53 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Tue, 9 Jul 2024 23:45:51 -0400 Subject: [PATCH] Logs for blob storage and some improvements --- Directory.Packages.props | 1 + src/Application/Application.csproj | 1 + src/Application/Common/Models/Result.cs | 5 + src/Application/Users/Commands/CreateUser.cs | 9 +- .../Commands/UpdateCurrentUserCommand.cs | 13 +-- .../Users/Commands/UploadBannerPicture.cs | 29 +++-- .../Users/Commands/UploadProfilePicture.cs | 12 ++- .../Users/Commands/UploadWebsiteIcon.cs | 12 ++- .../AzureBlob/AzureBlobStorageService.cs | 102 +++++++++++++----- src/Web/Endpoints/UpdateMyUser.cs | 8 +- src/Web/Endpoints/Users.cs | 10 +- 11 files changed, 133 insertions(+), 69 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3c60acd..f6e9876 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 34570d3..996edb9 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Application/Common/Models/Result.cs b/src/Application/Common/Models/Result.cs index 294e2ed..8c30b4a 100644 --- a/src/Application/Common/Models/Result.cs +++ b/src/Application/Common/Models/Result.cs @@ -31,6 +31,11 @@ public class Result( { return Value ?? default(T)!; } + + public string GetErrorsAsString() + { + return Errors.Length == 0 ? string.Empty : string.Join(", ", Errors); + } public static Result Success(T value) { diff --git a/src/Application/Users/Commands/CreateUser.cs b/src/Application/Users/Commands/CreateUser.cs index 54077eb..703066c 100644 --- a/src/Application/Users/Commands/CreateUser.cs +++ b/src/Application/Users/Commands/CreateUser.cs @@ -1,7 +1,8 @@ using Hutopy.Application.Common.Interfaces; +using Microsoft.AspNetCore.Http; namespace Hutopy.Application.Users.Commands; -public record CreateUserCommand : IRequest +public record CreateUserCommand : IRequest { public required string FirstName { get; init; } public required string LastName { get; init; } @@ -10,7 +11,7 @@ public record CreateUserCommand : IRequest public required string Password { get; init; } } -public class CreateUserCommandHandler : IRequestHandler +public class CreateUserCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IIdentityService _identityService; @@ -21,7 +22,7 @@ public class CreateUserCommandHandler : IRequestHandler _identityService = identityService; } - public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { await _identityService.CreateUserAsync(request.EmailAddress, request.UserName, request.FirstName, request.LastName, request.Password); @@ -29,6 +30,6 @@ public class CreateUserCommandHandler : IRequestHandler await _context.SaveChangesAsync(cancellationToken); - return new Guid(user?.Id ?? string.Empty); + return Results.Ok(new Guid(user?.Id ?? string.Empty)); } } diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index 7d55961..682cc3e 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -2,10 +2,11 @@ using System.ComponentModel.DataAnnotations.Schema; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; using Hutopy.Application.Users.Models; +using Microsoft.AspNetCore.Http; namespace Hutopy.Application.Users.Commands; -public class UpdateCurrentUserCommand : IRequest +public class UpdateCurrentUserCommand : IRequest { public required string FirstName { get; init; } public required string LastName { get; init; } @@ -32,13 +33,13 @@ public class UpdateCurrentUserCommand : IRequest } public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService, IMapper mapper) : - IRequestHandler + IRequestHandler { - public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) + public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) { var identityUser = await identityService.GetCurrentUserAsync(); - if (identityUser?.Id is null) return string.Empty; + if (identityUser?.Id is null) return Results.Problem("Current user not found."); var userModel = mapper.Map(request); userModel.Id = identityUser.Id; @@ -46,8 +47,8 @@ public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIde var result = await identityService.UpdateCurrentUserAsync(userModel); await context.SaveChangesAsync(cancellationToken); - - return result.GetValueOrDefault(); + + return result.Succeeded ? Results.Ok(result.GetValueOrDefault()) : Results.Problem(result.GetErrorsAsString()); } } diff --git a/src/Application/Users/Commands/UploadBannerPicture.cs b/src/Application/Users/Commands/UploadBannerPicture.cs index e99f28a..96728b8 100644 --- a/src/Application/Users/Commands/UploadBannerPicture.cs +++ b/src/Application/Users/Commands/UploadBannerPicture.cs @@ -1,37 +1,50 @@ using Hutopy.Application.AzureBlobStorage.Constants; using Hutopy.Application.Common.Interfaces; +using Microsoft.AspNetCore.Http; namespace Hutopy.Application.Users.Commands; /// /// Upload a banner picture. If the user has the url already, set the BannerPictureUrl in the user only without upload. /// -public class UploadBannerPictureCommand : IRequest +public class UploadBannerPictureCommand : IRequest { public required Stream BannerPicture { get; init; } public string BannerPictureUrl { get; init; } = string.Empty; } -public class UploadBannerPictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +public class UploadBannerPictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler { - public async Task Handle(UploadBannerPictureCommand request, CancellationToken cancellationToken) + public async Task Handle(UploadBannerPictureCommand request, CancellationToken cancellationToken) { + // If an url to the picture is provided, use it right away and don't upload anything. if (!string.IsNullOrEmpty(request.BannerPictureUrl)) { await identityService.UpdateCurrentUserBannerPictureUrlAsync(request.BannerPictureUrl); - return request.BannerPictureUrl; + return Results.Ok(request.BannerPictureUrl); } var identityUser = await identityService.GetCurrentUserAsync(); var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}"; - - var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture, ContentTypes.ImagePng); - await identityService.UpdateCurrentUserBannerPictureUrlAsync(url); + try + { + var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture, ContentTypes.ImagePng); + + await identityService.UpdateCurrentUserBannerPictureUrlAsync(url); - return url; + return Results.Ok(url); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(ex.Message); + } + catch (Exception) + { + return Results.StatusCode(StatusCodes.Status500InternalServerError); + } } } diff --git a/src/Application/Users/Commands/UploadProfilePicture.cs b/src/Application/Users/Commands/UploadProfilePicture.cs index ae15d31..daefbe7 100644 --- a/src/Application/Users/Commands/UploadProfilePicture.cs +++ b/src/Application/Users/Commands/UploadProfilePicture.cs @@ -1,25 +1,27 @@ using Hutopy.Application.AzureBlobStorage.Constants; using Hutopy.Application.Common.Interfaces; +using Microsoft.AspNetCore.Http; namespace Hutopy.Application.Users.Commands; /// /// Upload a profile picture. If the user has the url already, set the ProfilePictureUrl in the user only without upload. /// -public class UploadProfilePictureCommand : IRequest +public class UploadProfilePictureCommand : IRequest { public required Stream ProfilePicture { get; init; } public string ProfilePictureUrl { get; init; } = string.Empty; } -public class UploadProfilePictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +public class UploadProfilePictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler { - public async Task Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken) + public async Task Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken) { + // If an url to the picture is provided, use it right away and don't upload anything. if (!string.IsNullOrEmpty(request.ProfilePictureUrl)) { await identityService.UpdateCurrentUserProfilePictureUrlAsync(request.ProfilePictureUrl); - return request.ProfilePictureUrl; + return Results.Ok(request.ProfilePictureUrl); } var identityUser = await identityService.GetCurrentUserAsync(); @@ -31,7 +33,7 @@ public class UploadProfilePictureCommandHandler(IIdentityService identityService await identityService.UpdateCurrentUserProfilePictureUrlAsync(url); - return url; + return Results.Ok(url); } } diff --git a/src/Application/Users/Commands/UploadWebsiteIcon.cs b/src/Application/Users/Commands/UploadWebsiteIcon.cs index 31f3f5f..2c18756 100644 --- a/src/Application/Users/Commands/UploadWebsiteIcon.cs +++ b/src/Application/Users/Commands/UploadWebsiteIcon.cs @@ -1,26 +1,28 @@ using Hutopy.Application.AzureBlobStorage.Constants; using Hutopy.Application.Common.Interfaces; +using Microsoft.AspNetCore.Http; namespace Hutopy.Application.Users.Commands; /// /// Upload a website icon. If the user has the url already, set the WebsitePictureUrl in the user only without upload. /// -public class UploadWebsiteIconCommand : IRequest +public class UploadWebsiteIconCommand : IRequest { public required Stream WebsiteIcon { get; init; } public string WebsitePictureUrl { get; init; } = string.Empty; } -public class UploadWebsiteIconCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +public class UploadWebsiteIconCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler { - public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken) + public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken) { + // If an url to the picture is provided, use it right away and don't upload anything. if (!string.IsNullOrEmpty(request.WebsitePictureUrl)) { await identityService.UpdateCurrentUserWebsiteIconUrlAsync(request.WebsitePictureUrl); - return request.WebsitePictureUrl; + return Results.Ok(request.WebsitePictureUrl); } var identityUser = await identityService.GetCurrentUserAsync(); @@ -32,7 +34,7 @@ public class UploadWebsiteIconCommandHandler(IIdentityService identityService, I await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url); - return url; + return Results.Ok(request.WebsitePictureUrl); } } diff --git a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs index 18d7856..275b5a3 100644 --- a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs +++ b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs @@ -1,22 +1,27 @@ using System; using System.IO; using System.Threading.Tasks; +using Azure; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Hutopy.Application.Common.Interfaces; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; namespace Hutopy.Infrastructure.AzureBlob; public class AzureBlobStorageService : IAzureBlobStorageService { private readonly BlobServiceClient _blobServiceClient; + private readonly ILogger _logger; + private readonly long _maxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes - public AzureBlobStorageService(IConfiguration configuration) + + public AzureBlobStorageService(IConfiguration configuration, ILogger logger) { + _logger = logger; var connectionString = configuration["Azure-Blob-Connection-String"] ?? ""; _blobServiceClient = new BlobServiceClient(connectionString); - } /// @@ -29,29 +34,65 @@ public class AzureBlobStorageService : IAzureBlobStorageService /// public async Task UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType) { - // Get a reference to a container - var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); - - // Create the container if it does not exist - await containerClient.CreateIfNotExistsAsync(); + // 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; - // Get a reference to a blob - var blobClient = containerClient.GetBlobClient(blobName); - - // Define the BlobHttpHeaders to include the content type - var blobHttpHeaders = new BlobHttpHeaders + // Check if the file size exceeds the maximum upload size + if (memoryStream.Length > _maxUploadSize) { - ContentType = contentType - }; + _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."); + } + + try + { + // Get a reference to a container + var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); - // Upload the file - await blobClient.UploadAsync(fileStream, new BlobUploadOptions + // Create the container if it does not exist + await containerClient.CreateIfNotExistsAsync(); + + // 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 + }; + + // Upload the file + var response = await blobClient.UploadAsync(memoryStream, new BlobUploadOptions + { + HttpHeaders = blobHttpHeaders + }); + + 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} ]" + ); + + // Return the URI of the uploaded blob + return fileUri; + } + catch (RequestFailedException ex) { - HttpHeaders = blobHttpHeaders - }); - - // Return the URI of the uploaded blob - return blobClient.Uri.ToString(); + _logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}"); + throw new Exception("Error uploading file to Azure Blob Storage", ex); + } + catch (Exception ex) + { + _logger.LogError($"Blob storage: An error occurred: {ex.Message}"); + throw; + } } /// @@ -62,14 +103,14 @@ public class AzureBlobStorageService : IAzureBlobStorageService /// public async Task DownloadFileAsync(string containerName, string blobName) { - // Get a reference to a container - var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); - - // Get a reference to a blob - var blobClient = containerClient.GetBlobClient(blobName); - 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(); MemoryStream memoryStream = new(); @@ -78,9 +119,14 @@ public class AzureBlobStorageService : IAzureBlobStorageService return memoryStream; } + catch (RequestFailedException ex) + { + _logger.LogError($"Azure Storage request failed: {ex.Message}"); + throw new Exception("Error downloading file from Azure Blob Storage", ex); + } catch (Exception ex) { - // Log and handle the exception as needed + _logger.LogError($"An error occurred: {ex.Message}"); throw new ApplicationException("Error downloading file from Azure Blob Storage.", ex); } } diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs index 8f13133..b26303a 100644 --- a/src/Web/Endpoints/UpdateMyUser.cs +++ b/src/Web/Endpoints/UpdateMyUser.cs @@ -14,24 +14,24 @@ public class UpdateMyUser : EndpointGroupBase .MapPatch("/profile", UpdateCurrentUser); } - private static async Task UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command) + private static async Task UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command) { return await sender.Send(command); } - private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream stream, string url = "") + private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream 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, Stream 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, Stream stream, string url = "") { var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url}; return await sender.Send(command); diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index 39bc408..bbdeb4c 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -10,11 +10,10 @@ public class Users : EndpointGroupBase app.MapGroup(this) .MapPost(CreateUser) .MapPost(Login, "/login") - .MapPost(UploadProfilePicture, "/upload-profile-picture") .MapGet(GetUser); } - private static async Task CreateUser(ISender sender, CreateUserCommand command) + private static async Task CreateUser(ISender sender, CreateUserCommand command) { return await sender.Send(command); } @@ -28,11 +27,4 @@ public class Users : EndpointGroupBase { return await sender.Send(command); } - - private static async Task UploadProfilePicture(ISender sender, Stream stream) - { - var command = new UploadProfilePictureCommand { ProfilePicture = stream }; - - return await sender.Send(command); - } }