From ad09331a9010f40e530dc616ba4a1fdb96ac4eee Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Fri, 26 Apr 2024 17:27:13 +0200 Subject: [PATCH 1/7] calling azure ai services to remove background from an image is now possible --- Core/Core.csproj | 1 + Core/Options/AzureVisionOptions.cs | 3 +- .../Services/ImageBackgroundRemoverService.cs | 53 +++++++++++++++++++ .../Exceptions/InvalidFileFormatException.cs | 16 ++++++ .../ClientWantsToRemoveBackgroundFromImage.cs | 35 ++++++++++++ .../ImageUpload/ServerRejectsInvalidFile.cs | 5 ++ .../ServerSendsImageWithoutBackground.cs | 8 +++ .../AddServicesAndRepositoriesExtension.cs | 1 + api/GlobalExceptionHandler.cs | 2 + api/Program.cs | 4 +- api/api.csproj | 18 +++---- 11 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 Core/Services/ImageBackgroundRemoverService.cs create mode 100644 Shared/Models/Exceptions/InvalidFileFormatException.cs create mode 100644 api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs create mode 100644 api/Events/ImageUpload/ServerRejectsInvalidFile.cs create mode 100644 api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs diff --git a/Core/Core.csproj b/Core/Core.csproj index 0793d35..d7e73f7 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -12,6 +12,7 @@ + diff --git a/Core/Options/AzureVisionOptions.cs b/Core/Options/AzureVisionOptions.cs index f7ddd70..95bf8f9 100644 --- a/Core/Options/AzureVisionOptions.cs +++ b/Core/Options/AzureVisionOptions.cs @@ -2,6 +2,7 @@ namespace Core.Options; public class AzureVisionOptions { - public string Endpoint { get; set; } = null!; + public string BaseUrl { get; set; } = null!; public string Key { get; set; } = null!; + public string RemoveBackgroundEndpoint { get; set; } = null!; } \ No newline at end of file diff --git a/Core/Services/ImageBackgroundRemoverService.cs b/Core/Services/ImageBackgroundRemoverService.cs new file mode 100644 index 0000000..9e52c9c --- /dev/null +++ b/Core/Services/ImageBackgroundRemoverService.cs @@ -0,0 +1,53 @@ +using System.Net.Http.Headers; +using System.Text; +using Azure; +using Azure.AI.Vision.ImageAnalysis; +using Core.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Core.Services; + +public class ImageBackgroundRemoverService(IOptions options) +{ + public async Task RemoveBackground(IFormFile image) + { + byte[] bytes = File.ReadAllBytes("api/logo.png"); + string bytesStr = string.Join(",", bytes); + Console.WriteLine(bytesStr); + + throw new NotImplementedException(); + } + + public async Task RemoveBackground(string imageUrl) + { + var request = options.Value.BaseUrl + options.Value.RemoveBackgroundEndpoint; + + var url1 = + "https://media.istockphoto.com/id/1372896722/photo/potted-banana-plant-isolated-on-white-background.jpg?s=612x612&w=0&k=20&c=bioeNAo7zEqALK6jvyGlxeP_Y7h6j0QjuWbwY4E_eP8="; + + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", $"{options.Value.Key}"); + + var requestBody = new + { + url = url1 + }; + + // Request body + byte[] byteData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestBody)); + + HttpResponseMessage response; + using (var content = new ByteArrayContent(byteData)) + { + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + response = await client.PostAsync(request, content); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + Console.WriteLine(responseContent); + + return Encoding.UTF8.GetBytes(responseContent); + } +} \ No newline at end of file diff --git a/Shared/Models/Exceptions/InvalidFileFormatException.cs b/Shared/Models/Exceptions/InvalidFileFormatException.cs new file mode 100644 index 0000000..9e8ed80 --- /dev/null +++ b/Shared/Models/Exceptions/InvalidFileFormatException.cs @@ -0,0 +1,16 @@ +namespace Shared.Models.Exceptions; + +public class InvalidFileFormatException : AppException +{ + public InvalidFileFormatException(string message) : base(message) + { + } + + public InvalidFileFormatException(string message, Exception innerException) : base(message, innerException) + { + } + + public InvalidFileFormatException() : base("Invalid file format. Please upload a valid file.") + { + } +} \ No newline at end of file diff --git a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs new file mode 100644 index 0000000..7cb74d8 --- /dev/null +++ b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs @@ -0,0 +1,35 @@ +using api.Extensions; +using Core.Services; +using Fleck; +using lib; +using Shared.Models; +using Shared.Models.Exceptions; + +namespace api.Events.ImageUpload; + +public class ClientWantsToRemoveBackgroundFromImageDto : BaseDtoWithJwt +{ + // public required IFormFile Image { get; set; } +} + +public class ClientWantsToRemoveBackgroundFromImage(ImageBackgroundRemoverService backgroundRemoverService) : BaseEventHandler +{ + private string[] allowedExtensions = [".jpg", ".jpeg", ".png"]; + + public override async Task Handle(ClientWantsToRemoveBackgroundFromImageDto dto, IWebSocketConnection socket) + { + /*var image = dto.Image; + + if (image.Length == 0) throw new InvalidFileFormatException("File is empty."); + + var extension = Path.GetExtension(image.FileName); + if (!allowedExtensions.Contains(extension)) throw new InvalidFileFormatException("Invalid file format. Please upload a valid file.");*/ + + var removedBackgroundImage = await backgroundRemoverService.RemoveBackground("hello"); + socket.SendDto(new ServerSendsImageWithoutBackground + { + Image = removedBackgroundImage + }); + } +} + diff --git a/api/Events/ImageUpload/ServerRejectsInvalidFile.cs b/api/Events/ImageUpload/ServerRejectsInvalidFile.cs new file mode 100644 index 0000000..885d001 --- /dev/null +++ b/api/Events/ImageUpload/ServerRejectsInvalidFile.cs @@ -0,0 +1,5 @@ +using api.Events.Global; + +namespace api.Events.ImageUpload; + +public class ServerRejectsInvalidFile : ServerSendsErrorMessage; diff --git a/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs b/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs new file mode 100644 index 0000000..5d028c2 --- /dev/null +++ b/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs @@ -0,0 +1,8 @@ +using lib; + +namespace api.Events.ImageUpload; + +public class ServerSendsImageWithoutBackground : BaseDto +{ + public byte[] Image { get; set; } = null!; +} \ No newline at end of file diff --git a/api/Extensions/AddServicesAndRepositoriesExtension.cs b/api/Extensions/AddServicesAndRepositoriesExtension.cs index 57bc818..0adf97f 100644 --- a/api/Extensions/AddServicesAndRepositoriesExtension.cs +++ b/api/Extensions/AddServicesAndRepositoriesExtension.cs @@ -23,5 +23,6 @@ public static void AddServicesAndRepositories(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } } \ No newline at end of file diff --git a/api/GlobalExceptionHandler.cs b/api/GlobalExceptionHandler.cs index a2ef334..33fd119 100644 --- a/api/GlobalExceptionHandler.cs +++ b/api/GlobalExceptionHandler.cs @@ -1,5 +1,6 @@ using api.Events.Auth.Server; using api.Events.Global; +using api.Events.ImageUpload; using api.Extensions; using Fleck; using Serilog; @@ -23,6 +24,7 @@ public static void Handle(this Exception ex, IWebSocketConnection socket, string NoAccessException => new ServerRespondsNotAuthorized { Error = ex.Message }, RegisterDeviceException => new ServerRespondsRegisterDevice { Error = ex.Message }, NotAuthenticatedException => new ServerRespondsNotAuthenticated { Error = ex.Message }, + InvalidFileFormatException => new ServerRejectsInvalidFile { Error = ex.Message }, _ => new ServerSendsErrorMessage { Error = message ?? ex.Message } }; else diff --git a/api/Program.cs b/api/Program.cs index 0c8bc5b..328ce0e 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -96,8 +96,9 @@ public static async Task StartApi(string[] args) builder.Services.Configure(options => { - options.Endpoint = Environment.GetEnvironmentVariable("AZURE_VISION_ENDPOINT") ?? throw new Exception("Azure vision endpoint is missing"); + options.BaseUrl = Environment.GetEnvironmentVariable("AZURE_VISION_BASE_URL") ?? throw new Exception("Azure vision endpoint is missing"); options.Key = Environment.GetEnvironmentVariable("AZURE_VISION_KEY") ?? throw new Exception("Azure vision key is missing"); + options.RemoveBackgroundEndpoint = Environment.GetEnvironmentVariable("AZURE_VISION_REMOVE_BACKGROUND_ENDPOINT") ?? throw new Exception("Azure vision remove background endpoint is missing"); }); } @@ -142,7 +143,6 @@ await userRepository.CreateUser(new RegisterUserDto var dto = JsonSerializer.Deserialize(message, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (dto is not null && _publicEvents.Contains(dto.eventType) == false) { - Console.WriteLine("Checking JWT token"); if (dto.Jwt is null) { throw new NotAuthenticatedException("JWT token is missing. Please log in."); diff --git a/api/api.csproj b/api/api.csproj index 4c28d60..48e9492 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -8,14 +8,14 @@ - - - - - - + + + + + + - + @@ -25,8 +25,8 @@ - - + + From e1e3335a13e0fbd98151b96a0e69ab33a9569752 Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Fri, 26 Apr 2024 18:01:29 +0200 Subject: [PATCH 2/7] save image after background removal --- Core/Options/VercelBlobOptions.cs | 6 ++++ .../Services/ImageBackgroundRemoverService.cs | 34 ++++++++---------- Core/Services/MqttSubscriberService.cs | 1 - .../{Models => }/Exceptions/AppException.cs | 6 ++-- .../Exceptions/InvalidCredentialsException.cs | 2 ++ .../Exceptions/InvalidFileFormatException.cs | 2 ++ .../Exceptions/ModelValidationException.cs | 2 ++ .../Exceptions/NoAccessException.cs | 2 ++ .../Exceptions/NotAuthenticatedException.cs | 2 ++ .../Exceptions/NotFoundException.cs | 4 ++- .../Exceptions/RegisterDeviceExeption.cs | 4 ++- .../Exceptions/UserAlreadyExistsException.cs | 2 ++ .../ClientWantsToRemoveBackgroundFromImage.cs | 9 ++--- api/GlobalExceptionHandler.cs | 1 + api/Program.cs | 1 + api/logo.png | Bin 0 -> 12284 bytes 16 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 Core/Options/VercelBlobOptions.cs rename Shared/{Models => }/Exceptions/AppException.cs (54%) rename Shared/{Models => }/Exceptions/InvalidCredentialsException.cs (93%) rename Shared/{Models => }/Exceptions/InvalidFileFormatException.cs (94%) rename Shared/{Models => }/Exceptions/ModelValidationException.cs (93%) rename Shared/{Models => }/Exceptions/NoAccessException.cs (93%) rename Shared/{Models => }/Exceptions/NotAuthenticatedException.cs (93%) rename Shared/{Models => }/Exceptions/NotFoundException.cs (82%) rename Shared/{Models => }/Exceptions/RegisterDeviceExeption.cs (85%) rename Shared/{Models => }/Exceptions/UserAlreadyExistsException.cs (93%) create mode 100644 api/logo.png diff --git a/Core/Options/VercelBlobOptions.cs b/Core/Options/VercelBlobOptions.cs new file mode 100644 index 0000000..7e3c9bc --- /dev/null +++ b/Core/Options/VercelBlobOptions.cs @@ -0,0 +1,6 @@ +namespace Core.Options; + +public class VercelBlobOptions +{ + public string Token { get; set; } = null!; +} \ No newline at end of file diff --git a/Core/Services/ImageBackgroundRemoverService.cs b/Core/Services/ImageBackgroundRemoverService.cs index 9e52c9c..e6a0a08 100644 --- a/Core/Services/ImageBackgroundRemoverService.cs +++ b/Core/Services/ImageBackgroundRemoverService.cs @@ -1,38 +1,25 @@ +using System.Net; using System.Net.Http.Headers; using System.Text; -using Azure; -using Azure.AI.Vision.ImageAnalysis; using Core.Options; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Shared.Exceptions; namespace Core.Services; public class ImageBackgroundRemoverService(IOptions options) { - public async Task RemoveBackground(IFormFile image) - { - byte[] bytes = File.ReadAllBytes("api/logo.png"); - string bytesStr = string.Join(",", bytes); - Console.WriteLine(bytesStr); - - throw new NotImplementedException(); - } - public async Task RemoveBackground(string imageUrl) { var request = options.Value.BaseUrl + options.Value.RemoveBackgroundEndpoint; - - var url1 = - "https://media.istockphoto.com/id/1372896722/photo/potted-banana-plant-isolated-on-white-background.jpg?s=612x612&w=0&k=20&c=bioeNAo7zEqALK6jvyGlxeP_Y7h6j0QjuWbwY4E_eP8="; var client = new HttpClient(); client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", $"{options.Value.Key}"); var requestBody = new { - url = url1 + url = imageUrl }; // Request body @@ -44,10 +31,19 @@ public async Task RemoveBackground(string imageUrl) content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); response = await client.PostAsync(request, content); } + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new AppException("Failed to remove background from image."); + } + + // The response is image/png + var imageBytes = await response.Content.ReadAsByteArrayAsync(); + var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{Guid.NewGuid()}.png"); + await File.WriteAllBytesAsync(filePath, imageBytes); - var responseContent = await response.Content.ReadAsStringAsync(); - Console.WriteLine(responseContent); + // TODO: Save to blob storage - return Encoding.UTF8.GetBytes(responseContent); + return imageBytes; } } \ No newline at end of file diff --git a/Core/Services/MqttSubscriberService.cs b/Core/Services/MqttSubscriberService.cs index 4ec0ba1..3a24a42 100644 --- a/Core/Services/MqttSubscriberService.cs +++ b/Core/Services/MqttSubscriberService.cs @@ -1,7 +1,6 @@ using System.Text; using System.Text.Json; using Core.Options; -using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using MQTTnet; using MQTTnet.Client; diff --git a/Shared/Models/Exceptions/AppException.cs b/Shared/Exceptions/AppException.cs similarity index 54% rename from Shared/Models/Exceptions/AppException.cs rename to Shared/Exceptions/AppException.cs index 56c5a47..89960c5 100644 --- a/Shared/Models/Exceptions/AppException.cs +++ b/Shared/Exceptions/AppException.cs @@ -1,8 +1,8 @@ -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; -public abstract class AppException : Exception +public class AppException : Exception { - protected AppException(string message) : base(message) + public AppException(string message) : base(message) { } diff --git a/Shared/Models/Exceptions/InvalidCredentialsException.cs b/Shared/Exceptions/InvalidCredentialsException.cs similarity index 93% rename from Shared/Models/Exceptions/InvalidCredentialsException.cs rename to Shared/Exceptions/InvalidCredentialsException.cs index 1a20422..44f0a2c 100644 --- a/Shared/Models/Exceptions/InvalidCredentialsException.cs +++ b/Shared/Exceptions/InvalidCredentialsException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class InvalidCredentialsException : AppException diff --git a/Shared/Models/Exceptions/InvalidFileFormatException.cs b/Shared/Exceptions/InvalidFileFormatException.cs similarity index 94% rename from Shared/Models/Exceptions/InvalidFileFormatException.cs rename to Shared/Exceptions/InvalidFileFormatException.cs index 9e8ed80..fd97336 100644 --- a/Shared/Models/Exceptions/InvalidFileFormatException.cs +++ b/Shared/Exceptions/InvalidFileFormatException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class InvalidFileFormatException : AppException diff --git a/Shared/Models/Exceptions/ModelValidationException.cs b/Shared/Exceptions/ModelValidationException.cs similarity index 93% rename from Shared/Models/Exceptions/ModelValidationException.cs rename to Shared/Exceptions/ModelValidationException.cs index c2101d9..db2760a 100644 --- a/Shared/Models/Exceptions/ModelValidationException.cs +++ b/Shared/Exceptions/ModelValidationException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class ModelValidationException : AppException diff --git a/Shared/Models/Exceptions/NoAccessException.cs b/Shared/Exceptions/NoAccessException.cs similarity index 93% rename from Shared/Models/Exceptions/NoAccessException.cs rename to Shared/Exceptions/NoAccessException.cs index 318b26f..2faaf1d 100644 --- a/Shared/Models/Exceptions/NoAccessException.cs +++ b/Shared/Exceptions/NoAccessException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class NoAccessException : AppException diff --git a/Shared/Models/Exceptions/NotAuthenticatedException.cs b/Shared/Exceptions/NotAuthenticatedException.cs similarity index 93% rename from Shared/Models/Exceptions/NotAuthenticatedException.cs rename to Shared/Exceptions/NotAuthenticatedException.cs index 7b9d7dd..698c77e 100644 --- a/Shared/Models/Exceptions/NotAuthenticatedException.cs +++ b/Shared/Exceptions/NotAuthenticatedException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class NotAuthenticatedException : AppException diff --git a/Shared/Models/Exceptions/NotFoundException.cs b/Shared/Exceptions/NotFoundException.cs similarity index 82% rename from Shared/Models/Exceptions/NotFoundException.cs rename to Shared/Exceptions/NotFoundException.cs index d36df4f..4a7d5c5 100644 --- a/Shared/Models/Exceptions/NotFoundException.cs +++ b/Shared/Exceptions/NotFoundException.cs @@ -1,4 +1,6 @@ -namespace Shared.Models.Exceptions; +using Shared.Exceptions; + +namespace Shared.Models.Exceptions; public class NotFoundException: AppException { diff --git a/Shared/Models/Exceptions/RegisterDeviceExeption.cs b/Shared/Exceptions/RegisterDeviceExeption.cs similarity index 85% rename from Shared/Models/Exceptions/RegisterDeviceExeption.cs rename to Shared/Exceptions/RegisterDeviceExeption.cs index 81a9f00..c13f1a6 100644 --- a/Shared/Models/Exceptions/RegisterDeviceExeption.cs +++ b/Shared/Exceptions/RegisterDeviceExeption.cs @@ -1,4 +1,6 @@ -namespace Shared.Models.Exceptions; +using Shared.Exceptions; + +namespace Shared.Models.Exceptions; public class RegisterDeviceException: AppException { diff --git a/Shared/Models/Exceptions/UserAlreadyExistsException.cs b/Shared/Exceptions/UserAlreadyExistsException.cs similarity index 93% rename from Shared/Models/Exceptions/UserAlreadyExistsException.cs rename to Shared/Exceptions/UserAlreadyExistsException.cs index b3ee974..27abe9e 100644 --- a/Shared/Models/Exceptions/UserAlreadyExistsException.cs +++ b/Shared/Exceptions/UserAlreadyExistsException.cs @@ -1,3 +1,5 @@ +using Shared.Exceptions; + namespace Shared.Models.Exceptions; public class UserAlreadyExistsException : AppException diff --git a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs index 7cb74d8..bf97f3b 100644 --- a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs +++ b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs @@ -3,13 +3,12 @@ using Fleck; using lib; using Shared.Models; -using Shared.Models.Exceptions; namespace api.Events.ImageUpload; public class ClientWantsToRemoveBackgroundFromImageDto : BaseDtoWithJwt { - // public required IFormFile Image { get; set; } + public required string ImageUrl { get; set; } } public class ClientWantsToRemoveBackgroundFromImage(ImageBackgroundRemoverService backgroundRemoverService) : BaseEventHandler @@ -18,14 +17,12 @@ public class ClientWantsToRemoveBackgroundFromImage(ImageBackgroundRemoverServic public override async Task Handle(ClientWantsToRemoveBackgroundFromImageDto dto, IWebSocketConnection socket) { - /*var image = dto.Image; - - if (image.Length == 0) throw new InvalidFileFormatException("File is empty."); + /*if (image.Length == 0) throw new InvalidFileFormatException("File is empty."); var extension = Path.GetExtension(image.FileName); if (!allowedExtensions.Contains(extension)) throw new InvalidFileFormatException("Invalid file format. Please upload a valid file.");*/ - var removedBackgroundImage = await backgroundRemoverService.RemoveBackground("hello"); + var removedBackgroundImage = await backgroundRemoverService.RemoveBackground(dto.ImageUrl); socket.SendDto(new ServerSendsImageWithoutBackground { Image = removedBackgroundImage diff --git a/api/GlobalExceptionHandler.cs b/api/GlobalExceptionHandler.cs index 33fd119..80ee2ca 100644 --- a/api/GlobalExceptionHandler.cs +++ b/api/GlobalExceptionHandler.cs @@ -4,6 +4,7 @@ using api.Extensions; using Fleck; using Serilog; +using Shared.Exceptions; using Shared.Models.Exceptions; namespace api; diff --git a/api/Program.cs b/api/Program.cs index 328ce0e..185990f 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -72,6 +72,7 @@ public static async Task StartApi(string[] args) builder.Services.Configure(builder.Configuration.GetSection("JWT")); builder.Services.Configure(builder.Configuration.GetSection("MQTT")); builder.Services.Configure(builder.Configuration.GetSection("AzureVision")); + builder.Services.Configure(builder.Configuration.GetSection("VercelBlob")); // On ci options are stored as repository secrets if (args.Contains("ENVIRONMENT=Testing") && Environment.GetEnvironmentVariable("CI") is not null) diff --git a/api/logo.png b/api/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ac63e43657dfb2eb19bce1e4409f08654a77347 GIT binary patch literal 12284 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 Date: Wed, 1 May 2024 13:36:29 +0200 Subject: [PATCH 3/7] server can accept files from FE for background removal and send them back --- Core/Core.csproj | 1 + .../Services/ImageBackgroundRemoverService.cs | 23 ++++++------------- .../ClientWantsToRemoveBackgroundFromImage.cs | 15 ++++++------ .../ServerSendsImageWithoutBackground.cs | 2 +- api/Program.cs | 4 ++-- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/Core/Core.csproj b/Core/Core.csproj index d7e73f7..afcd884 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -13,6 +13,7 @@ + diff --git a/Core/Services/ImageBackgroundRemoverService.cs b/Core/Services/ImageBackgroundRemoverService.cs index e6a0a08..e2e354c 100644 --- a/Core/Services/ImageBackgroundRemoverService.cs +++ b/Core/Services/ImageBackgroundRemoverService.cs @@ -2,6 +2,7 @@ using System.Net.Http.Headers; using System.Text; using Core.Options; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Shared.Exceptions; @@ -10,25 +11,17 @@ namespace Core.Services; public class ImageBackgroundRemoverService(IOptions options) { - public async Task RemoveBackground(string imageUrl) + public async Task RemoveBackground(byte[] imageBytes) { var request = options.Value.BaseUrl + options.Value.RemoveBackgroundEndpoint; var client = new HttpClient(); client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", $"{options.Value.Key}"); - var requestBody = new - { - url = imageUrl - }; - - // Request body - byte[] byteData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestBody)); - HttpResponseMessage response; - using (var content = new ByteArrayContent(byteData)) + using (var content = new ByteArrayContent(imageBytes)) { - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); response = await client.PostAsync(request, content); } @@ -38,12 +31,10 @@ public async Task RemoveBackground(string imageUrl) } // The response is image/png - var imageBytes = await response.Content.ReadAsByteArrayAsync(); + var removedBgImageBytes = await response.Content.ReadAsByteArrayAsync(); var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{Guid.NewGuid()}.png"); - await File.WriteAllBytesAsync(filePath, imageBytes); - - // TODO: Save to blob storage + await File.WriteAllBytesAsync(filePath, removedBgImageBytes); - return imageBytes; + return removedBgImageBytes; } } \ No newline at end of file diff --git a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs index bf97f3b..ae713d1 100644 --- a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs +++ b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs @@ -3,29 +3,28 @@ using Fleck; using lib; using Shared.Models; +using Shared.Models.Exceptions; namespace api.Events.ImageUpload; public class ClientWantsToRemoveBackgroundFromImageDto : BaseDtoWithJwt { - public required string ImageUrl { get; set; } + public required string Base64Image { get; set; } } public class ClientWantsToRemoveBackgroundFromImage(ImageBackgroundRemoverService backgroundRemoverService) : BaseEventHandler { - private string[] allowedExtensions = [".jpg", ".jpeg", ".png"]; - public override async Task Handle(ClientWantsToRemoveBackgroundFromImageDto dto, IWebSocketConnection socket) { - /*if (image.Length == 0) throw new InvalidFileFormatException("File is empty."); + byte[] imageBytes = Convert.FromBase64String(dto.Base64Image); - var extension = Path.GetExtension(image.FileName); - if (!allowedExtensions.Contains(extension)) throw new InvalidFileFormatException("Invalid file format. Please upload a valid file.");*/ + if (imageBytes.Length == 0) throw new InvalidFileFormatException("File is empty."); - var removedBackgroundImage = await backgroundRemoverService.RemoveBackground(dto.ImageUrl); + var removedBackgroundImage = await backgroundRemoverService.RemoveBackground(imageBytes); + var removedBackgroundImageBase64 = Convert.ToBase64String(removedBackgroundImage); socket.SendDto(new ServerSendsImageWithoutBackground { - Image = removedBackgroundImage + Base64Image = removedBackgroundImageBase64 }); } } diff --git a/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs b/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs index 5d028c2..0499458 100644 --- a/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs +++ b/api/Events/ImageUpload/ServerSendsImageWithoutBackground.cs @@ -4,5 +4,5 @@ namespace api.Events.ImageUpload; public class ServerSendsImageWithoutBackground : BaseDto { - public byte[] Image { get; set; } = null!; + public required string Base64Image { get; set; } = null!; } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 185990f..910ab12 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -19,7 +19,7 @@ namespace api; public static class Startup { - private static readonly List _publicEvents = + private static readonly List PublicEvents = [ nameof(ClientWantsToLogIn), nameof(ClientWantsToLogOut), @@ -142,7 +142,7 @@ await userRepository.CreateUser(new RegisterUserDto { // Check if the message contains a JWT token and if it is valid var dto = JsonSerializer.Deserialize(message, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (dto is not null && _publicEvents.Contains(dto.eventType) == false) + if (dto is not null && PublicEvents.Contains(dto.eventType) == false) { if (dto.Jwt is null) { From 6e87d159f074265a5ecf7df9a57948d811a3d2de Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Wed, 1 May 2024 21:18:29 +0200 Subject: [PATCH 4/7] possible to save a plant through FE --- Core/Services/PlantService.cs | 5 +++-- Core/Services/RequirementService.cs | 2 +- Infrastructure/ApplicationDbContext.cs | 2 +- Infrastructure/Repositories/PlantRepository.cs | 2 +- .../Repositories/RequirementsRepository.cs | 2 +- Shared/Dtos/FromClient/Plant/CreatePlantDto.cs | 2 +- .../Requirements/UpdateRequirementDto.cs | 2 +- Shared/Models/Information/Conditions.cs | 11 ----------- Shared/Models/Information/Requirements.cs | 14 +++++++++++--- Shared/Models/Plant.cs | 2 +- Tests/PlantTests.cs | 2 +- .../PlantEvents/Client/ClientWantsToCreatePlant.cs | 4 ++-- .../PlantEvents/Server/ServerCreatesNewPlant.cs | 9 +++++++++ 13 files changed, 33 insertions(+), 26 deletions(-) delete mode 100644 Shared/Models/Information/Conditions.cs create mode 100644 api/Events/PlantEvents/Server/ServerCreatesNewPlant.cs diff --git a/Core/Services/PlantService.cs b/Core/Services/PlantService.cs index 6e97600..fc4075c 100644 --- a/Core/Services/PlantService.cs +++ b/Core/Services/PlantService.cs @@ -12,6 +12,7 @@ public class PlantService (PlantRepository plantRepository, RequirementService r private const string DefaultImageUrl = "https://www.creativefabrica.com/wp-content/uploads/2022/01/20/Animated-Plant-Graphics-23785833-1.jpg"; + // TODO: decode base 64 image and save it to blob storage public async Task CreatePlant(CreatePlantDto createPlantDto) { if (string.IsNullOrEmpty(createPlantDto.Nickname)) @@ -26,7 +27,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) UserEmail = createPlantDto.UserEmail, // CollectionId = Guid.Empty, // TODO: fix when collections are implemented Nickname = createPlantDto.Nickname, - ImageUrl = createPlantDto.ImageUrl ?? DefaultImageUrl, + ImageUrl = createPlantDto.Base64Image ?? DefaultImageUrl, DeviceId = createPlantDto.DeviceId }; @@ -35,7 +36,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) // Create requirements for the plant to crete a link between the two var requirementsDto = createPlantDto.CreateRequirementsDto; requirementsDto.PlantId = plant.PlantId; - await requirementService.CreateRequirements(requirementsDto); + plant.Requirements = await requirementService.CreateRequirements(requirementsDto); return plant; } diff --git a/Core/Services/RequirementService.cs b/Core/Services/RequirementService.cs index 89a0728..f972ba1 100644 --- a/Core/Services/RequirementService.cs +++ b/Core/Services/RequirementService.cs @@ -22,7 +22,7 @@ public async Task CreateRequirements(CreateRequirementsDto createR public async Task UpdateRequirements(UpdateRequirementDto updateRequirementDto, Guid plantId) { - var requirements = await requirementsRepository.GetRequirements(updateRequirementDto.ConditionsId); + var requirements = await requirementsRepository.GetRequirements(updateRequirementDto.RequirementsId); if (requirements is null) throw new NotFoundException("Requirements not found"); if (requirements.PlantId != plantId) throw new NoAccessException("You don't have access to this plant"); diff --git a/Infrastructure/ApplicationDbContext.cs b/Infrastructure/ApplicationDbContext.cs index 4d83f2e..98a8fa7 100644 --- a/Infrastructure/ApplicationDbContext.cs +++ b/Infrastructure/ApplicationDbContext.cs @@ -44,7 +44,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(e => e.PlantId); modelBuilder.Entity() - .HasKey(e => e.ConditionsId); + .HasKey(e => e.RequirementsId); modelBuilder.Entity() .HasKey(e => e.ConditionsId); diff --git a/Infrastructure/Repositories/PlantRepository.cs b/Infrastructure/Repositories/PlantRepository.cs index 66ac727..e0e12e4 100644 --- a/Infrastructure/Repositories/PlantRepository.cs +++ b/Infrastructure/Repositories/PlantRepository.cs @@ -66,7 +66,7 @@ public async Task GetPlantIdByDeviceIdAsync(string deviceId) return plant.PlantId; } - public async Task GetRequirementsForPlant(Guid plantId) + public async Task GetRequirementsForPlant(Guid plantId) { await using var context = await dbContextFactory.CreateDbContextAsync(); var plant = await context.Plants diff --git a/Infrastructure/Repositories/RequirementsRepository.cs b/Infrastructure/Repositories/RequirementsRepository.cs index 73d1e68..a3bee05 100644 --- a/Infrastructure/Repositories/RequirementsRepository.cs +++ b/Infrastructure/Repositories/RequirementsRepository.cs @@ -8,7 +8,7 @@ public class RequirementsRepository(IDbContextFactory dbCo public async Task GetRequirements(Guid requirementsId) { await using var context = await dbContextFactory.CreateDbContextAsync(); - return await context.Requirements.FirstOrDefaultAsync(r => r.ConditionsId == requirementsId); + return await context.Requirements.FirstOrDefaultAsync(r => r.RequirementsId == requirementsId); } public async Task CreateRequirements(Requirements requirements) diff --git a/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs b/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs index 2992fbf..0a351ec 100644 --- a/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs +++ b/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs @@ -9,6 +9,6 @@ public class CreatePlantDto public Guid? CollectionId { get; set; } public string? DeviceId { get; set; } [MaxLength(50)] public string? Nickname { get; set; } - public string? ImageUrl { get; set; } + public string? Base64Image { get; set; } public CreateRequirementsDto CreateRequirementsDto { get; set; } = null!; } \ No newline at end of file diff --git a/Shared/Dtos/FromClient/Requirements/UpdateRequirementDto.cs b/Shared/Dtos/FromClient/Requirements/UpdateRequirementDto.cs index 0f11954..2673b51 100644 --- a/Shared/Dtos/FromClient/Requirements/UpdateRequirementDto.cs +++ b/Shared/Dtos/FromClient/Requirements/UpdateRequirementDto.cs @@ -4,7 +4,7 @@ namespace Shared.Dtos.FromClient.Requirements; public class UpdateRequirementDto { - public Guid ConditionsId { get; set; } + public Guid RequirementsId { get; set; } public RequirementLevel SoilMoistureLevel { get; set; } public RequirementLevel LightLevel { get; set; } public RequirementLevel TemperatureLevel { get; set; } diff --git a/Shared/Models/Information/Conditions.cs b/Shared/Models/Information/Conditions.cs deleted file mode 100644 index 38e5848..0000000 --- a/Shared/Models/Information/Conditions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Shared.Models.Information; - -public abstract class Conditions -{ - public Guid ConditionsId { get; set; } - public Guid PlantId { get; set; } - public RequirementLevel SoilMoistureLevel { get; set; } - public RequirementLevel LightLevel { get; set; } - public RequirementLevel TemperatureLevel { get; set; } - public RequirementLevel HumidityLevel { get; set; } -} \ No newline at end of file diff --git a/Shared/Models/Information/Requirements.cs b/Shared/Models/Information/Requirements.cs index 45d90cb..3b776f3 100644 --- a/Shared/Models/Information/Requirements.cs +++ b/Shared/Models/Information/Requirements.cs @@ -2,15 +2,23 @@ namespace Shared.Models.Information; -public class Requirements : Conditions +public class Requirements { + public Guid RequirementsId { get; set; } + public Guid PlantId { get; set; } + public RequirementLevel SoilMoistureLevel { get; set; } + public RequirementLevel LightLevel { get; set; } + public RequirementLevel TemperatureLevel { get; set; } + public RequirementLevel HumidityLevel { get; set; } + public Requirements() { + } public Requirements(CreateRequirementsDto createRequirementsDto) { - ConditionsId = Guid.NewGuid(); + RequirementsId = Guid.NewGuid(); PlantId = createRequirementsDto.PlantId!.Value; // plantID should be assigned to the dto before using this constructor SoilMoistureLevel = createRequirementsDto.SoilMoistureLevel; LightLevel = createRequirementsDto.LightLevel; @@ -20,7 +28,7 @@ public Requirements(CreateRequirementsDto createRequirementsDto) public Requirements(UpdateRequirementDto updateRequirementDto, Guid plantId) { - ConditionsId = updateRequirementDto.ConditionsId; + RequirementsId = updateRequirementDto.RequirementsId; PlantId = plantId; SoilMoistureLevel = updateRequirementDto.SoilMoistureLevel; LightLevel = updateRequirementDto.LightLevel; diff --git a/Shared/Models/Plant.cs b/Shared/Models/Plant.cs index 9ba1c31..fcdc2a4 100644 --- a/Shared/Models/Plant.cs +++ b/Shared/Models/Plant.cs @@ -8,7 +8,7 @@ public class Plant public string? DeviceId { get; set; } public string UserEmail { get; set; } public Guid? CollectionId { get; set; } - public string? Nickname { get; set; } // if not provided make one up + public string Nickname { get; set; } // if not provided make one up public string ImageUrl { get; set; } = null!; public Requirements? Requirements { get; set; } public List ConditionsLogs { get; set; } = new(); diff --git a/Tests/PlantTests.cs b/Tests/PlantTests.cs index 4600636..7ef0a12 100644 --- a/Tests/PlantTests.cs +++ b/Tests/PlantTests.cs @@ -43,7 +43,7 @@ private CreatePlantDto GenerateRandomCreatePlantDto(string email) UserEmail = email, CollectionId = null, Nickname = "Nickname", - ImageUrl = "https://realurl.com", + Base64Image = "https://realurl.com", CreateRequirementsDto = new CreateRequirementsDto { SoilMoistureLevel = RequirementLevel.Low, diff --git a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs index 374a38d..c5fdb19 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs @@ -22,10 +22,10 @@ public override async Task Handle(ClientWantsToCreatePlantDto dto, IWebSocketCon { var createPlantDto = dto.CreatePlantDto; var plant = await plantService.CreatePlant(createPlantDto); - var serverSendsPlant = new ServerSendsPlant + var serverCreatesNewPlant = new ServerCreatesNewPlant { Plant = plant }; - socket.SendDto(serverSendsPlant); + socket.SendDto(serverCreatesNewPlant); } } \ No newline at end of file diff --git a/api/Events/PlantEvents/Server/ServerCreatesNewPlant.cs b/api/Events/PlantEvents/Server/ServerCreatesNewPlant.cs new file mode 100644 index 0000000..6b73f39 --- /dev/null +++ b/api/Events/PlantEvents/Server/ServerCreatesNewPlant.cs @@ -0,0 +1,9 @@ +using lib; +using Shared.Models; + +namespace api.Events.PlantEvents.Server; + +public class ServerCreatesNewPlant : BaseDto +{ + public Plant Plant { get; set; } +} \ No newline at end of file From 4de3e872931038b33ee86e02d813d8d51dc35074 Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Thu, 2 May 2024 11:00:45 +0200 Subject: [PATCH 5/7] blobs can be successfully uploaded when creating a plant --- Core/Core.csproj | 1 + Core/Options/AzureBlobStorageOptions.cs | 8 ++++ Core/Options/VercelBlobOptions.cs | 6 --- Core/Services/BlobStorageService.cs | 6 +++ .../Services/ImageBackgroundRemoverService.cs | 3 -- Core/Services/PlantService.cs | 37 +++++++++++++++---- .../AddServicesAndRepositoriesExtension.cs | 1 + api/Program.cs | 10 ++++- 8 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 Core/Options/AzureBlobStorageOptions.cs delete mode 100644 Core/Options/VercelBlobOptions.cs create mode 100644 Core/Services/BlobStorageService.cs diff --git a/Core/Core.csproj b/Core/Core.csproj index afcd884..61a8631 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -13,6 +13,7 @@ + diff --git a/Core/Options/AzureBlobStorageOptions.cs b/Core/Options/AzureBlobStorageOptions.cs new file mode 100644 index 0000000..9204969 --- /dev/null +++ b/Core/Options/AzureBlobStorageOptions.cs @@ -0,0 +1,8 @@ +namespace Core.Options; + +public class AzureBlobStorageOptions +{ + public string ConnectionString { get; set; } = null!; + public string PlantImagesContainer { get; set; } = null!; + public string UserProfileImagesContainer { get; set; } = null!; +} \ No newline at end of file diff --git a/Core/Options/VercelBlobOptions.cs b/Core/Options/VercelBlobOptions.cs deleted file mode 100644 index 7e3c9bc..0000000 --- a/Core/Options/VercelBlobOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Core.Options; - -public class VercelBlobOptions -{ - public string Token { get; set; } = null!; -} \ No newline at end of file diff --git a/Core/Services/BlobStorageService.cs b/Core/Services/BlobStorageService.cs new file mode 100644 index 0000000..3c29238 --- /dev/null +++ b/Core/Services/BlobStorageService.cs @@ -0,0 +1,6 @@ +namespace Core.Services; + +public class BlobStorageService +{ + +} \ No newline at end of file diff --git a/Core/Services/ImageBackgroundRemoverService.cs b/Core/Services/ImageBackgroundRemoverService.cs index e2e354c..f4fb8a7 100644 --- a/Core/Services/ImageBackgroundRemoverService.cs +++ b/Core/Services/ImageBackgroundRemoverService.cs @@ -32,9 +32,6 @@ public async Task RemoveBackground(byte[] imageBytes) // The response is image/png var removedBgImageBytes = await response.Content.ReadAsByteArrayAsync(); - var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{Guid.NewGuid()}.png"); - await File.WriteAllBytesAsync(filePath, removedBgImageBytes); - return removedBgImageBytes; } } \ No newline at end of file diff --git a/Core/Services/PlantService.cs b/Core/Services/PlantService.cs index fc4075c..d71688f 100644 --- a/Core/Services/PlantService.cs +++ b/Core/Services/PlantService.cs @@ -1,4 +1,9 @@ -using Infrastructure.Repositories; +using Azure.Identity; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Core.Options; +using Infrastructure.Repositories; +using Microsoft.Extensions.Options; using Shared.Dtos.FromClient.Plant; using Shared.Dtos.Plant; using Shared.Models; @@ -7,11 +12,14 @@ namespace Core.Services; -public class PlantService (PlantRepository plantRepository, RequirementService requirementService) +public class PlantService( + PlantRepository plantRepository, + RequirementService requirementService, + IOptions azureBlobStorageOptions) { private const string DefaultImageUrl = "https://www.creativefabrica.com/wp-content/uploads/2022/01/20/Animated-Plant-Graphics-23785833-1.jpg"; - + // TODO: decode base 64 image and save it to blob storage public async Task CreatePlant(CreatePlantDto createPlantDto) { @@ -19,7 +27,13 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) { createPlantDto.Nickname = GenerateRandomNickname(); } - + + string? ímageUrl = null; + if (createPlantDto.Base64Image is not null) + { + ímageUrl = await SaveImageToBlobStorage(createPlantDto.Base64Image, createPlantDto.UserEmail); + } + // Insert plant first to get the plantId var plant = new Plant { @@ -27,7 +41,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) UserEmail = createPlantDto.UserEmail, // CollectionId = Guid.Empty, // TODO: fix when collections are implemented Nickname = createPlantDto.Nickname, - ImageUrl = createPlantDto.Base64Image ?? DefaultImageUrl, + ImageUrl = ímageUrl ?? DefaultImageUrl, DeviceId = createPlantDto.DeviceId }; @@ -37,10 +51,19 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) var requirementsDto = createPlantDto.CreateRequirementsDto; requirementsDto.PlantId = plant.PlantId; plant.Requirements = await requirementService.CreateRequirements(requirementsDto); - return plant; } - + + private async Task SaveImageToBlobStorage(string base64Image, string userEmail) + { + var imageBytes = Convert.FromBase64String(base64Image); + var blobUrl = userEmail + "_" + Guid.NewGuid(); + var blobClient = new BlobClient(azureBlobStorageOptions.Value.ConnectionString, azureBlobStorageOptions.Value.PlantImagesContainer, blobUrl); + var binaryData = new BinaryData(imageBytes); + await blobClient.UploadAsync(binaryData, true); + return blobClient.Uri.ToString(); + } + public async Task GetPlantById(Guid id, string requesterEmail) { var plant = await VerifyPlantExistsAndUserHasAccess(id, requesterEmail); diff --git a/api/Extensions/AddServicesAndRepositoriesExtension.cs b/api/Extensions/AddServicesAndRepositoriesExtension.cs index 0adf97f..4af4703 100644 --- a/api/Extensions/AddServicesAndRepositoriesExtension.cs +++ b/api/Extensions/AddServicesAndRepositoriesExtension.cs @@ -24,5 +24,6 @@ public static void AddServicesAndRepositories(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 910ab12..47aab18 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -72,7 +72,8 @@ public static async Task StartApi(string[] args) builder.Services.Configure(builder.Configuration.GetSection("JWT")); builder.Services.Configure(builder.Configuration.GetSection("MQTT")); builder.Services.Configure(builder.Configuration.GetSection("AzureVision")); - builder.Services.Configure(builder.Configuration.GetSection("VercelBlob")); + builder.Services.Configure(builder.Configuration.GetSection("AzureBlob")); + // On ci options are stored as repository secrets if (args.Contains("ENVIRONMENT=Testing") && Environment.GetEnvironmentVariable("CI") is not null) @@ -101,6 +102,13 @@ public static async Task StartApi(string[] args) options.Key = Environment.GetEnvironmentVariable("AZURE_VISION_KEY") ?? throw new Exception("Azure vision key is missing"); options.RemoveBackgroundEndpoint = Environment.GetEnvironmentVariable("AZURE_VISION_REMOVE_BACKGROUND_ENDPOINT") ?? throw new Exception("Azure vision remove background endpoint is missing"); }); + + builder.Services.Configure(options => + { + options.ConnectionString = Environment.GetEnvironmentVariable("AZURE_BLOB_CONNECTION_STRING") ?? throw new Exception("Azure blob connection string is missing"); + options.PlantImagesContainer = Environment.GetEnvironmentVariable("AZURE_BLOB_PLANT_IMAGES_CONTAINERE") ?? throw new Exception("Azure blob container name is missing"); + options.UserProfileImagesContainer = Environment.GetEnvironmentVariable("AZURE_BLOB_USER_PROFILE_IMAGES_CONTAINER") ?? throw new Exception("Azure blob container name is missing"); + }); } From bf91b40dbaafa288891f7075e294de6bc0a3be5d Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Thu, 2 May 2024 11:38:10 +0200 Subject: [PATCH 6/7] blobs should be retrieved --- Core/Services/ConditionsLogsService.cs | 2 +- Core/Services/PlantService.cs | 47 +++++++++++++------ Core/Services/RequirementService.cs | 2 +- Core/Services/UserService.cs | 2 +- .../Repositories/PlantRepository.cs | 3 +- .../FromClient/Identity/RegisterUserDto.cs | 2 +- .../Dtos/FromClient/Plant/UpdatePlantDto.cs | 5 +- .../Exceptions/InvalidCredentialsException.cs | 4 +- .../Exceptions/InvalidFileFormatException.cs | 4 +- Shared/Exceptions/ModelValidationException.cs | 4 +- Shared/Exceptions/NoAccessException.cs | 4 +- .../Exceptions/NotAuthenticatedException.cs | 4 +- Shared/Exceptions/NotFoundException.cs | 4 +- Shared/Exceptions/RegisterDeviceExeption.cs | 4 +- .../Exceptions/UserAlreadyExistsException.cs | 4 +- Shared/Models/Plant.cs | 4 +- api/EventFilters/ValidateDataAnnotations.cs | 2 +- api/Events/Auth/Client/ClientWantsToLogIn.cs | 2 +- api/Events/Auth/Client/ClientWantsToSignUp.cs | 1 + .../ClientWantsToRemoveBackgroundFromImage.cs | 2 +- .../Client/ClientWantsToCreatePlant.cs | 1 - .../Client/ClientWantsToUpdatePlant.cs | 2 +- api/GlobalExceptionHandler.cs | 1 - api/Program.cs | 3 +- api/WebSocketConnectionService.cs | 2 +- 25 files changed, 57 insertions(+), 58 deletions(-) diff --git a/Core/Services/ConditionsLogsService.cs b/Core/Services/ConditionsLogsService.cs index 289e1ee..538c4c0 100644 --- a/Core/Services/ConditionsLogsService.cs +++ b/Core/Services/ConditionsLogsService.cs @@ -1,6 +1,6 @@ using Infrastructure.Repositories; using Shared.Dtos; -using Shared.Models.Exceptions; +using Shared.Exceptions; using Shared.Models.Information; namespace Core.Services; diff --git a/Core/Services/PlantService.cs b/Core/Services/PlantService.cs index d71688f..4e767fe 100644 --- a/Core/Services/PlantService.cs +++ b/Core/Services/PlantService.cs @@ -1,14 +1,10 @@ -using Azure.Identity; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs; using Core.Options; using Infrastructure.Repositories; using Microsoft.Extensions.Options; using Shared.Dtos.FromClient.Plant; -using Shared.Dtos.Plant; +using Shared.Exceptions; using Shared.Models; -using Shared.Models.Exceptions; -using Shared.Models.Information; namespace Core.Services; @@ -17,10 +13,6 @@ public class PlantService( RequirementService requirementService, IOptions azureBlobStorageOptions) { - private const string DefaultImageUrl = - "https://www.creativefabrica.com/wp-content/uploads/2022/01/20/Animated-Plant-Graphics-23785833-1.jpg"; - - // TODO: decode base 64 image and save it to blob storage public async Task CreatePlant(CreatePlantDto createPlantDto) { if (string.IsNullOrEmpty(createPlantDto.Nickname)) @@ -41,7 +33,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) UserEmail = createPlantDto.UserEmail, // CollectionId = Guid.Empty, // TODO: fix when collections are implemented Nickname = createPlantDto.Nickname, - ImageUrl = ímageUrl ?? DefaultImageUrl, + ImageUrl = ímageUrl ?? "", DeviceId = createPlantDto.DeviceId }; @@ -54,15 +46,34 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) return plant; } - private async Task SaveImageToBlobStorage(string base64Image, string userEmail) + private async Task SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null) { var imageBytes = Convert.FromBase64String(base64Image); - var blobUrl = userEmail + "_" + Guid.NewGuid(); + blobUrl ??= userEmail + "_" + Guid.NewGuid(); var blobClient = new BlobClient(azureBlobStorageOptions.Value.ConnectionString, azureBlobStorageOptions.Value.PlantImagesContainer, blobUrl); var binaryData = new BinaryData(imageBytes); await blobClient.UploadAsync(binaryData, true); return blobClient.Uri.ToString(); } + + private async Task DeleteImageFromBlobStorage(string imageUrl) + { + var blobClient = new BlobClient(new Uri(imageUrl)); + return await blobClient.DeleteIfExistsAsync(); + } + + private async Task GetImageFromBlobStorage(string imageUrl) + { + if (string.IsNullOrEmpty(imageUrl)) return ""; + + var blobClient = new BlobClient(new Uri(imageUrl)); + if (await blobClient.ExistsAsync() == false) throw new NotFoundException("Image not found"); + + using var memoryStream = new MemoryStream(); + await blobClient.DownloadToAsync(memoryStream); + var imageBytes = memoryStream.ToArray(); + return Convert.ToBase64String(imageBytes); + } public async Task GetPlantById(Guid id, string requesterEmail) { @@ -87,14 +98,20 @@ public async Task UpdatePlant(UpdatePlantDto updatePlantDto, string reque requirements = await requirementService.UpdateRequirements(updatePlantDto.UpdateRequirementDto, plant.PlantId); } + var imageUrl = plant.ImageUrl; + if (updatePlantDto.Base64Image is not null) + { + imageUrl = await SaveImageToBlobStorage(updatePlantDto.Base64Image, requesterEmail, plant.ImageUrl); + } + // Update the plant plant = new Plant { PlantId = updatePlantDto.PlantId, UserEmail = plant.UserEmail, CollectionId = updatePlantDto.CollectionId, - Nickname = updatePlantDto.Nickname, - ImageUrl = updatePlantDto.ImageUrl ?? DefaultImageUrl, + Nickname = updatePlantDto.Nickname ?? plant.Nickname, + ImageUrl = imageUrl, Requirements = requirements, ConditionsLogs = plant.ConditionsLogs }; diff --git a/Core/Services/RequirementService.cs b/Core/Services/RequirementService.cs index f972ba1..97004dc 100644 --- a/Core/Services/RequirementService.cs +++ b/Core/Services/RequirementService.cs @@ -1,6 +1,6 @@ using Infrastructure.Repositories; using Shared.Dtos.FromClient.Requirements; -using Shared.Models.Exceptions; +using Shared.Exceptions; using Shared.Models.Information; namespace Core.Services; diff --git a/Core/Services/UserService.cs b/Core/Services/UserService.cs index b17ccc9..1a28de8 100644 --- a/Core/Services/UserService.cs +++ b/Core/Services/UserService.cs @@ -1,7 +1,7 @@ using Infrastructure.Repositories; using Shared.Dtos.FromClient; using Shared.Dtos.FromClient.Identity; -using Shared.Models.Exceptions; +using Shared.Exceptions; using Shared.Models.Identity; namespace Core.Services; diff --git a/Infrastructure/Repositories/PlantRepository.cs b/Infrastructure/Repositories/PlantRepository.cs index e0e12e4..6a7cddd 100644 --- a/Infrastructure/Repositories/PlantRepository.cs +++ b/Infrastructure/Repositories/PlantRepository.cs @@ -1,8 +1,7 @@ using Microsoft.EntityFrameworkCore; using Shared.Dtos.FromClient.Plant; -using Shared.Dtos.Plant; +using Shared.Exceptions; using Shared.Models; -using Shared.Models.Exceptions; using Shared.Models.Information; namespace Infrastructure.Repositories; diff --git a/Shared/Dtos/FromClient/Identity/RegisterUserDto.cs b/Shared/Dtos/FromClient/Identity/RegisterUserDto.cs index fabb11c..22823a8 100644 --- a/Shared/Dtos/FromClient/Identity/RegisterUserDto.cs +++ b/Shared/Dtos/FromClient/Identity/RegisterUserDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Shared.Dtos.FromClient; +namespace Shared.Dtos.FromClient.Identity; public class RegisterUserDto { diff --git a/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs b/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs index ed8fb2f..fb0b9b4 100644 --- a/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs +++ b/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs @@ -1,8 +1,7 @@ using System.ComponentModel.DataAnnotations; using Shared.Dtos.FromClient.Requirements; -using Shared.Models.Information; -namespace Shared.Dtos.Plant; +namespace Shared.Dtos.FromClient.Plant; public class UpdatePlantDto { @@ -10,6 +9,6 @@ public class UpdatePlantDto public Guid? CollectionId { get; set; } public string? DeviceId { get; set; } [MaxLength(50)] public string? Nickname { get; set; } - public string? ImageUrl { get; set; } + public string? Base64Image { get; set; } // should be null if the image should not be updated public UpdateRequirementDto? UpdateRequirementDto { get; set; } } \ No newline at end of file diff --git a/Shared/Exceptions/InvalidCredentialsException.cs b/Shared/Exceptions/InvalidCredentialsException.cs index 44f0a2c..bc37139 100644 --- a/Shared/Exceptions/InvalidCredentialsException.cs +++ b/Shared/Exceptions/InvalidCredentialsException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class InvalidCredentialsException : AppException { diff --git a/Shared/Exceptions/InvalidFileFormatException.cs b/Shared/Exceptions/InvalidFileFormatException.cs index fd97336..e9b0b48 100644 --- a/Shared/Exceptions/InvalidFileFormatException.cs +++ b/Shared/Exceptions/InvalidFileFormatException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class InvalidFileFormatException : AppException { diff --git a/Shared/Exceptions/ModelValidationException.cs b/Shared/Exceptions/ModelValidationException.cs index db2760a..5a48eee 100644 --- a/Shared/Exceptions/ModelValidationException.cs +++ b/Shared/Exceptions/ModelValidationException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class ModelValidationException : AppException { diff --git a/Shared/Exceptions/NoAccessException.cs b/Shared/Exceptions/NoAccessException.cs index 2faaf1d..6180961 100644 --- a/Shared/Exceptions/NoAccessException.cs +++ b/Shared/Exceptions/NoAccessException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class NoAccessException : AppException { diff --git a/Shared/Exceptions/NotAuthenticatedException.cs b/Shared/Exceptions/NotAuthenticatedException.cs index 698c77e..46bc7a3 100644 --- a/Shared/Exceptions/NotAuthenticatedException.cs +++ b/Shared/Exceptions/NotAuthenticatedException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class NotAuthenticatedException : AppException { diff --git a/Shared/Exceptions/NotFoundException.cs b/Shared/Exceptions/NotFoundException.cs index 4a7d5c5..7eb2f2b 100644 --- a/Shared/Exceptions/NotFoundException.cs +++ b/Shared/Exceptions/NotFoundException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class NotFoundException: AppException { diff --git a/Shared/Exceptions/RegisterDeviceExeption.cs b/Shared/Exceptions/RegisterDeviceExeption.cs index c13f1a6..1c34a55 100644 --- a/Shared/Exceptions/RegisterDeviceExeption.cs +++ b/Shared/Exceptions/RegisterDeviceExeption.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class RegisterDeviceException: AppException { diff --git a/Shared/Exceptions/UserAlreadyExistsException.cs b/Shared/Exceptions/UserAlreadyExistsException.cs index 27abe9e..f9eaf03 100644 --- a/Shared/Exceptions/UserAlreadyExistsException.cs +++ b/Shared/Exceptions/UserAlreadyExistsException.cs @@ -1,6 +1,4 @@ -using Shared.Exceptions; - -namespace Shared.Models.Exceptions; +namespace Shared.Exceptions; public class UserAlreadyExistsException : AppException { diff --git a/Shared/Models/Plant.cs b/Shared/Models/Plant.cs index fcdc2a4..ed96b90 100644 --- a/Shared/Models/Plant.cs +++ b/Shared/Models/Plant.cs @@ -6,9 +6,9 @@ public class Plant { public Guid PlantId { get; set; } public string? DeviceId { get; set; } - public string UserEmail { get; set; } + public string UserEmail { get; set; } = null!; public Guid? CollectionId { get; set; } - public string Nickname { get; set; } // if not provided make one up + public string? Nickname { get; set; } // if not provided make one up public string ImageUrl { get; set; } = null!; public Requirements? Requirements { get; set; } public List ConditionsLogs { get; set; } = new(); diff --git a/api/EventFilters/ValidateDataAnnotations.cs b/api/EventFilters/ValidateDataAnnotations.cs index f592771..51a3d39 100644 --- a/api/EventFilters/ValidateDataAnnotations.cs +++ b/api/EventFilters/ValidateDataAnnotations.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using Fleck; using lib; -using Shared.Models.Exceptions; +using Shared.Exceptions; namespace api.EventFilters; diff --git a/api/Events/Auth/Client/ClientWantsToLogIn.cs b/api/Events/Auth/Client/ClientWantsToLogIn.cs index 87fe1dd..06ca9e3 100644 --- a/api/Events/Auth/Client/ClientWantsToLogIn.cs +++ b/api/Events/Auth/Client/ClientWantsToLogIn.cs @@ -5,7 +5,7 @@ using lib; using Shared.Dtos.FromClient; using Shared.Dtos.FromClient.Identity; -using Shared.Models.Exceptions; +using Shared.Exceptions; namespace api.Events.Auth.Client; diff --git a/api/Events/Auth/Client/ClientWantsToSignUp.cs b/api/Events/Auth/Client/ClientWantsToSignUp.cs index aa04fd7..520a012 100644 --- a/api/Events/Auth/Client/ClientWantsToSignUp.cs +++ b/api/Events/Auth/Client/ClientWantsToSignUp.cs @@ -5,6 +5,7 @@ using Fleck; using lib; using Shared.Dtos.FromClient; +using Shared.Dtos.FromClient.Identity; namespace api.Events.Auth.Client; diff --git a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs index ae713d1..efe9093 100644 --- a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs +++ b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs @@ -2,8 +2,8 @@ using Core.Services; using Fleck; using lib; +using Shared.Exceptions; using Shared.Models; -using Shared.Models.Exceptions; namespace api.Events.ImageUpload; diff --git a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs index c5fdb19..22f7b87 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs @@ -5,7 +5,6 @@ using Fleck; using lib; using Shared.Dtos.FromClient.Plant; -using Shared.Dtos.Plant; using Shared.Models; namespace api.Events.PlantEvents.Client; diff --git a/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs index 38fe168..de117f9 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs @@ -3,7 +3,7 @@ using Core.Services; using Fleck; using lib; -using Shared.Dtos.Plant; +using Shared.Dtos.FromClient.Plant; using Shared.Models; namespace api.Events.PlantEvents.Client; diff --git a/api/GlobalExceptionHandler.cs b/api/GlobalExceptionHandler.cs index 80ee2ca..511ca11 100644 --- a/api/GlobalExceptionHandler.cs +++ b/api/GlobalExceptionHandler.cs @@ -5,7 +5,6 @@ using Fleck; using Serilog; using Shared.Exceptions; -using Shared.Models.Exceptions; namespace api; diff --git a/api/Program.cs b/api/Program.cs index 47aab18..64d8fea 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -11,8 +11,9 @@ using Microsoft.EntityFrameworkCore; using Serilog; using Shared.Dtos.FromClient; +using Shared.Dtos.FromClient.Identity; +using Shared.Exceptions; using Shared.Models; -using Shared.Models.Exceptions; using Testcontainers.PostgreSql; namespace api; diff --git a/api/WebSocketConnectionService.cs b/api/WebSocketConnectionService.cs index e823107..c32296d 100644 --- a/api/WebSocketConnectionService.cs +++ b/api/WebSocketConnectionService.cs @@ -1,5 +1,5 @@ using Fleck; -using Shared.Models.Exceptions; +using Shared.Exceptions; using Shared.Models.Identity; namespace api; From 80e166820f32184b3a5671dfaed591a0f54b3d2e Mon Sep 17 00:00:00 2001 From: Julia Ilasova <1julka1il@gmail.com> Date: Thu, 2 May 2024 12:08:54 +0200 Subject: [PATCH 7/7] mock external services for tests --- Core/Services/BlobStorageService.cs | 6 --- .../IImageBackgroundRemoverService.cs | 6 +++ .../ImageBackgroundRemoverService.cs | 7 +--- .../MockImageBackgroundRemoverService.cs | 9 +++++ .../BlobStorage/BlobStorageServiceService.cs | 38 +++++++++++++++++++ .../BlobStorage/IBlobStorageService.cs | 8 ++++ .../BlobStorage/MockBlobStorageService.cs | 19 ++++++++++ Core/Services/PlantService.cs | 36 ++---------------- Tests/GlobalTestSetup.cs | 3 +- Tests/PlantTests.cs | 4 +- .../ClientWantsToRemoveBackgroundFromImage.cs | 4 +- .../AddServicesAndRepositoriesExtension.cs | 17 ++++++++- api/Program.cs | 5 +-- 13 files changed, 109 insertions(+), 53 deletions(-) delete mode 100644 Core/Services/BlobStorageService.cs create mode 100644 Core/Services/External/BackgroundRemoval/IImageBackgroundRemoverService.cs rename Core/Services/{ => External/BackgroundRemoval}/ImageBackgroundRemoverService.cs (90%) create mode 100644 Core/Services/External/BackgroundRemoval/MockImageBackgroundRemoverService.cs create mode 100644 Core/Services/External/BlobStorage/BlobStorageServiceService.cs create mode 100644 Core/Services/External/BlobStorage/IBlobStorageService.cs create mode 100644 Core/Services/External/BlobStorage/MockBlobStorageService.cs diff --git a/Core/Services/BlobStorageService.cs b/Core/Services/BlobStorageService.cs deleted file mode 100644 index 3c29238..0000000 --- a/Core/Services/BlobStorageService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Core.Services; - -public class BlobStorageService -{ - -} \ No newline at end of file diff --git a/Core/Services/External/BackgroundRemoval/IImageBackgroundRemoverService.cs b/Core/Services/External/BackgroundRemoval/IImageBackgroundRemoverService.cs new file mode 100644 index 0000000..59cf2ef --- /dev/null +++ b/Core/Services/External/BackgroundRemoval/IImageBackgroundRemoverService.cs @@ -0,0 +1,6 @@ +namespace Core.Services.External; + +public interface IImageBackgroundRemoverService +{ + public Task RemoveBackground(byte[] imageBytes); +} \ No newline at end of file diff --git a/Core/Services/ImageBackgroundRemoverService.cs b/Core/Services/External/BackgroundRemoval/ImageBackgroundRemoverService.cs similarity index 90% rename from Core/Services/ImageBackgroundRemoverService.cs rename to Core/Services/External/BackgroundRemoval/ImageBackgroundRemoverService.cs index f4fb8a7..7e1aaa2 100644 --- a/Core/Services/ImageBackgroundRemoverService.cs +++ b/Core/Services/External/BackgroundRemoval/ImageBackgroundRemoverService.cs @@ -1,15 +1,12 @@ using System.Net; using System.Net.Http.Headers; -using System.Text; using Core.Options; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Shared.Exceptions; -namespace Core.Services; +namespace Core.Services.External.BackgroundRemoval; -public class ImageBackgroundRemoverService(IOptions options) +public class ImageBackgroundRemoverService(IOptions options) : IImageBackgroundRemoverService { public async Task RemoveBackground(byte[] imageBytes) { diff --git a/Core/Services/External/BackgroundRemoval/MockImageBackgroundRemoverService.cs b/Core/Services/External/BackgroundRemoval/MockImageBackgroundRemoverService.cs new file mode 100644 index 0000000..f65b0cd --- /dev/null +++ b/Core/Services/External/BackgroundRemoval/MockImageBackgroundRemoverService.cs @@ -0,0 +1,9 @@ +namespace Core.Services.External; + +public class MockImageBackgroundRemoverService : IImageBackgroundRemoverService +{ + public Task RemoveBackground(byte[] imageBytes) + { + return Task.FromResult(imageBytes); + } +} \ No newline at end of file diff --git a/Core/Services/External/BlobStorage/BlobStorageServiceService.cs b/Core/Services/External/BlobStorage/BlobStorageServiceService.cs new file mode 100644 index 0000000..780047a --- /dev/null +++ b/Core/Services/External/BlobStorage/BlobStorageServiceService.cs @@ -0,0 +1,38 @@ +using Azure.Storage.Blobs; +using Core.Options; +using Microsoft.Extensions.Options; +using Shared.Exceptions; + +namespace Core.Services.External.BlobStorage; + +public class BlobStorageServiceService(IOptions azureBlobStorageOptions) : IBlobStorageService +{ + public async Task SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null) + { + var imageBytes = Convert.FromBase64String(base64Image); + blobUrl ??= userEmail + "_" + Guid.NewGuid(); + var blobClient = new BlobClient(azureBlobStorageOptions.Value.ConnectionString, azureBlobStorageOptions.Value.PlantImagesContainer, blobUrl); + var binaryData = new BinaryData(imageBytes); + await blobClient.UploadAsync(binaryData, true); + return blobClient.Uri.ToString(); + } + + public async Task DeleteImageFromBlobStorage(string imageUrl) + { + var blobClient = new BlobClient(new Uri(imageUrl)); + return await blobClient.DeleteIfExistsAsync(); + } + + public async Task GetImageFromBlobStorage(string imageUrl) + { + if (string.IsNullOrEmpty(imageUrl)) return ""; + + var blobClient = new BlobClient(new Uri(imageUrl)); + if (await blobClient.ExistsAsync() == false) throw new NotFoundException("Image not found"); + + using var memoryStream = new MemoryStream(); + await blobClient.DownloadToAsync(memoryStream); + var imageBytes = memoryStream.ToArray(); + return Convert.ToBase64String(imageBytes); + } +} \ No newline at end of file diff --git a/Core/Services/External/BlobStorage/IBlobStorageService.cs b/Core/Services/External/BlobStorage/IBlobStorageService.cs new file mode 100644 index 0000000..1d6caf2 --- /dev/null +++ b/Core/Services/External/BlobStorage/IBlobStorageService.cs @@ -0,0 +1,8 @@ +namespace Core.Services.External.BlobStorage; + +public interface IBlobStorageService +{ + public Task SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null); + public Task DeleteImageFromBlobStorage(string imageUrl); + public Task GetImageFromBlobStorage(string imageUrl); +} \ No newline at end of file diff --git a/Core/Services/External/BlobStorage/MockBlobStorageService.cs b/Core/Services/External/BlobStorage/MockBlobStorageService.cs new file mode 100644 index 0000000..5546d0e --- /dev/null +++ b/Core/Services/External/BlobStorage/MockBlobStorageService.cs @@ -0,0 +1,19 @@ +namespace Core.Services.External.BlobStorage; + +public class MockBlobStorageService : IBlobStorageService +{ + public Task SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null) + { + return Task.FromResult("https://www.example.com"); + } + + public Task DeleteImageFromBlobStorage(string imageUrl) + { + return Task.FromResult(true); + } + + public Task GetImageFromBlobStorage(string imageUrl) + { + return Task.FromResult("base64Image"); + } +} \ No newline at end of file diff --git a/Core/Services/PlantService.cs b/Core/Services/PlantService.cs index 4e767fe..83979d1 100644 --- a/Core/Services/PlantService.cs +++ b/Core/Services/PlantService.cs @@ -1,5 +1,6 @@ using Azure.Storage.Blobs; using Core.Options; +using Core.Services.External.BlobStorage; using Infrastructure.Repositories; using Microsoft.Extensions.Options; using Shared.Dtos.FromClient.Plant; @@ -11,7 +12,7 @@ namespace Core.Services; public class PlantService( PlantRepository plantRepository, RequirementService requirementService, - IOptions azureBlobStorageOptions) + IBlobStorageService blobStorageService) { public async Task CreatePlant(CreatePlantDto createPlantDto) { @@ -23,7 +24,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) string? ímageUrl = null; if (createPlantDto.Base64Image is not null) { - ímageUrl = await SaveImageToBlobStorage(createPlantDto.Base64Image, createPlantDto.UserEmail); + ímageUrl = await blobStorageService.SaveImageToBlobStorage(createPlantDto.Base64Image, createPlantDto.UserEmail); } // Insert plant first to get the plantId @@ -46,35 +47,6 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) return plant; } - private async Task SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null) - { - var imageBytes = Convert.FromBase64String(base64Image); - blobUrl ??= userEmail + "_" + Guid.NewGuid(); - var blobClient = new BlobClient(azureBlobStorageOptions.Value.ConnectionString, azureBlobStorageOptions.Value.PlantImagesContainer, blobUrl); - var binaryData = new BinaryData(imageBytes); - await blobClient.UploadAsync(binaryData, true); - return blobClient.Uri.ToString(); - } - - private async Task DeleteImageFromBlobStorage(string imageUrl) - { - var blobClient = new BlobClient(new Uri(imageUrl)); - return await blobClient.DeleteIfExistsAsync(); - } - - private async Task GetImageFromBlobStorage(string imageUrl) - { - if (string.IsNullOrEmpty(imageUrl)) return ""; - - var blobClient = new BlobClient(new Uri(imageUrl)); - if (await blobClient.ExistsAsync() == false) throw new NotFoundException("Image not found"); - - using var memoryStream = new MemoryStream(); - await blobClient.DownloadToAsync(memoryStream); - var imageBytes = memoryStream.ToArray(); - return Convert.ToBase64String(imageBytes); - } - public async Task GetPlantById(Guid id, string requesterEmail) { var plant = await VerifyPlantExistsAndUserHasAccess(id, requesterEmail); @@ -101,7 +73,7 @@ public async Task UpdatePlant(UpdatePlantDto updatePlantDto, string reque var imageUrl = plant.ImageUrl; if (updatePlantDto.Base64Image is not null) { - imageUrl = await SaveImageToBlobStorage(updatePlantDto.Base64Image, requesterEmail, plant.ImageUrl); + imageUrl = await blobStorageService.SaveImageToBlobStorage(updatePlantDto.Base64Image, requesterEmail, plant.ImageUrl); } // Update the plant diff --git a/Tests/GlobalTestSetup.cs b/Tests/GlobalTestSetup.cs index c80cbf1..941cfde 100644 --- a/Tests/GlobalTestSetup.cs +++ b/Tests/GlobalTestSetup.cs @@ -8,6 +8,7 @@ public class GlobalTestSetup [OneTimeSetUp] public async Task StartApi() { - await Startup.StartApi(["ENVIRONMENT=Testing", "--db-init"]); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing"); + await Startup.StartApi(["--db-init"]); } } \ No newline at end of file diff --git a/Tests/PlantTests.cs b/Tests/PlantTests.cs index 7ef0a12..407de3a 100644 --- a/Tests/PlantTests.cs +++ b/Tests/PlantTests.cs @@ -21,7 +21,7 @@ public async Task CreatePlant() await webSocketTestClient.DoAndAssert(new ClientWantsToCreatePlantDto { CreatePlantDto = createPlantDto, Jwt = jwt }, receivedMessages => { - return receivedMessages.Count(e => e.eventType == nameof(ServerSendsPlant)) == 1; + return receivedMessages.Count(e => e.eventType == nameof(ServerCreatesNewPlant)) == 1; }); await webSocketTestClient.DoAndAssert(new ClientWantsAllPlantsDto @@ -43,7 +43,7 @@ private CreatePlantDto GenerateRandomCreatePlantDto(string email) UserEmail = email, CollectionId = null, Nickname = "Nickname", - Base64Image = "https://realurl.com", + Base64Image = "iVBORw0KGgoAAAANSUhEUgAAAFgAAABHCAYAAACDFYB6AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAACmSURBVHhe7dAxAQAADMOg+TfdqeALEriFKhgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYKxgrGCsYK5jaHjFvRBBJ1UDnAAAAAElFTkSuQmCC", CreateRequirementsDto = new CreateRequirementsDto { SoilMoistureLevel = RequirementLevel.Low, diff --git a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs index efe9093..c0d01f4 100644 --- a/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs +++ b/api/Events/ImageUpload/ClientWantsToRemoveBackgroundFromImage.cs @@ -1,5 +1,5 @@ using api.Extensions; -using Core.Services; +using Core.Services.External; using Fleck; using lib; using Shared.Exceptions; @@ -12,7 +12,7 @@ public class ClientWantsToRemoveBackgroundFromImageDto : BaseDtoWithJwt public required string Base64Image { get; set; } } -public class ClientWantsToRemoveBackgroundFromImage(ImageBackgroundRemoverService backgroundRemoverService) : BaseEventHandler +public class ClientWantsToRemoveBackgroundFromImage(IImageBackgroundRemoverService backgroundRemoverService) : BaseEventHandler { public override async Task Handle(ClientWantsToRemoveBackgroundFromImageDto dto, IWebSocketConnection socket) { diff --git a/api/Extensions/AddServicesAndRepositoriesExtension.cs b/api/Extensions/AddServicesAndRepositoriesExtension.cs index 4af4703..9a963ae 100644 --- a/api/Extensions/AddServicesAndRepositoriesExtension.cs +++ b/api/Extensions/AddServicesAndRepositoriesExtension.cs @@ -1,4 +1,7 @@ using Core.Services; +using Core.Services.External; +using Core.Services.External.BackgroundRemoval; +using Core.Services.External.BlobStorage; using Infrastructure.Repositories; namespace api.Extensions; @@ -23,7 +26,17 @@ public static void AddServicesAndRepositories(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + + // External services + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing") + { + services.AddSingleton(); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + services.AddSingleton(); + } } } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 64d8fea..43d119c 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -42,7 +42,7 @@ public static async Task StartApi(string[] args) var builder = WebApplication.CreateBuilder(args); - if (args.Contains("ENVIRONMENT=Testing")) + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing") { var dbContainer = new PostgreSqlBuilder() @@ -77,7 +77,7 @@ public static async Task StartApi(string[] args) // On ci options are stored as repository secrets - if (args.Contains("ENVIRONMENT=Testing") && Environment.GetEnvironmentVariable("CI") is not null) + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing" && Environment.GetEnvironmentVariable("CI") is not null) { builder.Services.Configure(options => { @@ -112,7 +112,6 @@ public static async Task StartApi(string[] args) }); } - builder.Services.AddServicesAndRepositories(); var services = builder.FindAndInjectClientEventHandlers(Assembly.GetExecutingAssembly());