Merged PR 82: #67 added blob storage service + endpoints for profile picture. WIP
#67 added blob storage service + endpoints for profile picture. WIP Related work items: #67
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.11.0" />
|
||||
<PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.AzureBlobStorage.Constants;
|
||||
|
||||
public static class CommonFileNames
|
||||
{
|
||||
public static string ProfilePicture = "profilePicture";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.AzureBlobStorage.Constants;
|
||||
|
||||
public static class ContainerNames
|
||||
{
|
||||
public static string Users = "users";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Hutopy.Application.AzureBlobStorage.Constants;
|
||||
|
||||
public static class SubDirectoryNames
|
||||
{
|
||||
public static string Profile = "profile";
|
||||
public static string Posts = "posts";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IAzureBlobStorageService
|
||||
{
|
||||
Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream);
|
||||
Task<MemoryStream> DownloadFileAsync(string containerName, string blobName);
|
||||
}
|
||||
25
src/Application/Users/Commands/UploadProfilePicture.cs
Normal file
25
src/Application/Users/Commands/UploadProfilePicture.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Hutopy.Application.AzureBlobStorage.Constants;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.Users.Commands;
|
||||
|
||||
public class UploadProfilePictureCommand : IRequest<string>
|
||||
{
|
||||
public required Stream ProfilePicture { get; init; }
|
||||
}
|
||||
|
||||
public class UploadProfilePictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler<UploadProfilePictureCommand, string>
|
||||
{
|
||||
public async Task<string> Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await identityService.GetCurrentUserAsync();
|
||||
var currentUserId = new Guid(identityUser?.Id ?? "").ToString();
|
||||
|
||||
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}";
|
||||
|
||||
var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Hutopy.Application.AzureBlobStorage.Constants;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.Users.Queries.GetCurrentUser;
|
||||
|
||||
public record GetCurrentUserProfilePictureQuery : IRequest<Stream>;
|
||||
|
||||
public class GetCurrentUserProfilePictureQueryHandler(
|
||||
IIdentityService identityService,
|
||||
IAzureBlobStorageService azureBlobStorageService
|
||||
)
|
||||
: IRequestHandler<GetCurrentUserProfilePictureQuery, Stream>
|
||||
{
|
||||
public async Task<Stream> Handle(GetCurrentUserProfilePictureQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await identityService.GetCurrentUserAsync();
|
||||
var currentUserId = new Guid(identityUser?.Id ?? "");
|
||||
|
||||
var blobName = $"{currentUserId.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}";
|
||||
|
||||
return await azureBlobStorageService.DownloadFileAsync(ContainerNames.Users, blobName);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ public class UserDto
|
||||
public string UserName { get; init; } = String.Empty;
|
||||
public List<UserTransactionDto> UserTransactions { get; init; } = [];
|
||||
public IList<string> UserRoles { get; init; } = [];
|
||||
|
||||
public required decimal TotalBalance { get; init; }
|
||||
|
||||
}
|
||||
|
||||
74
src/Infrastructure/AzureBlob/AzureBlobStorageService.cs
Normal file
74
src/Infrastructure/AzureBlob/AzureBlobStorageService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Hutopy.Infrastructure.AzureBlob;
|
||||
|
||||
public class AzureBlobStorageService : IAzureBlobStorageService
|
||||
{
|
||||
private readonly BlobServiceClient _blobServiceClient;
|
||||
|
||||
public AzureBlobStorageService(IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration["Azure-Blob-Connection-String"] ?? "";
|
||||
_blobServiceClient = new BlobServiceClient(connectionString);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a file to microsoft azure blob storage.
|
||||
/// </summary>
|
||||
/// <param name="blobName">The blob name (path within the container, include the file name).</param>
|
||||
/// <param name="containerName">The name of the container where the file is stored.</param>
|
||||
/// <param name="fileStream">The stream.</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream)
|
||||
{
|
||||
// Get a reference to a container
|
||||
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||
|
||||
// Create the container if it does not exist
|
||||
await containerClient.CreateIfNotExistsAsync();
|
||||
|
||||
// Get a reference to a blob
|
||||
var blobClient = containerClient.GetBlobClient(blobName);
|
||||
|
||||
// Upload the file
|
||||
await blobClient.UploadAsync(fileStream, true);
|
||||
|
||||
// Return the URI of the uploaded blob
|
||||
return blobClient.Uri.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a file to microsoft azure blob storage.
|
||||
/// </summary>
|
||||
/// <param name="blobName">The blob name (path within the container).</param>
|
||||
/// <param name="containerName">The name of the container where the file is stored. (users)</param>
|
||||
/// <returns></returns>
|
||||
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName)
|
||||
{
|
||||
// Get a reference to a container
|
||||
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
||||
|
||||
// Get a reference to a blob
|
||||
var blobClient = containerClient.GetBlobClient(blobName);
|
||||
|
||||
try
|
||||
{
|
||||
// Download the blob to a stream
|
||||
BlobDownloadInfo download = await blobClient.DownloadAsync();
|
||||
MemoryStream memoryStream = new();
|
||||
await download.Content.CopyToAsync(memoryStream);
|
||||
memoryStream.Position = 0; // Ensure the stream is at the beginning
|
||||
|
||||
return memoryStream;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log and handle the exception as needed
|
||||
throw new ApplicationException("Error downloading file from Azure Blob Storage.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Infrastructure/AzureBlob/BlobStructure.txt
Normal file
33
src/Infrastructure/AzureBlob/BlobStructure.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
users/
|
||||
│
|
||||
├── userId1/
|
||||
│ ├── profile/
|
||||
│ │ └── profilePicture.jpg
|
||||
│ │ └── data.json
|
||||
│ │
|
||||
│ ├── posts/
|
||||
│ │ ├── post1/
|
||||
│ │ │ ├── image1.jpg
|
||||
│ │ │ ├── video1.mp4
|
||||
│ │ │ └── audio1.mp3
|
||||
│ │ ├── post2/
|
||||
│ │ │ ├── image2.jpg
|
||||
│ │ │ └── video2.mp4
|
||||
│ │ └── ...
|
||||
│
|
||||
├── userId2/
|
||||
│ ├── profile/
|
||||
│ │ └── profilePicture.jpg
|
||||
│ │ └── data.json
|
||||
│ │
|
||||
│ ├── posts/
|
||||
│ │ ├── post1/
|
||||
│ │ │ ├── image1.jpg
|
||||
│ │ │ ├── video1.mp4
|
||||
│ │ │ └── audio1.mp3
|
||||
│ │ ├── post2/
|
||||
│ │ │ ├── image2.jpg
|
||||
│ │ │ └── video2.mp4
|
||||
│ │ └── ...
|
||||
│
|
||||
└── ...
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Infrastructure.AzureBlob;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Data.Interceptors;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
@@ -63,8 +64,14 @@ public static class DependencyInjection
|
||||
.AddSignInManager<SignInManager<ApplicationUser>>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
// Singleton services
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<IAzureBlobStorageService, AzureBlobStorageService>();
|
||||
|
||||
// Scoped services
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
|
||||
// Transient services
|
||||
services.AddTransient<IStripeService, StripeService>();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
|
||||
@@ -9,11 +9,17 @@ public class GetMyUser : EndpointGroupBase
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetCurrentUser);
|
||||
.MapGet(GetCurrentUser)
|
||||
.MapGet(GetCurrentUserProfilePicture, "profile-picture");
|
||||
}
|
||||
|
||||
private static async Task<UserDto> GetCurrentUser(ISender sender, [AsParameters] GetCurrentUserQuery query)
|
||||
{
|
||||
return await sender.Send(query);
|
||||
}
|
||||
|
||||
private static async Task<Stream> GetCurrentUserProfilePicture(ISender sender, [AsParameters] GetCurrentUserProfilePictureQuery query)
|
||||
{
|
||||
return await sender.Send(query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public class Users : EndpointGroupBase
|
||||
app.MapGroup(this)
|
||||
.MapPost(CreateUser)
|
||||
.MapPost(Login, "/login")
|
||||
.MapPost(UploadProfilePicture, "/upload-profile-picture")
|
||||
.MapGet(GetMinimalUser);
|
||||
}
|
||||
|
||||
@@ -27,4 +28,13 @@ public class Users : EndpointGroupBase
|
||||
{
|
||||
return await sender.Send(command);
|
||||
}
|
||||
|
||||
private static async Task<string> UploadProfilePicture(ISender sender, Stream stream)
|
||||
{
|
||||
var command = new UploadProfilePictureCommand
|
||||
{
|
||||
ProfilePicture = stream
|
||||
};
|
||||
return await sender.Send(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,56 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/GetMyUser/profile-picture": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"GetMyUser"
|
||||
],
|
||||
"operationId": "GetCurrentUserProfilePicture",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"JWT": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/GetMyUser/profile-picture-2": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
"GetMyUser"
|
||||
],
|
||||
"operationId": "PatchApiGetMyUserProfilePicture2",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"JWT": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/JoinUs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -313,6 +363,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Users/upload-profile-picture": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"operationId": "UploadProfilePicture",
|
||||
"requestBody": {
|
||||
"x-name": "stream",
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-position": 1
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/WeatherForecasts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -448,6 +532,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Stream": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/MarshalByRefObject"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"x-abstract": true,
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"canRead": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canWrite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canSeek": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canTimeout": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"length": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"readTimeout": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"writeTimeout": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MarshalByRefObject": {
|
||||
"type": "object",
|
||||
"x-abstract": true,
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PaginatedListOfFutureCreatorListDto": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user