Skip to content

Commit

Permalink
Merge pull request #823 from neozhu/feature/minio
Browse files Browse the repository at this point in the history
Integrate MinIO for File Uploads
  • Loading branch information
neozhu authored Feb 22, 2025
2 parents 79b50c7 + ffcfdf0 commit fe3d71a
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 92 deletions.
6 changes: 1 addition & 5 deletions src/Application/Common/Interfaces/IUploadService.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using SixLabors.ImageSharp.Processing;

namespace CleanArchitecture.Blazor.Application.Common.Interfaces;

public interface IUploadService
{
Task<string> UploadAsync(UploadRequest request);
void Remove(string filename);

Task<string> UploadImageAsync(Stream imageStream, UploadType uploadType, ResizeOptions? resizeOptions = null, string? fileName=null);
Task RemoveAsync(string filename);
}
8 changes: 6 additions & 2 deletions src/Application/Common/Models/UploadRequest.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using SixLabors.ImageSharp.Processing;

namespace CleanArchitecture.Blazor.Application.Common.Models;

public class UploadRequest
{
public UploadRequest(string fileName, UploadType uploadType, byte[] data, bool overwrite = false)
public UploadRequest(string fileName, UploadType uploadType, byte[] data, bool overwrite = false,string? folder=null, ResizeOptions? resizeOptions=null)
{
FileName = fileName;
UploadType = uploadType;
Data = data;
Overwrite = overwrite;
Folder = folder;
ResizeOptions = resizeOptions;
}

public string FileName { get; set; }
public string? Extension { get; set; }
public UploadType UploadType { get; set; }
public bool Overwrite { get; set; }
public byte[] Data { get; set; }
public string? Folder { get; set; }
public ResizeOptions? ResizeOptions { get; set; }
}
5 changes: 3 additions & 2 deletions src/Domain/Common/Enums/UploadType.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
Expand All @@ -9,5 +9,6 @@ public enum UploadType : byte
{
[Description(@"Products")] Product,
[Description(@"ProfilePictures")] ProfilePicture,
[Description(@"Documents")] Document
[Description(@"Documents")] Document,
[Description(@"Images")] Image,
}
16 changes: 16 additions & 0 deletions src/Infrastructure/Configurations/MinioOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CleanArchitecture.Blazor.Infrastructure.Configurations;

public class MinioOptions
{
public const string Key = "Minio";
public string Endpoint { get; set; } = "https://minio.blazors.app:8843";
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string BucketName { get; set; } = "files";
}
7 changes: 6 additions & 1 deletion src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ private static IServiceCollection AddSettings(this IServiceCollection services,

services.Configure<DatabaseSettings>(configuration.GetSection(DATABASE_SETTINGS_KEY))
.AddSingleton(s => s.GetRequiredService<IOptions<DatabaseSettings>>().Value);

services.Configure<MinioOptions>(configuration.GetSection(MinioOptions.Key))
.AddSingleton(s => s.GetRequiredService<IOptions<MinioOptions>>().Value);
return services;
}

Expand Down Expand Up @@ -179,11 +182,13 @@ private static IServiceCollection AddServices(this IServiceCollection services)
service.Initialize();
return service;
});


