Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File Upload Infrastructure #330

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using AirBnB.Persistence.Interceptors;
using AirBnB.Application.Currencies.Services;
using AirBnB.Application.StorageFiles.Brokers;
using AirBnB.Application.StorageFiles.Services;
using AirBnB.Infrastructure.Currencies.Services;
using AirBnB.Infrastructure.StorageFiles.Brokers;
using AirBnB.Infrastructure.StorageFiles.Services;

namespace AirBnB.Api.Configurations;

Expand Down Expand Up @@ -261,11 +265,27 @@ private static WebApplicationBuilder AddIdentityInfrastructure(this WebApplicati
/// <returns></returns>
private static WebApplicationBuilder AddStorageFileInfrastructure(this WebApplicationBuilder builder)
{
// configure settings
builder.Services.Configure<StorageFileSettings>(builder.Configuration.GetSection(nameof(StorageFileSettings)));

// register brokers
builder.Services
.AddScoped<IFileBroker, FileBroker>()
.AddScoped<IDirectoryBroker, DirectoryBroker>();

// register repositories
builder.Services
.AddScoped<IStorageFileRepository, StorageFileRepository>()
.AddScoped<IStorageFileService, StorageFileService>();
.AddScoped<IListingMediaFileRepository, ListingMediaFileRepository>()
.AddScoped<IUserProfileMediaFileRepository, UserProfileMediaFileRepository>();

// register services
builder.Services
.AddScoped<IStorageFileService, StorageFileService>()
.AddScoped<IFileService, FileService>()
.AddScoped<IFileProcessingService, FileProcessingService>()
.AddScoped<IListingMediaFileService, ListingMediaFileService>()
.AddScoped<IUserProfileMediaFileService, UserProfileMediaFileService>();

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using AirBnB.Api.Models.DTOs;
using AirBnB.Application.Common.Identity.Queries;
using AirBnB.Application.Common.Identity.Services;
using AirBnB.Application.StorageFiles.Models;
using AirBnB.Application.StorageFiles.Services;
using AirBnB.Domain.Brokers;
using AirBnB.Domain.Common.Query;
using AirBnB.Domain.Entities;
using AirBnB.Domain.Enums;
using AirBnB.Persistence.Repositories.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Http.HttpResults;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
Expand All @@ -12,7 +18,7 @@ namespace AirBnB.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AccountsController(IMediator mediator, IUserService userService, IMapper mapper) : ControllerBase
public class AccountsController(IMediator mediator, IUserService userService, IMapper mapper, IRequestUserContextProvider requestUserContextProvider, IUserProfileMediaFileService userProfileMediaFileService) : ControllerBase
{
#region Users

Expand Down Expand Up @@ -71,6 +77,20 @@ public async ValueTask<IActionResult> Create(
return Ok(mapper.Map<UserDto>(result));
}

[HttpPost("profilePictures")]
public async ValueTask<IActionResult> UploadProfilePicture([FromForm] IFormFile profilePicture,
CancellationToken cancellationToken = default)
{
var uploadFileInfo = mapper.Map<UploadFileInfoDto>(profilePicture);
uploadFileInfo.StorageFileType = StorageFileType.UserProfileImage;
uploadFileInfo.OwnerId = requestUserContextProvider.GetUserId();

var result =
await userProfileMediaFileService.CreateAsync(uploadFileInfo, cancellationToken: cancellationToken);

return Ok(mapper.Map<UserProfilePictureDto>(result));
}

[HttpPut]
public async ValueTask<IActionResult> Update(
[FromBody] UserDto userDto,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
using AirBnB.Application.Common.Identity.Services;
using AirBnB.Application.Listings.Models;
using AirBnB.Application.Listings.Services;
using AirBnB.Application.StorageFiles.Models;
using AirBnB.Application.StorageFiles.Services;
using AirBnB.Domain.Common.Query;
using AirBnB.Domain.Entities;
using AirBnB.Domain.Enums;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
Expand All @@ -12,7 +15,10 @@ namespace AirBnB.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ListingsController(IListingService listingService, IMapper mapper) : ControllerBase
public class ListingsController(
IListingService listingService,
IListingMediaFileService listingMediaFileService,
IMapper mapper) : ControllerBase
{
[HttpGet]
public ValueTask<IActionResult> Get([FromQuery] FilterPagination filterPagination)
Expand All @@ -24,11 +30,20 @@ public ValueTask<IActionResult> Get([FromQuery] FilterPagination filterPaginatio

[HttpGet("category/{categoryId:guid}")]
public async ValueTask<IActionResult> GetListingByCategoryId([FromQuery] FilterPagination filterPagination,
[FromRoute] Guid categoryId)
[FromRoute] Guid categoryId, CancellationToken cancellationToken = default)
{
var listings = await listingService.GetByCategoryId(filterPagination, categoryId, true).ToListAsync();
var listings = await listingService.GetByCategoryId(filterPagination, categoryId, true).ToListAsync(cancellationToken);
return listings.Count != 0 ? Ok(mapper.Map<List<ListingDto>>(listings)) : NoContent();
}

[HttpGet("listingImages/{listingId:guid}")]
public IActionResult GetListingImagesByListingId([FromRoute] Guid listingId,
CancellationToken cancellationToken = default)
{
var result = listingMediaFileService.GetListingMediaFilesByListingId(listingId);

return Ok(mapper.Map<List<ListingMediaFileDto>>(result));
}

[HttpGet("{listingId:guid}")]
public async ValueTask<IActionResult> GetListingById([FromRoute]Guid listingId, bool asNoTracking = false,
Expand All @@ -51,14 +66,40 @@ public async ValueTask<IActionResult> GetListingCategories(

[HttpPost]
public async ValueTask<IActionResult> CreateAsync([FromBody] ListingDto listingDto,
CancellationToken cancellationToken)
CancellationToken cancellationToken = default)
{
var listing = mapper.Map<Listing>(listingDto);
var result = await listingService.CreateAsync(listing, true, cancellationToken);

return Ok(mapper.Map<ListingDto>(result));
}

[HttpPost("listingImages/{listingId:guid}")]
public async ValueTask<IActionResult> UploadListingImage(
[FromForm] IFormFile listingImage,
[FromRoute] Guid listingId,
CancellationToken cancellationToken = default)
{
var listingFileInfo = mapper.Map<UploadFileInfoDto>(listingImage);
listingFileInfo.OwnerId = listingId;
listingFileInfo.StorageFileType = StorageFileType.ListingImage;

return Ok(await listingMediaFileService
.CreateAsync(listingFileInfo, cancellationToken: cancellationToken));
}

[HttpPut("listingImages")]
public async ValueTask<IActionResult> UpdateImageOrder(IEnumerable<ListingMediaFileDto> listingMediaFileDtos,
CancellationToken cancellationToken = default)
{
var mediaFiles = mapper.Map<List<ListingMediaFile>>(listingMediaFileDtos);

await listingMediaFileService
.ReorderListingMediaFiles(mediaFiles, cancellationToken: cancellationToken);

return NoContent();
}

[HttpPut]
public async ValueTask<IActionResult> UpdateAsync([FromBody] ListingDto listingDto,
CancellationToken cancellationToken)
Expand All @@ -77,4 +118,14 @@ public async ValueTask<IActionResult> DeleteListingById([FromRoute]Guid listingI

return listing is not null ? Ok(mapper.Map<ListingDto>(listing)) : NotFound();
}

[HttpDelete("listingImages/{listingMediaFileId:guid}")]
public async ValueTask<IActionResult> DeleteListingImageById([FromRoute] Guid listingMediaFileId,
CancellationToken cancellationToken = default)
{
await listingMediaFileService
.DeleteByIdAsync(listingMediaFileId, cancellationToken: cancellationToken);

return NoContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Bogus;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using NUnit.Framework;

namespace AirBnB.Api.Data;

Expand Down Expand Up @@ -161,7 +160,7 @@ private static async ValueTask SeedUsersAsync(this AppDbContext dbContext, IPass
PasswordHash = passwordHasherService.HashPassword(data.Internet.Password(8))
});

await dbContext.AddRangeAsync(hostFaker.Generate(10));
await dbContext.Users.AddRangeAsync(hostFaker.Generate(10));

// Add guests.
var guestRole = roles.First(role => role.Type == RoleType.Guest);
Expand All @@ -177,7 +176,7 @@ private static async ValueTask SeedUsersAsync(this AppDbContext dbContext, IPass
PasswordHash = passwordHasherService.HashPassword(data.Internet.Password(8))
});

await dbContext.AddRangeAsync(guestFaker.Generate(50));
await dbContext.Users.AddRangeAsync(guestFaker.Generate(10));

var hostGuestFaker = new Faker<User>()
.RuleFor(user => user.FirstName, data => data.Name.FirstName())
Expand All @@ -190,8 +189,8 @@ private static async ValueTask SeedUsersAsync(this AppDbContext dbContext, IPass
PasswordHash = passwordHasherService.HashPassword(data.Internet.Password(8))
});

await dbContext.AddRangeAsync(hostGuestFaker.Generate(10));
await dbContext.SaveChangesAsync();
await dbContext.Users.AddRangeAsync(hostGuestFaker.Generate(10));
dbContext.SaveChanges();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using AirBnB.Application.StorageFiles.Models;
using AutoMapper;

namespace AirBnB.Api.Mappers;

public class FormFileMapper : Profile
{
public FormFileMapper()
{
CreateMap<IFormFile, UploadFileInfoDto>()
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => src.OpenReadStream()))
.ForMember(dest => dest.Size, opt => opt.MapFrom(src => src.Length));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AirBnB.Api.Models.DTOs;
using AirBnB.Application.StorageFiles.Models;
using AirBnB.Domain.Entities;
using AirBnB.Infrastructure.StorageFiles.Mappers;
using AutoMapper;

namespace AirBnB.Api.Mappers;

public class ListingMediaFileMapper : Profile
{
public ListingMediaFileMapper()
{
CreateMap<UploadFileInfoDto, ListingMediaFile>()
.ForMember(dest => dest.ListingId, opt => opt.MapFrom(src => src.OwnerId));

CreateMap<ListingMediaFile, ListingMediaFileDto>()
.ForMember(dest => dest.ImageUrl,
opt => opt.ConvertUsing<StorageFileToUrlConverter, StorageFile>(src => src.StorageFile))
.ReverseMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AirBnB.Api.Models.DTOs;
using AirBnB.Application.StorageFiles.Models;
using AirBnB.Domain.Entities;
using AirBnB.Infrastructure.StorageFiles.Mappers;
using AutoMapper;

namespace AirBnB.Api.Mappers;

public class UserProfileMediaFileMapper : Profile
{
public UserProfileMediaFileMapper()
{
CreateMap<UploadFileInfoDto, UserProfileMediaFile>()
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.OwnerId));

CreateMap<UserProfileMediaFile, UserProfilePictureDto>()
.ForMember(dest => dest.ImageUrl,
opt => opt.ConvertUsing<StorageFileToUrlConverter, StorageFile>(src => src.StorageFile))
.ReverseMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace AirBnB.Api.Models.DTOs;

/// <summary>
/// Data Transfer Object (DTO) representing information about a listing media file.
/// </summary>
public class ListingMediaFileDto
{
/// <summary>
/// Gets or initializes the unique identifier of the listing media file.
/// </summary>
public Guid Id { get; init; }

/// <summary>
/// Gets or initializes the URL of the image associated with the listing media file.
/// </summary>
public string ImageUrl { get; init; } = default!;

/// <summary>
/// Gets or initializes the order number of the listing media file.
/// </summary>
public byte OrderNumber { get; init; }

/// <summary>
/// Gets or sets the unique identifier of the listing associated with the media file.
/// </summary>
public Guid ListingId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace AirBnB.Api.Models.DTOs;

/// <summary>
/// Data Transfer Object (DTO) representing a user profile picture.
/// </summary>
public class UserProfilePictureDto
{
/// <summary>
/// Gets or sets the unique identifier of the user profile picture.
/// </summary>
public Guid Id { get; set; }

/// <summary>
/// Gets or sets the URL of the user profile picture.
/// </summary>
public string ImageUrl { get; set; } = default!;

/// <summary>
/// Gets or sets the unique identifier of the user associated with this profile picture.
/// </summary>
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,24 @@
"LocationSettings": [
{
"StorageFileType": "ListingCategoryImage",
"AllowedImageExtensions": ["jpg", "jpeg", "svg"],
"MinimumImageSizeInBytes": 250000,
"MaximumImageSizeInBytes": 5000000,
"FolderPath": "Assets\\ListingCategories\\Images"
},
{
"StorageFileType": "ListingImage",
"AllowedImageExtensions": ["jpg", "jpeg", "png", "tiff", "tif", "heif", "heic", "svg"],
"MinimumImageSizeInBytes": 350000,
"MaximumImageSizeInBytes": 10500000,
"FolderPath": "Assets\\Listings\\Images"
},
{
"StorageFileType": "UserProfileImage",
"AllowedImageExtensions": ["jpg", "jpeg", "png", "tiff", "tif", "heif", "heic"],
"MinimumImageSizeInBytes": 250000,
"MaximumImageSizeInBytes": 7000000,
"FolderPath": "Assets\\UserProfileImages"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AirBnB.Application.StorageFiles.Brokers;

/// <summary>
/// Represents an interface that defines operations related to directory manipulation.
/// </summary>
public interface IDirectoryBroker
{
/// <summary>
/// Creates a DirectoryInfo for the specified directory path.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
DirectoryInfo CreateDirectory(string path);
}
Loading