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)
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 1016 KiB After Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 11 MiB |