return services
.AddScoped<IValidationService, ValidationService>()
.AddScoped<IDateTime, DateTimeService>()
.AddScoped<IExcelService, ExcelService>()
.AddScoped<IUploadService, UploadService>()
.AddScoped<IUploadService, MinioUploadService>()
.AddScoped<IPDFService, PDFService>()
.AddTransient<IDocumentOcrJob, DocumentOcrJob>();
}
Expand Down
1 change: 1 addition & 0 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="9.0.2" />
<PackageReference Include="Minio" Version="6.0.4" />
<PackageReference Include="QuestPDF" Version="2025.1.2" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="8.1.1-dev-00120" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace CleanArchitecture.Blazor.Infrastructure.Services;
/// <summary>
/// Service for uploading files.
/// </summary>
public class UploadService : IUploadService
public class FileUploadService : IUploadService
{
private static readonly string NumberPattern = " ({0})";

Expand All @@ -25,6 +25,16 @@ public async Task<string> UploadAsync(UploadRequest request)
{
if (request.Data == null || !request.Data.Any()) return string.Empty;

if (request.ResizeOptions != null)
{
using var inputStream = new MemoryStream(request.Data);
using var outputStream = new MemoryStream();
using var image = Image.Load(inputStream);
image.Mutate(i => i.Resize(request.ResizeOptions));
image.Save(outputStream, PngFormat.Instance);
request.Data = outputStream.ToArray();
}

var folder = request.UploadType.GetDescription();
var folderName = Path.Combine("Files", folder);
if (!string.IsNullOrEmpty(request.Folder))
Expand Down Expand Up @@ -58,50 +68,16 @@ public async Task<string> UploadAsync(UploadRequest request)
/// remove file
/// </summary>
/// <param name="filename"></param>
public void Remove(string filename)
public Task RemoveAsync(string filename)
{
var removefile = Path.Combine(Directory.GetCurrentDirectory(), filename);
if (File.Exists(removefile))
{
File.Delete(removefile);
}
return Task.CompletedTask;
}
/// <summary>
/// Uploads and processes an image.
/// </summary>
/// <param name="imageStream">The image stream.</param>
/// <param name="fileName">The file name.</param>
/// <param name="uploadType">The upload type.</param>
/// <param name="resizeOptions">The resize options (optional).</param>
/// <returns>The path of the uploaded image.</returns>
public async Task<string> UploadImageAsync(Stream imageStream, UploadType uploadType, ResizeOptions? resizeOptions = null, string? fileName=null)
{
var attachStream = new MemoryStream();
await imageStream.CopyToAsync(attachStream);
attachStream.Position = 0;

using (var outStream = new MemoryStream())
{
using (var image = Image.Load(attachStream))
{
// Apply resize if resize options are provided
if (resizeOptions != null)
{
image.Mutate(i => i.Resize(resizeOptions));
}

image.Save(outStream, PngFormat.Instance);
var result = await UploadAsync(new UploadRequest(

fileName: fileName ?? $"{Guid.NewGuid()}_{DateTime.UtcNow.Ticks}.png",
uploadType: uploadType,
data: outStream.ToArray(),
overwrite: true
));
return result;
}
}
}

/// <summary>
/// Gets the next available filename based on the given path.
/// </summary>
Expand Down
117 changes: 117 additions & 0 deletions src/Infrastructure/Services/MinioUploadService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using CleanArchitecture.Blazor.Application.Common.Extensions;
using CleanArchitecture.Blazor.Infrastructure.Configurations;
using Microsoft.AspNetCore.StaticFiles;
using Minio;
using Minio.DataModel.Args;
using Minio.Exceptions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;

namespace CleanArchitecture.Blazor.Infrastructure.Services;
public class MinioUploadService : IUploadService
{
private readonly IMinioClient _minioClient;
private readonly string _bucketName;
private readonly string _endpoint;
public MinioUploadService(MinioOptions options)
{
var opt = options;
_endpoint = opt.Endpoint;
_minioClient = new MinioClient()
.WithEndpoint(_endpoint)
.WithCredentials(opt.AccessKey, opt.SecretKey)
.WithSSL()
.Build();
_bucketName = opt.BucketName;
}

public async Task<string> UploadAsync(UploadRequest request)
{
// Use FileExtensionContentTypeProvider to determine the MIME type.
var provider = new FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(request.FileName, out var contentType))
{
contentType = "application/octet-stream";
}

// Define common bitmap image extensions (not including vector formats like SVG).
var bitmapImageExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
var ext = Path.GetExtension(request.FileName).ToLowerInvariant();

// If ResizeOptions is provided and the file is a bitmap image, process the image.
if (request.ResizeOptions != null && Array.Exists(bitmapImageExtensions, e => e.Equals(ext, StringComparison.OrdinalIgnoreCase)))
{
using var inputStream = new MemoryStream(request.Data);
using var outputStream = new MemoryStream();
using var image = Image.Load(inputStream);
image.Mutate(x => x.Resize(request.ResizeOptions));
// Convert the image to PNG format.
image.Save(outputStream, new PngEncoder());
request.Data = outputStream.ToArray();
contentType = "image/png";
}

// Ensure the bucket exists.
bool bucketExists = await _minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(_bucketName));
if (!bucketExists)
{
await _minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(_bucketName));
}

// Build folder path based on UploadType and optional Folder property.
string folderPath = $"{request.UploadType.GetDescription()}";
if (!string.IsNullOrWhiteSpace(request.Folder))
{
folderPath = $"{folderPath}/{request.Folder.Trim('/')}";
}

// Construct the object name including the folder path.
string objectName = $"{folderPath}/{request.FileName}";

using (var stream = new MemoryStream(request.Data))
{
await _minioClient.PutObjectAsync(new PutObjectArgs()
.WithBucket(_bucketName)
.WithObject(objectName)
.WithStreamData(stream)
.WithObjectSize(stream.Length)
.WithContentType(contentType)
);
}

// Return the URL constructed using the configured Endpoint.
return $"https://{_endpoint}/{_bucketName}/{objectName}";
}
public async Task RemoveAsync(string filename)
{
// Remove the "https://" or "http://" prefix from the URL and extract the bucket and object name.
Uri fileUri = new Uri(filename);

// Ensure the URL is well-formed and can be parsed
if (!fileUri.IsAbsoluteUri)
throw new ArgumentException("Invalid URL format.");

// Extract the bucket from the path portion of the URL
string[] pathParts = fileUri.AbsolutePath.TrimStart('/').Split('/', 2);
if (pathParts.Length < 2)
throw new ArgumentException("URL format must be 'https://<endpoint>/<bucket>/<object>'.");

string bucket = pathParts[0];
string objectName = pathParts[1];

try
{
// Proceed to remove the object from the correct bucket
await _minioClient.RemoveObjectAsync(new RemoveObjectArgs()
.WithBucket(bucket)
.WithObject(objectName));
}
catch (Exception ex)
{
throw new Exception("Error deleting object", ex);
}
}


}
21 changes: 16 additions & 5 deletions src/Server.UI/Pages/Identity/Users/Components/UserForm.razor
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,22 @@
}
private async Task UploadPhoto(IBrowserFile file)
{
var filestream = file.OpenReadStream(GlobalVariable.MaxAllowedSize);
var imgStream = new MemoryStream();
await filestream.CopyToAsync(imgStream);
imgStream.Position = 0;
var result = await UploadService.UploadImageAsync(imgStream, UploadType.ProfilePicture, new ResizeOptions { Mode = ResizeMode.Crop, Size = new Size(128, 128) });
using var fileStream = file.OpenReadStream(GlobalVariable.MaxAllowedSize);
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
byte[] fileData = memoryStream.ToArray();

var uploadRequest = new UploadRequest(file.Name, UploadType.ProfilePicture, fileData, overwrite: true)
{
ResizeOptions = new ResizeOptions()
{
Mode = ResizeMode.Max,
Size = new Size(128, 128)
},
Folder = Model.Id
};

var result = await UploadService.UploadAsync(uploadRequest);
Model.ProfilePictureDataUrl = result;
Snackbar.Add(ConstantString.UploadSuccess, Severity.Info);
}
Expand Down
25 changes: 16 additions & 9 deletions src/Server.UI/Pages/Identity/Users/Profile.razor
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,22 @@ else

