From a9cd4e506ef1ef17ed5177749f32174981e0097a Mon Sep 17 00:00:00 2001 From: Bao To Quoc Date: Wed, 11 Dec 2024 01:27:30 +0700 Subject: [PATCH] feat: integrate Azure Blob Storage for file management and update image retrieval logic --- .../Features/Products/GetProductImage.cs | 15 +++----- .../MicroCommerce.ApiService.csproj | 4 -- .../Services/FileService.cs | 20 ++++++++++ code/src/MicroCommerce.AppHost/Program.cs | 1 + .../DbInitializer.cs | 35 +++++++++++++++--- .../MicroCommerce.MigrationService.csproj | 2 +- .../MicroCommerce.MigrationService/Program.cs | 12 +++++- .../Resources/Images/1.png | Bin .../Resources/Images/10.png | Bin .../Resources/Images/11.png | Bin .../Resources/Images/12.png | Bin .../Resources/Images/13.png | Bin .../Resources/Images/14.png | Bin .../Resources/Images/2.png | Bin .../Resources/Images/3.png | Bin .../Resources/Images/4.png | Bin .../Resources/Images/5.png | Bin .../Resources/Images/6.png | Bin .../Resources/Images/7.png | Bin .../Resources/Images/8.png | Bin .../Resources/Images/9.png | Bin 21 files changed, 68 insertions(+), 21 deletions(-) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/1.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/10.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/11.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/12.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/13.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/14.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/2.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/3.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/4.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/5.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/6.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/7.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/8.png (100%) rename code/src/{MicroCommerce.ApiService => MicroCommerce.MigrationService}/Resources/Images/9.png (100%) diff --git a/code/src/MicroCommerce.ApiService/Features/Products/GetProductImage.cs b/code/src/MicroCommerce.ApiService/Features/Products/GetProductImage.cs index 955dcb0..23feba3 100644 --- a/code/src/MicroCommerce.ApiService/Features/Products/GetProductImage.cs +++ b/code/src/MicroCommerce.ApiService/Features/Products/GetProductImage.cs @@ -1,6 +1,8 @@ +using System.Net.Mime; using Ardalis.GuardClauses; using MediatR; using MicroCommerce.ApiService.Infrastructure; +using MicroCommerce.ApiService.Services; using Microsoft.EntityFrameworkCore; namespace MicroCommerce.ApiService.Features.Products; @@ -9,16 +11,11 @@ public class GetProductImage : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder builder) { - builder.MapGet("/api/products/images/{url}", async (string url, IMediator mediator, IWebHostEnvironment environment) => + builder.MapGet("/api/products/images/{url}", async (string url, IFileService fileService) => { - var path = Path.Combine(environment.ContentRootPath, "Resources/Images", url); + var stream = await fileService.DownloadFileAsync(url); - if (!File.Exists(path)) - { - return Results.NotFound(); - } - - return Results.File(path, "image/jpeg"); - }); + return TypedResults.File(stream, MediaTypeNames.Image.Jpeg); + }).Produces(contentType: MediaTypeNames.Image.Jpeg); } } diff --git a/code/src/MicroCommerce.ApiService/MicroCommerce.ApiService.csproj b/code/src/MicroCommerce.ApiService/MicroCommerce.ApiService.csproj index 39390da..f19752d 100644 --- a/code/src/MicroCommerce.ApiService/MicroCommerce.ApiService.csproj +++ b/code/src/MicroCommerce.ApiService/MicroCommerce.ApiService.csproj @@ -37,8 +37,4 @@ - - - - diff --git a/code/src/MicroCommerce.ApiService/Services/FileService.cs b/code/src/MicroCommerce.ApiService/Services/FileService.cs index 6a371e7..515d238 100644 --- a/code/src/MicroCommerce.ApiService/Services/FileService.cs +++ b/code/src/MicroCommerce.ApiService/Services/FileService.cs @@ -6,6 +6,7 @@ public interface IFileService { Task UploadFileAsync(string fileName, Stream stream, CancellationToken cancellationToken = default); Task CreateContainerIfNotExistsAsync(CancellationToken cancellationToken = default); + Task DownloadFileAsync(string fileName, CancellationToken cancellationToken = default); } public class FileService : IFileService @@ -30,11 +31,30 @@ public async Task UploadFileAsync(string fileName, Stream stream, Cancel if (response.GetRawResponse().IsError) { _logger.LogError("Failed to upload file {FileName} to blob storage {Info}", fileName, response.ToString()); + throw new Exception($"Failed to upload file {fileName}"); } return blobClient.Uri.ToString(); } + public async Task DownloadFileAsync(string fileName, CancellationToken cancellationToken = default) + { + var containerClient = _blobServiceClient.GetBlobContainerClient(ContainerName); + var blobClient = containerClient.GetBlobClient(fileName); + + var memoryStream = new MemoryStream(); + var response = await blobClient.DownloadToAsync(memoryStream, cancellationToken); + + if (response.IsError) + { + _logger.LogError("Failed to download file {FileName} from blob storage {Info}", fileName, response.ToString()); + throw new Exception($"Failed to download file {fileName}"); + } + + memoryStream.Position = 0; + return memoryStream; + } + public async Task CreateContainerIfNotExistsAsync(CancellationToken cancellationToken = default) { var containerClient = _blobServiceClient.GetBlobContainerClient(ContainerName); diff --git a/code/src/MicroCommerce.AppHost/Program.cs b/code/src/MicroCommerce.AppHost/Program.cs index b106eaa..5e2f791 100644 --- a/code/src/MicroCommerce.AppHost/Program.cs +++ b/code/src/MicroCommerce.AppHost/Program.cs @@ -44,6 +44,7 @@ var migrationService = builder.AddProject("migrationservice") .WithReference(db).WaitFor(db) .WithReference(rabbitmq).WaitFor(rabbitmq) + .WithReference(blobs) .WithHttpHealthCheck("/health"); var apiService = builder.AddProject("apiservice") diff --git a/code/src/MicroCommerce.MigrationService/DbInitializer.cs b/code/src/MicroCommerce.MigrationService/DbInitializer.cs index 417dd77..acf140c 100644 --- a/code/src/MicroCommerce.MigrationService/DbInitializer.cs +++ b/code/src/MicroCommerce.MigrationService/DbInitializer.cs @@ -3,6 +3,7 @@ using MicroCommerce.ApiService.Domain.Entities; using MicroCommerce.ApiService.Features.DomainEvents; using MicroCommerce.ApiService.Infrastructure; +using MicroCommerce.ApiService.Services; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -24,8 +25,10 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var publishEndpoint = scope.ServiceProvider.GetRequiredService(); + var fileService = scope.ServiceProvider.GetRequiredService(); + var environment = scope.ServiceProvider.GetRequiredService(); - await InitializeDatabaseAsync(context, publishEndpoint, cancellationToken); + await InitializeDatabaseAsync(context, publishEndpoint, fileService, environment, cancellationToken); } catch (Exception ex) { @@ -34,20 +37,20 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) } } - public async Task InitializeDatabaseAsync(ApplicationDbContext context, IPublishEndpoint publishEndpoint, CancellationToken cancellationToken = default) + public async Task InitializeDatabaseAsync(ApplicationDbContext context, IPublishEndpoint publishEndpoint, IFileService fileService, IHostEnvironment environment, CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); var strategy = context.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(context.Database.MigrateAsync, cancellationToken); - await SeedDataAsync(context, cancellationToken); + await SeedDataAsync(context, fileService, environment, cancellationToken); await IndexData(context, publishEndpoint, cancellationToken); logger.LogInformation("Database initialization completed after {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); } - private static async Task SeedDataAsync(ApplicationDbContext context, CancellationToken cancellationToken) + private static async Task SeedDataAsync(ApplicationDbContext context, IFileService fileService, IHostEnvironment environment, CancellationToken cancellationToken) { if (!context.Products.Any()) { @@ -69,7 +72,29 @@ static List GetPreconfiguredItems() ]; } - await context.Products.AddRangeAsync(GetPreconfiguredItems(), cancellationToken); + var products = GetPreconfiguredItems(); + + var tasks = new List(); + foreach (var product in products) + { + var filePath = Path.Combine(environment.ContentRootPath, "Resources/Images", product.ImageUrl); + if (!File.Exists(filePath)) + { + continue; + } + + var uploadTask = Task.Run(async () => + { + await using var stream = File.OpenRead(filePath); + await fileService.UploadFileAsync(product.ImageUrl, stream, cancellationToken); + }, cancellationToken); + + tasks.Add(uploadTask); + } + + await Task.WhenAll(tasks); + + await context.Products.AddRangeAsync(products, cancellationToken); } await context.SaveChangesAsync(cancellationToken); diff --git a/code/src/MicroCommerce.MigrationService/MicroCommerce.MigrationService.csproj b/code/src/MicroCommerce.MigrationService/MicroCommerce.MigrationService.csproj index 68f85b8..59b152f 100644 --- a/code/src/MicroCommerce.MigrationService/MicroCommerce.MigrationService.csproj +++ b/code/src/MicroCommerce.MigrationService/MicroCommerce.MigrationService.csproj @@ -19,7 +19,7 @@ - + diff --git a/code/src/MicroCommerce.MigrationService/Program.cs b/code/src/MicroCommerce.MigrationService/Program.cs index 64f0f4f..564b8d3 100644 --- a/code/src/MicroCommerce.MigrationService/Program.cs +++ b/code/src/MicroCommerce.MigrationService/Program.cs @@ -1,6 +1,7 @@ using MassTransit; using MassTransit.Transports; using MicroCommerce.ApiService.Infrastructure; +using MicroCommerce.ApiService.Services; using MicroCommerce.MigrationService; using MicroCommerce.ServiceDefaults; using Microsoft.EntityFrameworkCore; @@ -46,6 +47,8 @@ }); }); +builder.AddAzureBlobClient("blobs"); +builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.AddHealthChecks() @@ -55,11 +58,16 @@ if (app.Environment.IsDevelopment()) { - app.MapPost("/reset", async (ApplicationDbContext dbContext, IPublishEndpoint publishEndpoint, DbInitializer dbInitializer, CancellationToken cancellationToken) => + app.MapGet("/reset", async (ApplicationDbContext dbContext, IPublishEndpoint publishEndpoint, + DbInitializer dbInitializer, + IFileService fileService, + IHostEnvironment environment, CancellationToken cancellationToken) => { // Delete and recreate the database. This is useful for development scenarios to reset the database to its initial state. await dbContext.Database.EnsureDeletedAsync(cancellationToken); - await dbInitializer.InitializeDatabaseAsync(dbContext, publishEndpoint, cancellationToken); + await dbInitializer.InitializeDatabaseAsync(dbContext, publishEndpoint, fileService, environment, cancellationToken); + + return Results.Ok("ok"); }); } diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/1.png b/code/src/MicroCommerce.MigrationService/Resources/Images/1.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/1.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/1.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/10.png b/code/src/MicroCommerce.MigrationService/Resources/Images/10.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/10.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/10.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/11.png b/code/src/MicroCommerce.MigrationService/Resources/Images/11.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/11.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/11.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/12.png b/code/src/MicroCommerce.MigrationService/Resources/Images/12.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/12.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/12.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/13.png b/code/src/MicroCommerce.MigrationService/Resources/Images/13.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/13.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/13.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/14.png b/code/src/MicroCommerce.MigrationService/Resources/Images/14.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/14.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/14.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/2.png b/code/src/MicroCommerce.MigrationService/Resources/Images/2.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/2.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/2.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/3.png b/code/src/MicroCommerce.MigrationService/Resources/Images/3.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/3.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/3.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/4.png b/code/src/MicroCommerce.MigrationService/Resources/Images/4.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/4.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/4.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/5.png b/code/src/MicroCommerce.MigrationService/Resources/Images/5.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/5.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/5.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/6.png b/code/src/MicroCommerce.MigrationService/Resources/Images/6.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/6.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/6.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/7.png b/code/src/MicroCommerce.MigrationService/Resources/Images/7.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/7.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/7.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/8.png b/code/src/MicroCommerce.MigrationService/Resources/Images/8.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/8.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/8.png diff --git a/code/src/MicroCommerce.ApiService/Resources/Images/9.png b/code/src/MicroCommerce.MigrationService/Resources/Images/9.png similarity index 100% rename from code/src/MicroCommerce.ApiService/Resources/Images/9.png rename to code/src/MicroCommerce.MigrationService/Resources/Images/9.png