#67 added blob storage service + endpoints for profile picture. WIP
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
||||||
<PackageVersion Include="Azure.Identity" Version="1.11.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="coverlet.collector" Version="6.0.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
||||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.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 string UserName { get; init; } = String.Empty;
|
||||||
public List<UserTransactionDto> UserTransactions { get; init; } = [];
|
public List<UserTransactionDto> UserTransactions { get; init; } = [];
|
||||||
public IList<string> UserRoles { get; init; } = [];
|
public IList<string> UserRoles { get; init; } = [];
|
||||||
|
|
||||||
public required decimal TotalBalance { 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.Application.Common.Interfaces;
|
||||||
using Hutopy.Domain.Constants;
|
using Hutopy.Domain.Constants;
|
||||||
|
using Hutopy.Infrastructure.AzureBlob;
|
||||||
using Hutopy.Infrastructure.Data;
|
using Hutopy.Infrastructure.Data;
|
||||||
using Hutopy.Infrastructure.Data.Interceptors;
|
using Hutopy.Infrastructure.Data.Interceptors;
|
||||||
using Hutopy.Infrastructure.Identity;
|
using Hutopy.Infrastructure.Identity;
|
||||||
@@ -63,8 +64,14 @@ public static class DependencyInjection
|
|||||||
.AddSignInManager<SignInManager<ApplicationUser>>()
|
.AddSignInManager<SignInManager<ApplicationUser>>()
|
||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
// Singleton services
|
||||||
services.AddSingleton(TimeProvider.System);
|
services.AddSingleton(TimeProvider.System);
|
||||||
|
services.AddSingleton<IAzureBlobStorageService, AzureBlobStorageService>();
|
||||||
|
|
||||||
|
// Scoped services
|
||||||
services.AddScoped<IIdentityService, IdentityService>();
|
services.AddScoped<IIdentityService, IdentityService>();
|
||||||
|
|
||||||
|
// Transient services
|
||||||
services.AddTransient<IStripeService, StripeService>();
|
services.AddTransient<IStripeService, StripeService>();
|
||||||
|
|
||||||
services.AddAuthorization(options =>
|
services.AddAuthorization(options =>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.Storage.Blobs" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||||
|
|||||||
@@ -9,11 +9,17 @@ public class GetMyUser : EndpointGroupBase
|
|||||||
{
|
{
|
||||||
app.MapGroup(this)
|
app.MapGroup(this)
|
||||||
.RequireAuthorization()
|
.RequireAuthorization()
|
||||||
.MapGet(GetCurrentUser);
|
.MapGet(GetCurrentUser)
|
||||||
|
.MapGet(GetCurrentUserProfilePicture, "profile-picture");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<UserDto> GetCurrentUser(ISender sender, [AsParameters] GetCurrentUserQuery query)
|
private static async Task<UserDto> GetCurrentUser(ISender sender, [AsParameters] GetCurrentUserQuery query)
|
||||||
{
|
{
|
||||||
return await sender.Send(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)
|
app.MapGroup(this)
|
||||||
.MapPost(CreateUser)
|
.MapPost(CreateUser)
|
||||||
.MapPost(Login, "/login")
|
.MapPost(Login, "/login")
|
||||||
|
.MapPost(UploadProfilePicture, "/upload-profile-picture")
|
||||||
.MapGet(GetMinimalUser);
|
.MapGet(GetMinimalUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,4 +28,13 @@ public class Users : EndpointGroupBase
|
|||||||
{
|
{
|
||||||
return await sender.Send(command);
|
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": {
|
"/api/JoinUs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"/api/WeatherForecasts": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"PaginatedListOfFutureCreatorListDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user