private async Task UploadPhoto(IBrowserFile file)
{
var filestream = file.OpenReadStream(GlobalVariable.MaxAllowedSize);
var imgstream = new MemoryStream();
await filestream.CopyToAsync(imgstream);
imgstream.Position = 0;

var user = await UserManager.FindByNameAsync(model.UserName) ?? throw new NotFoundException($"The application user [{model.UserName}] was not found.");
var result = await UploadService.UploadImageAsync(imgstream, UploadType.ProfilePicture, new ResizeOptions { Mode = ResizeMode.Crop, Size = new Size(128, 128) });

using var fileStream = file.OpenReadStream(GlobalVariable.MaxAllowedSize);
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
byte[] fileData = memoryStream.ToArray();
var user = await UserManager.FindByNameAsync(model.UserName)
?? throw new NotFoundException($"The application user [{model.UserName}] was not found.");
var uploadRequest = new UploadRequest(file.Name, UploadType.ProfilePicture, fileData, overwrite: true)
{
ResizeOptions = new SixLabors.ImageSharp.Processing.ResizeOptions
{
Mode = SixLabors.ImageSharp.Processing.ResizeMode.Crop,
Size = new Size(128, 128)
},
Folder = user.Id
};
var result = await UploadService.UploadAsync(uploadRequest);
model.ProfilePictureDataUrl = result;
user.ProfilePictureDataUrl = model.ProfilePictureDataUrl;
await UserManager.UpdateAsync(user);
Expand All @@ -247,7 +255,6 @@ else
user.UserName ?? "",
user.DisplayName ?? "",
user.ProfilePictureDataUrl);

}

private async Task Submit()
Expand Down
Loading

0 comments on commit fe3d71a

Please sign in to comment.