Skip to content

Commit

Permalink
Feature/images (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
FilipKolodziejczyk authored Jun 15, 2023
2 parents 16c5c20 + de4c9ac commit 1c459cf
Show file tree
Hide file tree
Showing 32 changed files with 525 additions and 44 deletions.
57 changes: 57 additions & 0 deletions backend/SoftwareEngineering2/Controllers/ImagesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SoftwareEngineering2.DTO;
using Swashbuckle.AspNetCore.Annotations;
using SoftwareEngineering2.Interfaces;

namespace SoftwareEngineering2.Controllers {
[Route("api/images")]
[ApiController]
public class ImagesController : ControllerBase {
private readonly IImageService _imageService;

public ImagesController(IImageService imageService, IProductService productService) {
_imageService = imageService;
}

// POST: api/images
[HttpPost]
[SwaggerOperation(Summary = "Upload new image")]
[SwaggerResponse(401, "Unauthorised")]
[SwaggerResponse(201, "Created")]
[Authorize(Roles = Roles.Employee)]
public async Task<ActionResult<ImageDTO>> UploadImage([FromForm] NewImageDTO image) {
var result = await _imageService.UploadImageAsync(image);
return CreatedAtAction(nameof(UploadImage), new {id = result.ImageId}, result);
}

// GET: api/images/5
[HttpGet("{imageId:int}")]
[SwaggerOperation(Summary = "Fetch a specific image")]
[SwaggerResponse(200, "Returns an image", typeof(ImageDTO))]
[SwaggerResponse(404, "Image not found")]
public async Task<IActionResult> GetImageById(int imageId) {
var image = await _imageService.GetImageByIdAsync(imageId);
return image != null ?
Ok(image) :
NotFound(new { message = $"No image found with id {imageId}" });
}

// DELETE: api/images/5
[HttpDelete("{imageId:int}")]
[SwaggerOperation(Summary = "Delete a specific image")]
[SwaggerResponse(204, "No Content")]
[SwaggerResponse(404, "Image not found")]
[Authorize(Roles = Roles.Employee)]
public async Task<IActionResult> DeleteImage(int imageId) {
try {
await _imageService.DeleteImageAsync(imageId);
}
catch (KeyNotFoundException e) {
return NotFound(new { message = $"No image found with id {imageId}" });
}

return NoContent();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ public async Task<IActionResult> Add([FromBody] NewProductDTO productModel) {
return BadRequest(new { message = "No name or description provided" });
}
//add to db
var result = await _productService.CreateModelAsync(productModel);
ProductDTO result;
try {
result = await _productService.CreateModelAsync(productModel);
} catch (KeyNotFoundException e) {
return BadRequest(new { message = e.Message });
}

return CreatedAtAction(nameof(Add), new { id = result.ProductID }, result);
}

Expand Down
7 changes: 7 additions & 0 deletions backend/SoftwareEngineering2/DTO/ImageDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SoftwareEngineering2.DTO;

public record ImageDTO() {
public int ImageId { get; init; }

public Uri ImageUri { get; init; }
}
5 changes: 5 additions & 0 deletions backend/SoftwareEngineering2/DTO/NewImageDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace SoftwareEngineering2.DTO;

public record NewImageDTO() {
public IFormFile Image { get; init; }
};
4 changes: 2 additions & 2 deletions backend/SoftwareEngineering2/DTO/NewProductDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace SoftwareEngineering2.DTO;
public record NewProductDTO() {
public string Name { get; init; }
public string Description { get; init; }
public string Image { get; init; } //to discuss
public List<int> ImageIds { get; init; }
public decimal Price { get; init; }
public int Quantity { get; init; }
public string Category{ get; init; }
public string Category { get; init; }
}
4 changes: 3 additions & 1 deletion backend/SoftwareEngineering2/DTO/ProductDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public record ProductDTO() {

public string? Description { get; init; }

public string Image { get; init; }
public List<int> ImageIds { get; init; }

public List<Uri> ImageUris { get; init; }

public bool Archived { get; init; }

Expand Down
2 changes: 1 addition & 1 deletion backend/SoftwareEngineering2/DTO/UpdateProductDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public record UpdateProductDTO() {

public string Description { get; init; }

public string Image { get; init; } //to discuss
public List<int> ImageIds { get; init; }

public decimal Price { get; init; }

Expand Down
1 change: 1 addition & 0 deletions backend/SoftwareEngineering2/FlowerShopContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public FlowerShopContext(DbContextOptions<FlowerShopContext> options) : base(opt
public DbSet<OrderModel> OrderModels { get; set; }
public DbSet<ProductModel> ProductModels { get; set; }
public DbSet<BasketItemModel> BasketItemModels { get; set; }
public DbSet<ImageModel> ImageModels { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.ApplyConfigurationsFromAssembly(typeof(FlowerShopContext).Assembly);
Expand Down
11 changes: 11 additions & 0 deletions backend/SoftwareEngineering2/Interfaces/IImageRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using SoftwareEngineering2.Models;

namespace SoftwareEngineering2.Interfaces;

public interface IImageRepository {
public Task AddAsync(ImageModel image);

Task<ImageModel?> GetByIdAsync(int id);

void Delete(ImageModel image);
}
11 changes: 11 additions & 0 deletions backend/SoftwareEngineering2/Interfaces/IImageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using SoftwareEngineering2.DTO;

namespace SoftwareEngineering2.Interfaces;

public interface IImageService {
Task<ImageDTO> UploadImageAsync(NewImageDTO image);

Task<ImageDTO?> GetImageByIdAsync(int imageId);

Task DeleteImageAsync(int imageId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SoftwareEngineering2.Models;

namespace SoftwareEngineering2.ModelEntityTypeConfiguration;

public class ImageModelEntityConfiguration : IEntityTypeConfiguration<ImageModel> {
public void Configure(EntityTypeBuilder<ImageModel> builder) {
builder.HasKey(x => x.ImageId);
builder.Property(x => x.ImageUri).IsRequired();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using System.Drawing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SoftwareEngineering2.Models;

Expand Down
11 changes: 11 additions & 0 deletions backend/SoftwareEngineering2/Models/ImageModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace SoftwareEngineering2.Models;

public class ImageModel {
public int ImageId { get; set; }

public Uri ImageUri { get; set; }

public List<ProductModel> Products { get; set; }
}
4 changes: 2 additions & 2 deletions backend/SoftwareEngineering2/Models/ProductModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ public class ProductModel {
[Required]
public string? Description { get; set; }

public string Image { get; set; }

[Required]
public bool Archived { get; set; }

public string Category { get; set; }

public List<ImageModel> Images { get; set; }

public ICollection<OrderDetailsModel>? OrderDetails { get; set; }
public ICollection<BasketItemModel>? BasketItems { get; set; }
Expand Down
9 changes: 8 additions & 1 deletion backend/SoftwareEngineering2/Profiles/AutoMapperProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ public AutoMapperProfile() {
CreateMap<NewProductDTO, ProductModel>()
.ForMember(dest => dest.Archived, opt => opt.MapFrom(src => false));

CreateMap<ProductModel, ProductDTO>();
CreateMap<ProductModel, ProductDTO>()
.IncludeAllDerived()
.ForMember(dest => dest.ImageUris, opt => opt.MapFrom(src => src.Images))
.ForMember(dest => dest.ImageIds, opt => opt.MapFrom(src => src.Images));
CreateMap<ImageModel, Uri>().ConstructUsing(image => image.ImageUri);
CreateMap<ImageModel, int>().ConstructUsing(image => image.ImageId);

CreateMap<ImageModel, ImageDTO>();

CreateMap<UpdateProductDTO, ProductModel>();

Expand Down
10 changes: 10 additions & 0 deletions backend/SoftwareEngineering2/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Data;
using Amazon;
using AutoMapper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -84,6 +86,14 @@
builder.Services.AddTransient<IOrderService, OrderService>();
builder.Services.AddTransient<IOrderModelRepository, OrderModelRepository>();
builder.Services.AddTransient<IOrderDetailsModelRepository, OrderDetailsModelRepository>();
builder.Services.AddTransient<IImageRepository, ImageRepository>();
builder.Services.AddTransient<IImageService, ImageService>(_ => new ImageService(
_.GetRequiredService<IUnitOfWork>(),
_.GetRequiredService<IImageRepository>(),
Environment.GetEnvironmentVariable("IMAGE_BUCKET_NAME")!,
RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable("AWS_REGION")!),
_.GetRequiredService<IMapper>()
));
builder.Services.AddTransient<IBasketService, BasketService>();
builder.Services.AddTransient<IBasketItemModelRepository, BasketItemModelRepository>();

Expand Down
26 changes: 26 additions & 0 deletions backend/SoftwareEngineering2/Repositories/ImageRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using SoftwareEngineering2.Interfaces;
using SoftwareEngineering2.Models;

namespace SoftwareEngineering2.Repositories;

public class ImageRepository : IImageRepository {
private readonly FlowerShopContext _context;

public ImageRepository(FlowerShopContext context) {
_context = context;
}

public async Task AddAsync(ImageModel image) {
await _context.ImageModels.AddAsync(image);
}

public async Task<ImageModel?> GetByIdAsync(int id) {
return await _context.ImageModels
.FirstOrDefaultAsync(image => image.ImageId == id);
}

public void Delete(ImageModel image) {
_context.ImageModels.Remove(image);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public async Task AddAsync(ProductModel product)
public async Task<ProductModel?> GetByIdAsync(int id)
{
return await _context.ProductModels
.Include(product => product.Images)
.FirstOrDefaultAsync(product => product.ProductID == id);
}

Expand All @@ -31,6 +32,7 @@ public void Delete(ProductModel product)
public async Task<IEnumerable<ProductModel>> GetAllFilteredAsync(string searchQuery, string filteredCategory, int pageNumber, int elementsOnPage)
{
return await _context.ProductModels
.Include(product => product.Images)
.Where(product => product.Name.Contains(searchQuery))
.Where(product => product.Category.Contains(filteredCategory))
.Skip(elementsOnPage * (pageNumber - 1))
Expand Down
88 changes: 88 additions & 0 deletions backend/SoftwareEngineering2/Services/ImageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using AutoMapper;
using SoftwareEngineering2.DTO;
using SoftwareEngineering2.Interfaces;
using SoftwareEngineering2.Models;

namespace SoftwareEngineering2.Services;

public class ImageService : IImageService {
private readonly IUnitOfWork _unitOfWork;
private readonly IImageRepository _imageRepository;
private IAmazonS3 _s3Client;
private readonly string _bucketName;
private readonly IMapper _mapper;

public ImageService(
IUnitOfWork unitOfWork,
IImageRepository imageRepository,
string bucketName,
RegionEndpoint regionEndpoint,
IMapper mapper) {
_unitOfWork = unitOfWork;
_imageRepository = imageRepository;
_bucketName = bucketName;
_s3Client = new AmazonS3Client(regionEndpoint);
_mapper = mapper;
}

public async Task<ImageDTO> UploadImageAsync(NewImageDTO image) {
var datatype = image.Image.FileName.Split('.').Last();
if (datatype != "png" && datatype != "jpg" && datatype != "jpeg") {
throw new Exception("Invalid image type");
}

var filename = Guid.NewGuid().ToString() + "." + datatype;
var path = Path.Combine(Path.GetTempPath(), filename);
await using (var stream = new FileStream(path, FileMode.Create)) {
await image.Image.CopyToAsync(stream);
}

var uploadRequest = new PutObjectRequest() {
BucketName = _bucketName,
Key = filename,
ContentType = "image/" + datatype,
FilePath = path
};

var response = await _s3Client.PutObjectAsync(uploadRequest);
if (response.HttpStatusCode != System.Net.HttpStatusCode.OK) {
throw new Exception("Failed to upload image");
}

var model = new ImageModel() {
ImageUri = new Uri($"https://{_bucketName}.s3.amazonaws.com/{uploadRequest.Key}")
};

await _imageRepository.AddAsync(model);
await _unitOfWork.SaveChangesAsync();
return _mapper.Map<ImageDTO>(model);
}

public async Task<ImageDTO?> GetImageByIdAsync(int imageId) {
var result = await _imageRepository.GetByIdAsync(imageId);
return result != null ?
_mapper.Map<ImageDTO>(result) :
null;
}

public async Task DeleteImageAsync(int imageId) {
var image = await _imageRepository.GetByIdAsync(imageId) ?? throw new KeyNotFoundException("Image not found");
var objectName = image.ImageUri.Segments.Last();

var request = new DeleteObjectRequest {
BucketName = _bucketName,
Key = objectName
};

var response = await _s3Client.DeleteObjectAsync(request);
if (response.HttpStatusCode != System.Net.HttpStatusCode.Accepted && response.HttpStatusCode != System.Net.HttpStatusCode.NoContent) {
throw new Exception("Failed to delete image");
}

_imageRepository.Delete(image);
await _unitOfWork.SaveChangesAsync();
}
}
Loading

0 comments on commit 1c459cf

Please sign in to comment.