Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6627940
Add framework for image storage migration
eliaspr Mar 15, 2026
2a58b24
Some WIP changes with todos
eliaspr Mar 15, 2026
04f7974
Merge branch 'main' into eliaspr/363-local-image-storage-path
eliaspr Apr 23, 2026
b5c94df
Refactor image storage migrations & integration into Turnierplan.App
eliaspr Apr 23, 2026
83d1d36
Remove implementation of IMigratableImageStorage in base image storage
eliaspr May 3, 2026
c7a6cf9
Implement migration
eliaspr May 3, 2026
827a0d0
Remove warning from documentation
eliaspr May 3, 2026
f596b5b
Fix typo
eliaspr May 3, 2026
db883ef
Move log messages to separate file
eliaspr May 3, 2026
a56c4de
Add some first migration tests
eliaspr May 3, 2026
79fea7c
Remove spacing
eliaspr May 3, 2026
db80579
Migration___With_Some_Images_In_Old_Structure_But_One_Image_File_Alre…
eliaspr May 3, 2026
1ad2af3
Harden regex and fix swapped paths in logging
eliaspr May 3, 2026
d663342
Increase number of images in test
eliaspr May 3, 2026
d85e753
Update using
eliaspr May 3, 2026
5daf45d
Migration___With_Images_In_Provider_But_Storage_Directory_Is_Empty___…
eliaspr May 3, 2026
13a3148
add WasQueried check
eliaspr May 3, 2026
0320025
Merge branch 'main' into eliaspr/363-local-image-storage-path
eliaspr May 3, 2026
1c9992b
Merge branch 'main' into eliaspr/363-local-image-storage-path
eliaspr May 4, 2026
a486a08
Merge branch 'main' into eliaspr/363-local-image-storage-path
eliaspr May 14, 2026
dc919b5
internal
eliaspr May 14, 2026
7bcda36
remove unnecessary base class
eliaspr May 14, 2026
07dd6a2
re-introduce interfaace IHostedImageStorage
eliaspr May 14, 2026
582c5ed
Merge branch 'refs/heads/main' into eliaspr/363-local-image-storage-path
eliaspr May 15, 2026
f156090
fix method order
eliaspr May 15, 2026
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
5 changes: 1 addition & 4 deletions docs/pages/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,11 @@ In der Weboberfläche können Bilddateien hochgeladen werden. Diese werden mit e
| `Turnierplan__ImageQuality` | Die Qualitätseinstellung für Bild-Uploads. Ein Wert von `100` entspricht einer verlustfreien Komprimierung. | `80` |
| `ImageStorage__StoragePath` | Das Verzeichnis innerhalb vom Container, hochgeladene Bilder gespeichert werden. | `/var/turnierplan/images` |

Alternativ können externe Services zum Speichern der Bilder konfiguriert werden. Dies hat den Vorteil, dass das Bereitstellen von Bilddateien keine CPU- und Netzwerkresourcen vom turnierplan.NET-Server beansprucht. Aktuell werden die folgenden externen Services unterstützt:
Alternativ können externe Services zum Speichern der Bilder konfiguriert werden. Dies hat den Vorteil, dass das Bereitstellen von Bilddateien keine CPU- und Netzwerkressourcen vom turnierplan.NET-Server beansprucht. Aktuell werden die folgenden externen Services unterstützt:

- **AWS S3** (oder kompatibler Dienst)
- **Azure Blob Storage**

!!! warning
Die nachfolgend vorgestellten Alternativen verwenden nicht zwangsläufig eine identische Verzeichnisstruktur zur Organisation der Dateien. Dadurch wird eine nachträgliche Umstellung ggf. erschwert!

### AWS S3

Um Bilder in einem AWS S3 oder S3-kompatiblen Bucket zu speichern, müssen die folgenden Umgebungsvariablen gesetzt werden:
Expand Down
13 changes: 13 additions & 0 deletions src/Turnierplan.App/Helpers/DatabaseImageProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Turnierplan.Core.Image;
using Turnierplan.Dal.Repositories;
using Turnierplan.ImageStorage;

namespace Turnierplan.App.Helpers;

internal sealed class DatabaseImageProvider(IImageRepository imageRepository) : IImageProvider
{
public async Task<IReadOnlyCollection<Image>> GetImagesAsync()
{
return (await imageRepository.GetAllImagesAsync()).AsReadOnly();
}
}
5 changes: 4 additions & 1 deletion src/Turnierplan.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
builder.Services.AddTurnierplanMonitoring(builder.Configuration);
builder.Services.AddTurnierplanDataAccessLayer(builder.Configuration);
builder.Services.AddTurnierplanDocumentRendering<ApplicationUrlProvider>();
builder.Services.AddTurnierplanImageStorage(builder.Configuration.GetSection("ImageStorage"));
builder.Services.AddTurnierplanImageStorage<DatabaseImageProvider>(builder.Configuration.GetSection("ImageStorage"));
builder.Services.AddTurnierplanLocalization();
builder.Services.AddTurnierplanSecurity(builder.Configuration.GetSection("Identity"));

Expand Down Expand Up @@ -116,4 +116,7 @@
// Migrate database and create admin user if DB is empty
await app.InitializeDatabaseAsync();

// Run image storage migrations if required by the configured image storage
await app.MigrateImageStorageAsync();

await app.RunAsync();
7 changes: 7 additions & 0 deletions src/Turnierplan.Dal/Repositories/ImageRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Turnierplan.Dal.Repositories;

public interface IImageRepository : IRepositoryWithPublicId<Image, long>
{
Task<List<Image>> GetAllImagesAsync();

Task<int> CountNumberOfReferencingTournamentsAsync(long imageId);
}

Expand All @@ -22,6 +24,11 @@ internal sealed class ImageRepository(TurnierplanContext context) : RepositoryBa
.FirstOrDefaultAsync();
}

public Task<List<Image>> GetAllImagesAsync()
{
return DbSet.ToListAsync();
}

public async Task<int> CountNumberOfReferencingTournamentsAsync(long imageId)
{
var count = await _context.Tournaments
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../version.xml" />

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<Import Project="../Turnierplan.Test.Common.props" />

<ItemGroup>
<ProjectReference Include="..\Turnierplan.ImageStorage\Turnierplan.ImageStorage.csproj" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions src/Turnierplan.ImageStorage/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Turnierplan.ImageStorage.Test.Migration")]

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Turnierplan.ImageStorage.Extensions;

public static class ServiceCollectionExtensions
{
public static void AddTurnierplanImageStorage(this IServiceCollection services, IConfigurationSection configuration)
public static void AddTurnierplanImageStorage<T>(this IServiceCollection services, IConfigurationSection configuration)
where T : IImageProvider
{
var type = configuration.GetValue<string>("Type");

Expand All @@ -29,5 +30,7 @@ public static void AddTurnierplanImageStorage(this IServiceCollection services,
default:
throw new InvalidOperationException($"Invalid image storage type specified: '{type}'");
}

services.AddTransient<ImageStorageMigrator>(sp => new ImageStorageMigrator(sp.GetRequiredService<IImageStorage>(), ActivatorUtilities.CreateInstance<T>(sp)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace Turnierplan.ImageStorage.Extensions;

public static class WebApplicationExtensions
{
public static void MapImageStorageEndpoint(this WebApplication application)
{
var imageStorage = application.Services.GetRequiredService<IImageStorage>();

if (imageStorage is not IHostedImageStorage localImageStorage)
{
return;
}

localImageStorage.MapEndpoint(application);
}

public static async Task MigrateImageStorageAsync(this WebApplication application)
{
await using var scope = application.Services.CreateAsyncScope();

var migrator = scope.ServiceProvider.GetRequiredService<ImageStorageMigrator>();

await migrator.MigrateAsync(application.Lifetime.ApplicationStopping);
}
}
8 changes: 8 additions & 0 deletions src/Turnierplan.ImageStorage/IHostedImageStorage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Builder;

namespace Turnierplan.ImageStorage;

internal interface IHostedImageStorage
{
void MapEndpoint(IApplicationBuilder applicationBuilder);
}
8 changes: 8 additions & 0 deletions src/Turnierplan.ImageStorage/IImageProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Turnierplan.Core.Image;

namespace Turnierplan.ImageStorage;

public interface IImageProvider
{
Task<IReadOnlyCollection<Image>> GetImagesAsync();
}
6 changes: 6 additions & 0 deletions src/Turnierplan.ImageStorage/IMigratableImageStorage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Turnierplan.ImageStorage;

internal interface IMigratableImageStorage
{
Task MigrateAsync(IImageProvider imageProvider, CancellationToken cancellationToken);
}
14 changes: 14 additions & 0 deletions src/Turnierplan.ImageStorage/ImageStorageMigrator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Turnierplan.ImageStorage;

internal sealed class ImageStorageMigrator(IImageStorage imageStorage, IImageProvider imageProvider)
{
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
if (imageStorage is not IMigratableImageStorage migratableImageStorage)
{
return;
}

await migratableImageStorage.MigrateAsync(imageProvider, cancellationToken);
}
}
8 changes: 0 additions & 8 deletions src/Turnierplan.ImageStorage/Local/ILocalImageStorage.cs

This file was deleted.

Loading