#67 added blob storage service + endpoints for profile picture. WIP

This commit is contained in:
Dominic Villemure
2024-06-23 20:24:15 -04:00
parent 0a9c3ec94c
commit ab88511f22
15 changed files with 338 additions and 2 deletions

View File

@@ -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" />

View File

@@ -0,0 +1,6 @@
namespace Hutopy.Application.AzureBlobStorage.Constants;
public static class CommonFileNames
{
public static string ProfilePicture = "profilePicture";
}

View File

@@ -0,0 +1,6 @@
namespace Hutopy.Application.AzureBlobStorage.Constants;
public static class ContainerNames
{
public static string Users = "users";
}

View File

@@ -0,0 +1,7 @@
namespace Hutopy.Application.AzureBlobStorage.Constants;
public static class SubDirectoryNames
{
public static string Profile = "profile";
public static string Posts = "posts";
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View 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);
}
}
}

View 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
│ │ └── ...
└── ...

View File

@@ -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 =>

View File

@@ -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" />

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,