Added simple support for image upload and requesting
This commit is contained in:
parent
4a4da6b8ee
commit
f26f961e79
58
Wave/Components/Pages/Upload.razor
Normal file
58
Wave/Components/Pages/Upload.razor
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
@page "/upload"
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Wave.Services
|
||||||
|
@using System.IO.Pipelines
|
||||||
|
|
||||||
|
@attribute [Authorize]
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@inject ImageService ImageService
|
||||||
|
|
||||||
|
<PageTitle>Upload</PageTitle>
|
||||||
|
|
||||||
|
<h3 class="title mb-3">Upload</h3>
|
||||||
|
@if (Busy) {
|
||||||
|
<progress class="progress progress-primary w-56"></progress>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<p>@Message</p>
|
||||||
|
@if (Path is not null) {
|
||||||
|
<img src="@Path" type="@ImageService.ImageMimeType" width="800" alt="" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputFile OnChange="@LoadFiles" accept="image/png, image/jpeg, image/webp" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private const long MaxFileSize = 1024 * 1024 * 5; // 5MB should be fine
|
||||||
|
private string Message { get; set; } = string.Empty;
|
||||||
|
private string? Path { get; set; }
|
||||||
|
private bool Busy { get; set; }
|
||||||
|
|
||||||
|
private async Task LoadFiles(InputFileChangeEventArgs args) {
|
||||||
|
Busy = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
string tempName = System.IO.Path.GetRandomFileName();
|
||||||
|
string tempDirectory = System.IO.Path.Combine(".", "files", "temp");
|
||||||
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
string tempPath = System.IO.Path.Combine(tempDirectory, tempName);
|
||||||
|
await using var fs = new FileStream(tempPath, FileMode.Create);
|
||||||
|
await args.File.OpenReadStream(MaxFileSize).CopyToAsync(fs);
|
||||||
|
|
||||||
|
var guid = await ImageService.StoreImageAsync(tempPath);
|
||||||
|
|
||||||
|
if (guid is null) {
|
||||||
|
Message = "Image upload failed";
|
||||||
|
} else {
|
||||||
|
Message = "Image uploaded successfully";
|
||||||
|
Path = "/images/" + guid;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Message = $"{ex.Message} ({ex.GetType().Name}).";
|
||||||
|
} finally {
|
||||||
|
Busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
Wave/Controllers/ImageController.cs
Normal file
20
Wave/Controllers/ImageController.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Wave.Services;
|
||||||
|
|
||||||
|
namespace Wave.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/images")]
|
||||||
|
public class ImageController(ImageService imageService) : ControllerBase {
|
||||||
|
private ImageService ImageService { get; } = imageService;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{imageId:guid}")]
|
||||||
|
public IActionResult Get(Guid imageId) {
|
||||||
|
string? path = ImageService.GetPath(imageId);
|
||||||
|
|
||||||
|
if (path is null) return NotFound();
|
||||||
|
return File(System.IO.File.OpenRead(path), ImageService.ImageMimeType);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
USER app
|
USER app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN mkdir ./files && chown app ./files
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
@ -21,5 +22,4 @@ RUN dotnet publish "./Wave.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:Us
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
RUN mkdir ./files
|
|
||||||
ENTRYPOINT ["dotnet", "Wave.dll"]
|
ENTRYPOINT ["dotnet", "Wave.dll"]
|
50
Wave/Services/ImageService.cs
Normal file
50
Wave/Services/ImageService.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using ImageMagick;
|
||||||
|
|
||||||
|
namespace Wave.Services;
|
||||||
|
|
||||||
|
public class ImageService(ILogger<ImageService> logger) {
|
||||||
|
private ILogger<ImageService> Logger { get; } = logger;
|
||||||
|
private const string BasePath = "./files/images";
|
||||||
|
private const string ImageExtension = ".jpg";
|
||||||
|
public string ImageMimeType => "image/jpg";
|
||||||
|
|
||||||
|
public string? GetPath(Guid imageId) {
|
||||||
|
string path = Path.Combine(BasePath, imageId + ImageExtension);
|
||||||
|
return File.Exists(path) ? path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Guid?> StoreImageAsync(string temporaryPath, int size = 800, CancellationToken cancellation = default) {
|
||||||
|
if (File.Exists(temporaryPath) is not true) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (File.Exists(BasePath) is not true) Directory.CreateDirectory(BasePath);
|
||||||
|
|
||||||
|
var image = new MagickImage();
|
||||||
|
await image.ReadAsync(temporaryPath, cancellation);
|
||||||
|
|
||||||
|
// Jpeg with 90% compression should look decent
|
||||||
|
image.Resize(new MagickGeometry(size)); // this preserves aspect ratio
|
||||||
|
image.Format = MagickFormat.Jpeg;
|
||||||
|
image.Quality = 90;
|
||||||
|
|
||||||
|
if (image.GetExifProfile() is not null) {
|
||||||
|
image.RemoveProfile(image.GetExifProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite exif for privacy reasons
|
||||||
|
var exif = new ExifProfile {
|
||||||
|
Parts = ExifParts.None
|
||||||
|
};
|
||||||
|
exif.CreateThumbnail();
|
||||||
|
image.SetProfile(exif);
|
||||||
|
|
||||||
|
var guid = Guid.NewGuid();
|
||||||
|
string path = Path.Combine(BasePath, guid + ImageExtension);
|
||||||
|
await image.WriteAsync(path, cancellation);
|
||||||
|
return guid;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogInformation(ex, "Failed to process uploaded image.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="13.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||||
|
@ -19,6 +20,8 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Controllers\" />
|
||||||
|
<Folder Include="Services\" />
|
||||||
<Folder Include="wwwroot\img\" />
|
<Folder Include="wwwroot\img\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
2
Wave/wwwroot/css/main.min.css
vendored
2
Wave/wwwroot/css/main.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue