Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove image background + add plant + blob #8

Merged
merged 8 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Vision.ImageAnalysis" Version="1.0.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.11.2" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.20.0-beta.2" />
<PackageReference Include="JWT" Version="10.1.1"/>
<PackageReference Include="MQTTnet" Version="4.3.3.952"/>
<PackageReference Include="Serilog" Version="3.1.1"/>
Expand Down
8 changes: 8 additions & 0 deletions Core/Options/AzureBlobStorageOptions.cs
Original file line number Diff line number Diff line change
@@ -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!;
}
3 changes: 2 additions & 1 deletion Core/Options/AzureVisionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
}
2 changes: 1 addition & 1 deletion Core/Services/ConditionsLogsService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Infrastructure.Repositories;
using Shared.Dtos;
using Shared.Models.Exceptions;
using Shared.Exceptions;
using Shared.Models.Information;

namespace Core.Services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Core.Services.External;

public interface IImageBackgroundRemoverService
{
public Task<byte[]> RemoveBackground(byte[] imageBytes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Net;
using System.Net.Http.Headers;
using Core.Options;
using Microsoft.Extensions.Options;
using Shared.Exceptions;

namespace Core.Services.External.BackgroundRemoval;

public class ImageBackgroundRemoverService(IOptions<AzureVisionOptions> options) : IImageBackgroundRemoverService
{
public async Task<byte[]> 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}");

HttpResponseMessage response;
using (var content = new ByteArrayContent(imageBytes))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
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 removedBgImageBytes = await response.Content.ReadAsByteArrayAsync();
return removedBgImageBytes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Core.Services.External;

public class MockImageBackgroundRemoverService : IImageBackgroundRemoverService
{
public Task<byte[]> RemoveBackground(byte[] imageBytes)
{
return Task.FromResult(imageBytes);
}
}
38 changes: 38 additions & 0 deletions Core/Services/External/BlobStorage/BlobStorageServiceService.cs
Original file line number Diff line number Diff line change
@@ -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> azureBlobStorageOptions) : IBlobStorageService
{
public async Task<string> 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<bool> DeleteImageFromBlobStorage(string imageUrl)
{
var blobClient = new BlobClient(new Uri(imageUrl));
return await blobClient.DeleteIfExistsAsync();
}

public async Task<string> 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);
}
}
8 changes: 8 additions & 0 deletions Core/Services/External/BlobStorage/IBlobStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Core.Services.External.BlobStorage;

public interface IBlobStorageService
{
public Task<string> SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null);
public Task<bool> DeleteImageFromBlobStorage(string imageUrl);
public Task<string> GetImageFromBlobStorage(string imageUrl);
}
19 changes: 19 additions & 0 deletions Core/Services/External/BlobStorage/MockBlobStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Core.Services.External.BlobStorage;

public class MockBlobStorageService : IBlobStorageService
{
public Task<string> SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null)
{
return Task.FromResult("https://www.example.com");
}

public Task<bool> DeleteImageFromBlobStorage(string imageUrl)
{
return Task.FromResult(true);
}

public Task<string> GetImageFromBlobStorage(string imageUrl)
{
return Task.FromResult("base64Image");
}
}
1 change: 0 additions & 1 deletion Core/Services/MqttSubscriberService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
43 changes: 28 additions & 15 deletions Core/Services/PlantService.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
using Infrastructure.Repositories;
using Azure.Storage.Blobs;
using Core.Options;
using Core.Services.External.BlobStorage;
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;

