diff --git a/src/Koogle.Application/Services/ClubGifService.cs b/src/Koogle.Application/Services/ClubGifService.cs
index 66e8e87..c1fb233 100644
--- a/src/Koogle.Application/Services/ClubGifService.cs
+++ b/src/Koogle.Application/Services/ClubGifService.cs
@@ -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
diff --git a/src/Koogle.Domain/Interfaces/IMediaStorageService.cs b/src/Koogle.Domain/Interfaces/IMediaStorageService.cs
index 4fcdcba..24d1a3b 100644
--- a/src/Koogle.Domain/Interfaces/IMediaStorageService.cs
+++ b/src/Koogle.Domain/Interfaces/IMediaStorageService.cs
@@ -80,4 +80,16 @@ public interface IMediaStorageService
/// Ensures the club's media directory exists.
///
void EnsureClubDirectoryExists(string clubLoginName);
+
+ ///
+ /// Gets the base path for media storage.
+ /// In production (Linux/Azure), returns /home/data/club-media.
+ /// In development, returns wwwroot/club-media.
+ ///
+ string GetMediaBasePath();
+
+ ///
+ /// Gets the full file path for a club GIF.
+ ///
+ string GetFilePath(string clubLoginName, string fileName);
}
diff --git a/src/Koogle.Infrastructure/Data/DemoSeeder.cs b/src/Koogle.Infrastructure/Data/DemoSeeder.cs
index 0a243c2..70666fa 100644
--- a/src/Koogle.Infrastructure/Data/DemoSeeder.cs
+++ b/src/Koogle.Infrastructure/Data/DemoSeeder.cs
@@ -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>();
var appDb = scope.ServiceProvider.GetRequiredService();
var triggerRepo = scope.ServiceProvider.GetRequiredService();
+ var loggerFactory = scope.ServiceProvider.GetRequiredService();
+ 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();
- //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();
+ var mediaStorage = scope.ServiceProvider.GetRequiredService();
+ 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);
+ }
}
///
/// Resets the Demo club to initial state: hard delete all data, then re-seed.
///
- 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();
var triggerRepo = scope.ServiceProvider.GetRequiredService();
var env = scope.ServiceProvider.GetRequiredService();
+ var mediaStorage = scope.ServiceProvider.GetRequiredService();
// 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
/// Service provider.
/// Club ID to seed GIFs for.
/// Club's login name for folder path.
- 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();
var env = scope.ServiceProvider.GetRequiredService();
+ var mediaStorage = scope.ServiceProvider.GetRequiredService();
- 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
///
/// Deletes all GIF files and database entries for a club.
///
- 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);
diff --git a/src/Koogle.Infrastructure/Services/DemoResetService.cs b/src/Koogle.Infrastructure/Services/DemoResetService.cs
index 26425a4..a8cb617 100644
--- a/src/Koogle.Infrastructure/Services/DemoResetService.cs
+++ b/src/Koogle.Infrastructure/Services/DemoResetService.cs
@@ -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 _logger;
public DemoResetService(
IServiceProvider services,
IConfiguration config,
- AppDbContext db)
+ AppDbContext db, ILogger logger)
{
_services = services;
_config = config;
_db = db;
+ _logger = logger;
}
///
@@ -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;
}
diff --git a/src/Koogle.Infrastructure/Services/MediaStorageService.cs b/src/Koogle.Infrastructure/Services/MediaStorageService.cs
index 807d9e1..46b31be 100644
--- a/src/Koogle.Infrastructure/Services/MediaStorageService.cs
+++ b/src/Koogle.Infrastructure/Services/MediaStorageService.cs
@@ -1,15 +1,19 @@
using Koogle.Domain.Interfaces;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
namespace Koogle.Infrastructure.Services;
///
/// 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.
///
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");
+ }
}
///
@@ -127,16 +141,20 @@ public class MediaStorageService : IMediaStorageService
}
}
- private string GetClubGifDirectory(string clubLoginName)
- {
- return Path.Combine(_env.WebRootPath, "club-media", clubLoginName, "gifs");
- }
+ ///
+ public string GetMediaBasePath() => _mediaBasePath;
- private string GetFilePath(string clubLoginName, string fileName)
+ ///
+ 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
diff --git a/src/Koogle.Web/Program.cs b/src/Koogle.Web/Program.cs
index a49ec59..670628f 100644
--- a/src/Koogle.Web/Program.cs
+++ b/src/Koogle.Web/Program.cs
@@ -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()
.AddInteractiveServerRenderMode();
diff --git a/src/Koogle.Web/wwwroot/club-media/demo/gifs/197d20a5-ebab-4e69-9c05-0af2ca2bfc8f.gif b/src/Koogle.Web/wwwroot/club-media/demo/gifs/197d20a5-ebab-4e69-9c05-0af2ca2bfc8f.gif
deleted file mode 100644
index 497d0f1..0000000
Binary files a/src/Koogle.Web/wwwroot/club-media/demo/gifs/197d20a5-ebab-4e69-9c05-0af2ca2bfc8f.gif and /dev/null differ
diff --git a/src/Koogle.Web/wwwroot/club-media/demo/gifs/55b20117-4c27-40dd-a42a-75a08e9d4675.gif b/src/Koogle.Web/wwwroot/club-media/demo/gifs/297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif
similarity index 100%
rename from src/Koogle.Web/wwwroot/club-media/demo/gifs/55b20117-4c27-40dd-a42a-75a08e9d4675.gif
rename to src/Koogle.Web/wwwroot/club-media/demo/gifs/297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif
diff --git a/src/Koogle.Web/wwwroot/club-media/demo/gifs/18d9f4f1-1eac-4473-9c70-a8655e2e183c.gif b/src/Koogle.Web/wwwroot/club-media/demo/gifs/44f5bc11-8471-4ca9-b764-58e3929d1215.gif
similarity index 100%
rename from src/Koogle.Web/wwwroot/club-media/demo/gifs/18d9f4f1-1eac-4473-9c70-a8655e2e183c.gif
rename to src/Koogle.Web/wwwroot/club-media/demo/gifs/44f5bc11-8471-4ca9-b764-58e3929d1215.gif
diff --git a/src/Koogle.Web/wwwroot/club-media/demo/gifs/98f219ae-6da5-47bf-b360-51abc4684412.gif b/src/Koogle.Web/wwwroot/club-media/demo/gifs/98f219ae-6da5-47bf-b360-51abc4684412.gif
deleted file mode 100644
index 3b60236..0000000
Binary files a/src/Koogle.Web/wwwroot/club-media/demo/gifs/98f219ae-6da5-47bf-b360-51abc4684412.gif and /dev/null differ
diff --git a/src/Koogle.Web/wwwroot/club-media/demo/gifs/dd850f8d-9310-4441-81c8-9d3ad51e1b6d.gif b/src/Koogle.Web/wwwroot/club-media/demo/gifs/dd850f8d-9310-4441-81c8-9d3ad51e1b6d.gif
deleted file mode 100644
index 7f647f7..0000000
Binary files a/src/Koogle.Web/wwwroot/club-media/demo/gifs/dd850f8d-9310-4441-81c8-9d3ad51e1b6d.gif and /dev/null differ