fix ro media folder on linux:

1. IMediaStorageService (src/Koogle.Domain/Interfaces/):
  - Neue Methoden GetMediaBasePath() und GetFilePath() hinzugefügt

  2. MediaStorageService (src/Koogle.Infrastructure/Services/):
  - Erkennt Production+Linux → verwendet /home/data/club-media
  - Development → verwendet wwwroot/club-media

  3. Program.cs (src/Koogle.Web/):
  - StaticFileOptions für /club-media aus /home/data/club-media in Production

  4. DemoSeeder (src/Koogle.Infrastructure/Data/):
  - Alle Methoden nutzen jetzt IMediaStorageService statt hardkodierte Pfade

  5. ClubGifService (src/Koogle.Application/Services/):
  - Alle Path.Combine("wwwroot", ...) durch _mediaStorage.GetFilePath() ersetzt

  Das Verhalten:
  - Windows/Development: weiterhin wwwroot/club-media
  - Linux/Production (Azure): /home/data/club-media (persistent und beschreibbar)
This commit is contained in:
beo3000 2026-01-01 12:21:17 +01:00
parent 2f1a2a159b
commit 504daec8d7
11 changed files with 94 additions and 30 deletions

View File

@ -88,7 +88,7 @@ public class ClubGifService : IClubGifService
var (fileName, contentType) = await _mediaStorage.SaveGifFromUrlAsync(club.LoginName, dto.SourceUrl, ct);
var fileInfo = new FileInfo(Path.Combine("wwwroot", "club-media", club.LoginName, "gifs", fileName));
var fileInfo = new FileInfo(_mediaStorage.GetFilePath(club.LoginName, fileName));
var gif = new ClubGif
{
@ -297,7 +297,7 @@ public class ClubGifService : IClubGifService
?? throw new ArgumentException("Club not found");
var (fileName, contentType) = await _mediaStorage.SaveGifFromUrlAsync(club.LoginName, url, ct);
var fileInfo = new FileInfo(Path.Combine("wwwroot", "club-media", club.LoginName, "gifs", fileName));
var fileInfo = new FileInfo(_mediaStorage.GetFilePath(club.LoginName, fileName));
var gif = new ClubGif
{
@ -378,7 +378,7 @@ public class ClubGifService : IClubGifService
var newFileName = await _mediaStorage.SaveGifAsync(club.LoginName, sourceStream, template.Filename, contentType, ct);
// Get file size
var targetFile = Path.Combine(_env.WebRootPath, "club-media", club.LoginName, "gifs", newFileName);
var targetFile = _mediaStorage.GetFilePath(club.LoginName, newFileName);
var fileInfo = new FileInfo(targetFile);
// Create database entry

View File

@ -80,4 +80,16 @@ public interface IMediaStorageService
/// Ensures the club's media directory exists.
/// </summary>
void EnsureClubDirectoryExists(string clubLoginName);
/// <summary>
/// Gets the base path for media storage.
/// In production (Linux/Azure), returns /home/data/club-media.
/// In development, returns wwwroot/club-media.
/// </summary>
string GetMediaBasePath();
/// <summary>
/// Gets the full file path for a club GIF.
/// </summary>
string GetFilePath(string clubLoginName, string fileName);
}

View File

@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Koogle.Infrastructure.Data;
@ -47,6 +48,8 @@ public static class DemoSeeder
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("DemoSeeder");
var demoEmail = config["Bootstrap:Demo:Email"] ?? "demo@koogle.de";
var demoPassword = config["Bootstrap:Demo:Password"] ?? "demo123";
@ -139,24 +142,25 @@ public static class DemoSeeder
}
// 7) Seed template GIFs if not already present
//var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
//var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
//if (!hasGifs)
//{
// await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName);
//}
var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var mediaStorage = scope.ServiceProvider.GetRequiredService<IMediaStorageService>();
var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
if (!hasGifs)
{
await SeedTemplateGifsInternalAsync(appDb, webEnv, mediaStorage, club.Id, club.LoginName, logger);
}
}
/// <summary>
/// Resets the Demo club to initial state: hard delete all data, then re-seed.
/// </summary>
public static async Task ResetDemoClubAsync(IServiceProvider services, Guid clubId)
public static async Task ResetDemoClubAsync(IServiceProvider services, Guid clubId, ILogger logger)
{
using var scope = services.CreateScope();
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var mediaStorage = scope.ServiceProvider.GetRequiredService<IMediaStorageService>();
// Find creator ID and club for seeding
var club = await appDb.Clubs.FindAsync(clubId);
@ -166,14 +170,14 @@ public static class DemoSeeder
var creatorId = membership?.AssignedById ?? Guid.Empty;
// Hard delete all club data including GIFs
await DeleteClubGifsAsync(appDb, env, clubId, club.LoginName);
await DeleteClubGifsAsync(appDb, mediaStorage, clubId, club.LoginName);
await HardDeleteClubDataAsync(appDb, clubId);
// Re-seed demo data
await SeedDemoDataAsync(appDb, triggerRepo, clubId, creatorId);
// Re-seed template GIFs
await SeedTemplateGifsInternalAsync(appDb, env, clubId, club.LoginName);
await SeedTemplateGifsInternalAsync(appDb, env, mediaStorage, clubId, club.LoginName, logger);
}
private static async Task HardDeleteClubDataAsync(AppDbContext db, Guid clubId)
@ -434,24 +438,28 @@ public static class DemoSeeder
/// <param name="services">Service provider.</param>
/// <param name="clubId">Club ID to seed GIFs for.</param>
/// <param name="clubLoginName">Club's login name for folder path.</param>
public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName)
public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName, ILogger logger)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var mediaStorage = scope.ServiceProvider.GetRequiredService<IMediaStorageService>();
await SeedTemplateGifsInternalAsync(db, env, clubId, clubLoginName);
await SeedTemplateGifsInternalAsync(db, env, mediaStorage, clubId, clubLoginName, logger);
}
private static async Task SeedTemplateGifsInternalAsync(
AppDbContext db,
IWebHostEnvironment env,
IMediaStorageService mediaStorage,
Guid clubId,
string clubLoginName)
string clubLoginName,
ILogger logger)
{
var templatePath = Path.Combine(env.WebRootPath, "club-template", "gifs");
var templateJsonPath = Path.Combine(templatePath, "giftemplates.json");
logger.LogInformation($"SeedTemplateGifs: templateJsonPath = {templateJsonPath}");
if (!File.Exists(templateJsonPath))
{
return; // No template file, skip seeding
@ -468,27 +476,35 @@ public static class DemoSeeder
{
return;
}
logger.LogInformation($"{templates.Count} Template-Gifs found");
// Create target directory
var targetPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
Directory.CreateDirectory(targetPath);
// Create target directory using MediaStorageService (handles /home/data vs wwwroot)
mediaStorage.EnsureClubDirectoryExists(clubLoginName);
var mediaBasePath = mediaStorage.GetMediaBasePath();
var targetPath = Path.Combine(mediaBasePath, clubLoginName, "gifs");
logger.LogInformation($"creating target directory: {targetPath}");
var now = DateTime.UtcNow;
foreach (var template in templates)
{
var sourceFile = Path.Combine(templatePath, template.Filename);
if (!File.Exists(sourceFile))
{
logger.LogWarning($"source-file {sourceFile} not found");
continue; // Skip if source file doesn't exist
}
logger.LogInformation($"source-file {sourceFile} found");
// Generate new filename for club
var extension = Path.GetExtension(template.Filename);
var newFileName = $"{Guid.NewGuid()}{extension}";
var newFileName = $"{template.Filename}";
var targetFile = Path.Combine(targetPath, newFileName);
// Copy file
logger.LogInformation($"copy from '{sourceFile}' to '{targetFile}'");
File.Copy(sourceFile, targetFile, overwrite: true);
// Parse event type
@ -536,15 +552,15 @@ public static class DemoSeeder
/// <summary>
/// Deletes all GIF files and database entries for a club.
/// </summary>
private static async Task DeleteClubGifsAsync(AppDbContext db, IWebHostEnvironment env, Guid clubId, string clubLoginName)
private static async Task DeleteClubGifsAsync(AppDbContext db, IMediaStorageService mediaStorage, Guid clubId, string clubLoginName)
{
// Delete database entries
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifRatings WHERE ClubGifId IN (SELECT Id FROM app.ClubGifs WHERE ClubId = {clubId})");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifs WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.GifSubmissionTokens WHERE ClubId = {clubId}");
// Delete files
var gifPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
// Delete files using MediaStorageService path
var gifPath = Path.Combine(mediaStorage.GetMediaBasePath(), clubLoginName, "gifs");
if (Directory.Exists(gifPath))
{
Directory.Delete(gifPath, recursive: true);

View File

@ -3,6 +3,7 @@ using Koogle.Infrastructure.Data;
using KoogleApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Koogle.Infrastructure.Services;
@ -15,15 +16,17 @@ public class DemoResetService : IDemoResetService
private readonly IConfiguration _config;
private readonly AppDbContext _db;
private Guid? _demoClubId;
private readonly ILogger<DemoResetService> _logger;
public DemoResetService(
IServiceProvider services,
IConfiguration config,
AppDbContext db)
AppDbContext db, ILogger<DemoResetService> logger)
{
_services = services;
_config = config;
_db = db;
_logger = logger;
}
/// <inheritdoc />
@ -32,7 +35,7 @@ public class DemoResetService : IDemoResetService
var clubId = await GetDemoClubIdAsync(ct);
if (!clubId.HasValue) return false;
await DemoSeeder.ResetDemoClubAsync(_services, clubId.Value);
await DemoSeeder.ResetDemoClubAsync(_services, clubId.Value, _logger);
return true;
}

View File

@ -1,15 +1,19 @@
using Koogle.Domain.Interfaces;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Koogle.Infrastructure.Services;
/// <summary>
/// Service for storing and retrieving media files on disk.
/// In production (Linux/Azure), uses /home/data/club-media (writable).
/// In development, uses wwwroot/club-media.
/// </summary>
public class MediaStorageService : IMediaStorageService
{
private readonly IWebHostEnvironment _env;
private readonly HttpClient _httpClient;
private readonly string _mediaBasePath;
private static readonly string[] ValidGifExtensions = [".gif"];
private static readonly string[] ValidVideoExtensions = [".mp4", ".webm"];
@ -18,6 +22,16 @@ public class MediaStorageService : IMediaStorageService
{
_env = env;
_httpClient = httpClientFactory.CreateClient("MediaDownload");
// Use /home/data in production Linux (Azure App Service), wwwroot in development
if (!env.IsDevelopment() && OperatingSystem.IsLinux())
{
_mediaBasePath = "/home/data/club-media";
}
else
{
_mediaBasePath = Path.Combine(env.WebRootPath, "club-media");
}
}
/// <inheritdoc />
@ -127,16 +141,20 @@ public class MediaStorageService : IMediaStorageService
}
}
private string GetClubGifDirectory(string clubLoginName)
{
return Path.Combine(_env.WebRootPath, "club-media", clubLoginName, "gifs");
}
/// <inheritdoc />
public string GetMediaBasePath() => _mediaBasePath;
private string GetFilePath(string clubLoginName, string fileName)
/// <inheritdoc />
public string GetFilePath(string clubLoginName, string fileName)
{
return Path.Combine(GetClubGifDirectory(clubLoginName), fileName);
}
private string GetClubGifDirectory(string clubLoginName)
{
return Path.Combine(_mediaBasePath, clubLoginName, "gifs");
}
private static string? GetExtensionFromContentType(string contentType)
{
return contentType.ToLowerInvariant() switch

View File

@ -93,6 +93,21 @@ app.UseHttpsRedirection();
app.MapControllers();
app.UseAntiforgery();
// Serve club-media from /home/data in production Linux (Azure App Service)
if (!app.Environment.IsDevelopment() && OperatingSystem.IsLinux())
{
var mediaPath = "/home/data/club-media";
if (!Directory.Exists(mediaPath))
{
Directory.CreateDirectory(mediaPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(mediaPath),
RequestPath = "/club-media"
});
}
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB