.dockerignore
+3
-0
diff --git a/.dockerignore b/.dockerignore
index 7dce0b2..64e3cd5 100644
@@ -489,3 +489,6 @@ GitBrowser/wwwroot/js/vendor/
# Git daemon export file
git-daemon-export-ok
# Development files
blobs
MatDenDagen/Components/Pages/UploadTest.razor
+56
-0
diff --git a/MatDenDagen/Components/Pages/UploadTest.razor b/MatDenDagen/Components/Pages/UploadTest.razor
new file mode 100644
index 0000000..9aad47b
@@ -0,0 +1,56 @@
@page "/upload-test"
@using System.IO
@using MatDenDagen.Infrastructure.Storage.BlobStorage
@using Microsoft.AspNetCore.Http
@using System.Runtime.CompilerServices
@using Microsoft.AspNetCore.Mvc.Infrastructure
@using System.Threading
@inject BlobStorageService blobService
@implements IDisposable
<EditForm FormName="UploadTest" Model="@model" OnSubmit="@Submit" enctype="multipart/form-data" Enhance>
<p>
<label>
<span>Fil:</span>
<InputFile name="model.File" required />
</label>
</p>
<p>
<input type="submit" value="Ladda upp" />
</p>
</EditForm>
@if (uploadedId is not null) {
<p>Filen har id: <code>@uploadedId</code></p>
}
@code {
private readonly CancellationTokenSource cts = new();
[SupplyParameterFromForm]
private UploadTestModel? model { get; set; } = new();
private string? uploadedId { get; set; }
private async Task Submit()
{
if (model?.File is not { } file)
{
return;
}
var result = await blobService.SaveBlob(file.OpenReadStream(), cts.Token);
uploadedId = result.Id;
}
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
private sealed class UploadTestModel
{
public IFormFile? File { get; set; }
}
}
MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageOptions.cs
+51
-0
diff --git a/MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageOptions.cs b/MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageOptions.cs
new file mode 100644
index 0000000..75a9dee
@@ -0,0 +1,51 @@
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace MatDenDagen.Infrastructure.Storage.BlobStorage;
public sealed class BlobStorageOptions
{
public required string BlobDirectory { get; set; }
}
file sealed class BlobStorageOptionsValidator : IValidateOptions<BlobStorageOptions>
{
public ValidateOptionsResult Validate(string? name, BlobStorageOptions options)
{
var builder = new ValidateOptionsResultBuilder();
ValidateUploadsPath(name, options.BlobDirectory, builder);
return builder.Build();
}
private static void ValidateUploadsPath(string? name, string uploadsPath, ValidateOptionsResultBuilder builder)
{
string propertyName = string.IsNullOrEmpty(name)
? nameof(BlobStorageOptions.BlobDirectory)
: $"{name}.{nameof(BlobStorageOptions.BlobDirectory)}";
if (string.IsNullOrWhiteSpace(uploadsPath))
{
builder.AddError("Cannot be null or empty.", propertyName);
return;
}
if (!Directory.Exists(uploadsPath))
{
builder.AddError("Directory must exist.", propertyName);
return;
}
}
}
public static class BlobStorageOptionsServiceExtensions
{
public static IServiceCollection AddBlobStorageOptions(this IServiceCollection services)
{
services.AddOptions<BlobStorageOptions>().BindConfiguration("Storage").ValidateOnStart();
services.AddTransient<IValidateOptions<BlobStorageOptions>, BlobStorageOptionsValidator>();
return services;
}
}
MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageService.cs
+58
-0
diff --git a/MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageService.cs b/MatDenDagen/Infrastructure/Storage/BlobStorage/BlobStorageService.cs
new file mode 100644
index 0000000..87bab08
@@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace MatDenDagen.Infrastructure.Storage.BlobStorage;
public sealed partial class BlobStorageService(
ILogger<BlobStorageService> logger,
IOptions<BlobStorageOptions> options,
TimeProvider timeProvider
)
{
private readonly string blobDirectory = options.Value.BlobDirectory;
public async Task<SaveBlobResult> SaveBlob(Stream blob, CancellationToken cancellationToken)
{
var id = Guid.CreateVersion7(timeProvider.GetUtcNow()).ToString();
LogCreatingFile(id);
await using var fileStream = new FileStream(
Path.Join(blobDirectory, id),
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
FileOptions.Asynchronous
);
await blob.CopyToAsync(fileStream, cancellationToken);
return new(id);
}
public Stream? GetBlob(string id)
{
LogOpeningFile(id);
var path = Path.Join(blobDirectory, id);
return File.Exists(path)
? new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
FileOptions.Asynchronous
)
: null;
}
[LoggerMessage(LogLevel.Debug, "Creating new file {Id}.")]
private partial void LogCreatingFile(string id);
[LoggerMessage(LogLevel.Debug, "Opening file {Id}.")]
private partial void LogOpeningFile(string id);
}
public sealed record SaveBlobResult(string Id);
MatDenDagen/Infrastructure/Storage/StorageServiceExtensions.cs
+18
-0
diff --git a/MatDenDagen/Infrastructure/Storage/StorageServiceExtensions.cs b/MatDenDagen/Infrastructure/Storage/StorageServiceExtensions.cs
new file mode 100644
index 0000000..7c84cc0
@@ -0,0 +1,18 @@
using System;
using MatDenDagen.Infrastructure.Storage.BlobStorage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace MatDenDagen.Infrastructure.Storage;
public static class StorageServiceExtensions
{
public static IServiceCollection AddStorageServices(this IServiceCollection services)
{
services.TryAddSingleton(TimeProvider.System);
services.AddBlobStorageOptions().AddTransient<BlobStorageService>();
return services;
}
}
MatDenDagen/Program.cs
+3
-0
diff --git a/MatDenDagen/Program.cs b/MatDenDagen/Program.cs
index 75f7812..d9cf0e3 100644
@@ -1,5 +1,6 @@
using MatDenDagen;
using MatDenDagen.Components;
using MatDenDagen.Infrastructure.Storage;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -7,6 +8,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.ConfigureOpenTelemetry();
builder.Services.AddStorageServices();
builder.Services.AddRazorComponents();
var app = builder.Build();
MatDenDagen/appsettings.Development.json
+5
-1
diff --git a/MatDenDagen/appsettings.Development.json b/MatDenDagen/appsettings.Development.json
index 0c208ae..07d40da 100644
@@ -2,7 +2,11 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"MatDenDagen": "Debug"
}
},
"Storage": {
"BlobDirectory": "../blobs"
}
}
README.md
+1
-1
diff --git a/README.md b/README.md
index b8aa8e7..917e2c5 100644
@@ -15,4 +15,4 @@
- [ ] Internals
- [ ] Job that sends out notifications
- [ ] Storage for text answers
- [ ] Storage for photo (video?) uploads
- [x] Storage for photo (video?) uploads
blobs/.gitignore
+2
-0
diff --git a/blobs/.gitignore b/blobs/.gitignore
new file mode 100644
index 0000000..d6b7ef3
@@ -0,0 +1,2 @@
*
!.gitignore