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