Skip to content

Commit

Permalink
Merge pull request #26 from quizer-app/develop
Browse files Browse the repository at this point in the history
Add image service
  • Loading branch information
EloToJaa authored Jan 9, 2024
2 parents 62f5ccd + 04bf160 commit 066e2e6
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/.vs
**/bin
**/obj
**/logs
**/logs
**/wwwroot
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY . .
RUN chmod +x ./scripts/move_image_files.sh
RUN ./scripts/move_image_files.sh
RUN dotnet restore "./src/Quizer.Api/Quizer.Api.csproj"
RUN dotnet publish "./src/Quizer.Api/Quizer.Api.csproj" -c $BUILD_CONFIGURATION -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app

VOLUME /app/logs
VOLUME /app/wwwroot

WORKDIR /app
EXPOSE 8080
EXPOSE 8081
COPY --from=build /app .
COPY --from=build /files .

ENTRYPOINT ["dotnet", "Quizer.Api.dll"]
Binary file added images/quiz.webp
Binary file not shown.
27 changes: 27 additions & 0 deletions scripts/move_image_files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

# Set the source and destination directories
source_dir="/src/images"
destination_dir="/files/wwwroot/images"

# Create the destination directory if it doesn't exist
mkdir -p "$destination_dir"

# Loop through .webp files in the source directory
for webp_file in "$source_dir"/*.webp; do
if [ -f "$webp_file" ]; then
# Extract the name (without extension) from the file
name_without_extension=$(basename "$webp_file" .webp)

# Create a subdirectory for each file in the destination directory
subdestination_dir="$destination_dir/$name_without_extension"
mkdir -p "$subdestination_dir"

# Move the .webp file to the subdirectory with the name "default.webp"
mv "$webp_file" "$subdestination_dir/default.webp"

echo "File '$webp_file' moved to '$subdestination_dir/default.webp'"
fi
done

echo "Files moved successfully."
1 change: 1 addition & 0 deletions src/Quizer.Api/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static WebApplication UsePresentation(this WebApplication app, Configurat

app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();

app.MapControllers();
app.Run();
Expand Down
2 changes: 0 additions & 2 deletions src/Quizer.Api/Common/Mapping/QuizMappingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
using Quizer.Contracts.Question;
using Quizer.Application.Quizes.Common;
using Quizer.Domain.QuestionAggregate.Entities;
using Quizer.Contracts.Common;
using Quizer.Application.Utils;

namespace Quizer.Api.Common.Mapping;

Expand Down
31 changes: 30 additions & 1 deletion src/Quizer.Api/Controllers/V1/QuizController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using Quizer.Application.Quizes.Commands.CreateQuiz;
using Quizer.Application.Quizes.Commands.DeleteQuiz;
using Quizer.Application.Quizes.Commands.UpdateQuiz;
using Quizer.Application.Quizes.Commands.UpdateQuizImage;
using Quizer.Application.Quizes.Queries.GetQuizById;
using Quizer.Application.Quizes.Queries.GetQuizByName;
using Quizer.Application.Quizes.Queries.GetQuizes;
using Quizer.Application.Services.Image;
using Quizer.Contracts.Common;
using Quizer.Contracts.Quiz;
using System.Security.Claims;
Expand All @@ -20,11 +22,13 @@ public class QuizController : ApiController
{
private readonly ISender _mediator;
private readonly IMapper _mapper;
private readonly IImageService _imageService;

public QuizController(ISender mediator, IMapper mapper)
public QuizController(ISender mediator, IMapper mapper, IImageService imageService)
{
_mediator = mediator;
_mapper = mapper;
_imageService = imageService;
}

[HttpGet]
Expand Down Expand Up @@ -102,6 +106,31 @@ public async Task<IActionResult> UpdateQuiz(
Problem);
}

[AllowAnonymous]
[HttpGet("{quizId:guid}/image")]
public async Task<IActionResult> GetQuizImage([FromRoute] Guid quizId)
{
string imagePath = _imageService.GetImagePath("quiz", quizId);

var imageData = System.IO.File.ReadAllBytes(imagePath);

return File(imageData, "image/webp");
}

[AllowAnonymous]
[HttpPut("{quizId:guid}/image")]
public async Task<IActionResult> UpdateQuizImage(
[FromRoute] Guid quizId,
[FromForm(Name = "Data")] IFormFile file)
{
var command = new UpdateQuizImageCommand(quizId, file);
var result = await _mediator.Send(command);

return result.Match(
quizId => Ok(_mapper.Map<QuizIdResponse>(quizId)),
Problem);
}

[HttpDelete("{quizId:guid}")]
public async Task<IActionResult> DeleteQuiz(Guid quizId)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Quizer.Api/Quizer.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.0.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
1 change: 1 addition & 0 deletions src/Quizer.Application/Quizer.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="FluentEmail.SendGrid" Version="3.0.2" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.5.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
using ErrorOr;
using MediatR;
using Quizer.Application.Common.Interfaces.Persistance;
using Quizer.Application.Quizes.Commands.UpdateQuiz;
using Quizer.Application.Services.Slugify;
using Quizer.Domain.Common.Errors;
using Quizer.Domain.QuizAggregate;

namespace Quizer.Application.Quizes.Commands.DeleteQuiz;
namespace Quizer.Application.Quizes.Commands.UpdateQuiz;

public class UpdateQuizCommandHandler : IRequestHandler<UpdateQuizCommand, ErrorOr<QuizId>>
public class UpdateQuizImageCommandHandler : IRequestHandler<UpdateQuizCommand, ErrorOr<QuizId>>
{
private readonly IQuizRepository _quizRepository;
private readonly ISlugifyService _slugifyService;

public UpdateQuizCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService)
public UpdateQuizImageCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService)
{
_quizRepository = quizRepository;
_slugifyService = slugifyService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ErrorOr;
using MediatR;
using Microsoft.AspNetCore.Http;
using Quizer.Domain.QuizAggregate;

namespace Quizer.Application.Quizes.Commands.UpdateQuizImage;

public record UpdateQuizImageCommand(
Guid QuizId,
IFormFile File) : IRequest<ErrorOr<QuizId>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using ErrorOr;
using MediatR;
using Microsoft.AspNetCore.Http;
using Quizer.Application.Services.Image;
using Quizer.Domain.Common.Errors;
using Quizer.Domain.QuizAggregate;

namespace Quizer.Application.Quizes.Commands.UpdateQuizImage;

public class UpdateQuizImageCommandHandler : IRequestHandler<UpdateQuizImageCommand, ErrorOr<QuizId>>
{
private readonly IImageService _imageService;

public UpdateQuizImageCommandHandler(IImageService imageService)
{
_imageService = imageService;
}

public async Task<ErrorOr<QuizId>> Handle(UpdateQuizImageCommand request, CancellationToken cancellationToken)
{
string[] correctExtensions = { ".jpg", ".jpeg", ".png", ".webp" };

if (!IsCorrectExtension(request.File.FileName, correctExtensions))
return Errors.Image.WrongFormat(correctExtensions);

var id = QuizId.Create(request.QuizId);

string tempFilePath = await SaveTempFile(request.File);

string imageDir = _imageService.GetImageDir("quiz");

var imageProcessResult = ProcessImage(tempFilePath, imageDir, id.Value);
if (imageProcessResult.IsError)
return imageProcessResult.Errors;

return id;
}

private ErrorOr<string> ProcessImage(string tempFilePath, string imageDir, Guid id)
{
string? imagePath = _imageService.FormatAndMove(tempFilePath, imageDir, id);
if (imagePath is null)
return Errors.Image.CannotUpload;

bool resized = _imageService.Resize(imagePath, 512, 288);
if (!resized)
return Errors.Image.CannotUpload;

return imagePath;
}

private bool IsCorrectExtension(string fileName, string[] correctExtensions)
{
string extension = Path.GetExtension(fileName);

return correctExtensions.Contains(extension);
}

private async Task<string> SaveTempFile(IFormFile file)
{
string tempFilePath = Path.GetTempFileName();
using (var stream = new FileStream(tempFilePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}

return tempFilePath;
}
}
2 changes: 2 additions & 0 deletions src/Quizer.Application/Services/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Diacritics;
using Microsoft.Extensions.DependencyInjection;
using Quizer.Application.Services.Email;
using Quizer.Application.Services.Image;
using Quizer.Application.Services.Slugify;

namespace Quizer.Application.Services;
Expand All @@ -12,6 +13,7 @@ public static IServiceCollection AddServices(this IServiceCollection services)
services.AddSingleton<IDiacriticsMapper, DefaultDiacriticsMapper>();
services.AddSingleton<ISlugifyService, SlugifyService>();
services.AddSingleton<IEmailService, EmailService>();
services.AddSingleton<IImageService, ImageService>();

return services;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Quizer.Application/Services/Image/IImageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

namespace Quizer.Application.Services.Image
{
public interface IImageService
{
string? FormatAndMove(string filePathIn, string dirPathOut, Guid id);
bool Optimize(string filePath);
bool Resize(string filePath, int width, int height);
string GetImageDir(string imageType);
string GetImagePath(string imageType, Guid id);
}
}
89 changes: 89 additions & 0 deletions src/Quizer.Application/Services/Image/ImageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using ImageMagick;
using Microsoft.Extensions.Logging;

namespace Quizer.Application.Services.Image;

public class ImageService : IImageService
{
private readonly ILogger<ImageService> _logger;
private readonly ImageOptimizer _optimizer;

public ImageService(ILogger<ImageService> logger)
{
_optimizer = new ImageOptimizer();
_logger = logger;
}

public string? FormatAndMove(string filePathIn, string dirPathOut, Guid id)
{
try
{
string fileName = $"{id}.webp";
string filePathOut = Path.Combine(dirPathOut, fileName);

using var image = new MagickImage(filePathIn);
image.Format = MagickFormat.WebP;

image.Write(filePathOut);

return filePathOut;
}
catch (Exception ex)
{
_logger.LogError("Error occured during image format: {@Exception}", ex);
return null;
}
}

public bool Resize(string filePath, int width, int height)
{
try
{
using var image = new MagickImage(filePath);
image.Resize(width, height);
image.Write(filePath);
}
catch (Exception ex)
{
_logger.LogError("Error occured during image resize: {@Exception}", ex);
return false;
}
return true;
}

public bool Optimize(string filePath)
{
try
{
var fileInfo = new FileInfo(filePath);
_optimizer.LosslessCompress(fileInfo);
}
catch (Exception ex)
{
_logger.LogError("Error occured during image optimize: {@Exception}", ex);
return false;
}
return true;
}

public string GetImageDir(string imageType)
{
string imageDir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images", imageType);
if (!Directory.Exists(imageDir))
Directory.CreateDirectory(imageDir);

return imageDir;
}

public string GetImagePath(string imageType, Guid id)
{
string imageDir = GetImageDir(imageType);
string filePath = Path.Combine(imageDir, $"{id}.webp");
if (!File.Exists(filePath))
{
filePath = Path.Combine(imageDir, $"default.webp");
}

return filePath;
}
}
17 changes: 17 additions & 0 deletions src/Quizer.Domain/Common/Errors/Errors.Image.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using ErrorOr;

namespace Quizer.Domain.Common.Errors;

public static partial class Errors
{
public static class Image
{
public static Error CannotUpload => Error.Failure(
code: "Image.CannotUpload",
description: "There was an error during image upload");

public static Error WrongFormat(string[] formats) => Error.Validation(
code: "Image.WrongFormat",
description: $"Image has wrong format. Accepted formats: {string.Join(", ", formats)}");
}
}
Loading

0 comments on commit 066e2e6

Please sign in to comment.