public class PlantService (PlantRepository plantRepository, RequirementService requirementService)
public class PlantService(
PlantRepository plantRepository,
RequirementService requirementService,
IBlobStorageService blobStorageService)
{
private const string DefaultImageUrl =
"https://www.creativefabrica.com/wp-content/uploads/2022/01/20/Animated-Plant-Graphics-23785833-1.jpg";

public async Task<Plant> CreatePlant(CreatePlantDto createPlantDto)
{
if (string.IsNullOrEmpty(createPlantDto.Nickname))
{
createPlantDto.Nickname = GenerateRandomNickname();
}


string? ímageUrl = null;
if (createPlantDto.Base64Image is not null)
{
ímageUrl = await blobStorageService.SaveImageToBlobStorage(createPlantDto.Base64Image, createPlantDto.UserEmail);
}

// Insert plant first to get the plantId
var plant = new Plant
{
PlantId = Guid.NewGuid(),
UserEmail = createPlantDto.UserEmail,
// CollectionId = Guid.Empty, // TODO: fix when collections are implemented
Nickname = createPlantDto.Nickname,
ImageUrl = createPlantDto.ImageUrl ?? DefaultImageUrl,
ImageUrl = ímageUrl ?? "",
DeviceId = createPlantDto.DeviceId
};

Expand All @@ -35,11 +43,10 @@ public async Task<Plant> 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;
}

public async Task<Plant> GetPlantById(Guid id, string requesterEmail)
{
var plant = await VerifyPlantExistsAndUserHasAccess(id, requesterEmail);
Expand All @@ -63,14 +70,20 @@ public async Task<Plant> 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 blobStorageService.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
};
Expand Down
4 changes: 2 additions & 2 deletions Core/Services/RequirementService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,7 +22,7 @@ public async Task<Requirements> CreateRequirements(CreateRequirementsDto createR

public async Task<Requirements?> 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");

Expand Down
2 changes: 1 addition & 1 deletion Core/Services/UserService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Infrastructure/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey<Requirements>(e => e.PlantId);

modelBuilder.Entity<Requirements>()
.HasKey(e => e.ConditionsId);
.HasKey(e => e.RequirementsId);

modelBuilder.Entity<ConditionsLog>()
.HasKey(e => e.ConditionsId);
Expand Down
5 changes: 2 additions & 3 deletions Infrastructure/Repositories/PlantRepository.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,7 +65,7 @@ public async Task<Guid> GetPlantIdByDeviceIdAsync(string deviceId)
return plant.PlantId;
}

public async Task<Conditions> GetRequirementsForPlant(Guid plantId)
public async Task<Requirements> GetRequirementsForPlant(Guid plantId)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var plant = await context.Plants
Expand Down
2 changes: 1 addition & 1 deletion Infrastructure/Repositories/RequirementsRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class RequirementsRepository(IDbContextFactory<ApplicationDbContext> dbCo
public async Task<Requirements?> 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<Requirements> CreateRequirements(Requirements requirements)
Expand Down
2 changes: 1 addition & 1 deletion Shared/Dtos/FromClient/Identity/RegisterUserDto.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Shared.Dtos.FromClient;
namespace Shared.Dtos.FromClient.Identity;

public class RegisterUserDto
{
[EmailAddress] public string Email { get; set; }

Check warning on line 7 in Shared/Dtos/FromClient/Identity/RegisterUserDto.cs

View workflow job for this annotation

GitHub Actions / tests

Non-nullable property 'Email' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[MaxLength(50)] public string Username { get; set; }

Check warning on line 9 in Shared/Dtos/FromClient/Identity/RegisterUserDto.cs

View workflow job for this annotation

GitHub Actions / tests

Non-nullable property 'Username' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[MinLength(8)] [MaxLength(256)] public string Password { get; set; }

Check warning on line 11 in Shared/Dtos/FromClient/Identity/RegisterUserDto.cs

View workflow job for this annotation

GitHub Actions / tests

Non-nullable property 'Password' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Expand Down
2 changes: 1 addition & 1 deletion Shared/Dtos/FromClient/Plant/CreatePlantDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
}
5 changes: 2 additions & 3 deletions Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
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
{
[Required] public Guid PlantId { get; set; }
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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Shared.Models.Exceptions;
namespace Shared.Exceptions;

public class InvalidCredentialsException : AppException
{
Expand Down
Loading
Loading