Files

364 lines
14 KiB
C#

using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Converters;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
using Cleanuparr.Persistence.Models.Configuration.Seeker;
using Cleanuparr.Persistence.Models.State;
using Cleanuparr.Persistence.Providers;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Cleanuparr.Persistence;
/// <summary>
/// Database context for configuration data
/// </summary>
public class DataContext : DbContext
{
public static SemaphoreSlim Lock { get; } = new(1, 1);
public DbSet<GeneralConfig> GeneralConfigs { get; set; }
public DbSet<DownloadClientConfig> DownloadClients { get; set; }
public DbSet<QueueCleanerConfig> QueueCleanerConfigs { get; set; }
public DbSet<StallRule> StallRules { get; set; }
public DbSet<SlowRule> SlowRules { get; set; }
public DbSet<ContentBlockerConfig> ContentBlockerConfigs { get; set; }
public DbSet<DownloadCleanerConfig> DownloadCleanerConfigs { get; set; }
public DbSet<QBitSeedingRule> QBitSeedingRules { get; set; }
public DbSet<DelugeSeedingRule> DelugeSeedingRules { get; set; }
public DbSet<TransmissionSeedingRule> TransmissionSeedingRules { get; set; }
public DbSet<UTorrentSeedingRule> UTorrentSeedingRules { get; set; }
public DbSet<RTorrentSeedingRule> RTorrentSeedingRules { get; set; }
public DbSet<UnlinkedConfig> UnlinkedConfigs { get; set; }
public DbSet<DeadTorrentConfig> DeadTorrentConfigs { get; set; }
public DbSet<ArrConfig> ArrConfigs { get; set; }
public DbSet<ArrInstance> ArrInstances { get; set; }
public DbSet<NotificationConfig> NotificationConfigs { get; set; }
public DbSet<NotifiarrConfig> NotifiarrConfigs { get; set; }
public DbSet<AppriseConfig> AppriseConfigs { get; set; }
public DbSet<NtfyConfig> NtfyConfigs { get; set; }
public DbSet<PushoverConfig> PushoverConfigs { get; set; }
public DbSet<TelegramConfig> TelegramConfigs { get; set; }
public DbSet<DiscordConfig> DiscordConfigs { get; set; }
public DbSet<GotifyConfig> GotifyConfigs { get; set; }
public DbSet<BlacklistSyncHistory> BlacklistSyncHistory { get; set; }
public DbSet<BlacklistSyncConfig> BlacklistSyncConfigs { get; set; }
public DbSet<OrphanedFilesConfig> OrphanedFilesConfigs { get; set; }
public DbSet<SeekerConfig> SeekerConfigs { get; set; }
public DbSet<SeekerInstanceConfig> SeekerInstanceConfigs { get; set; }
private readonly IDatabaseProvider _provider;
public DataContext() : this(DatabaseProviderFactory.Current)
{
}
public DataContext(IDatabaseProvider provider)
{
_provider = provider;
}
public DataContext(DbContextOptions<DataContext> options) : this(options, DatabaseProviderFactory.Current)
{
}
public DataContext(DbContextOptions<DataContext> options, IDatabaseProvider provider) : base(options)
{
_provider = provider;
}
public static DataContext CreateStaticInstance()
{
IDatabaseProvider provider = DatabaseProviderFactory.Current;
DbContextOptionsBuilder<DataContext> optionsBuilder = new();
provider.ConfigureContext(optionsBuilder, DbContextKind.Data);
return new DataContext(optionsBuilder.Options, provider);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
SetDbContextOptions(optionsBuilder);
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
_provider.ConfigureConventions(configurationBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
string? schema = _provider.GetSchema(DbContextKind.Data);
if (schema is not null)
{
modelBuilder.HasDefaultSchema(schema);
}
modelBuilder.Entity<GeneralConfig>(entity =>
{
entity.ComplexProperty(e => e.Auth, cp =>
{
cp.Property(a => a.TrustedNetworks)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
});
});
// Configure ArrConfig -> ArrInstance relationship
modelBuilder.Entity<ArrConfig>(entity =>
{
entity.HasMany(a => a.Instances)
.WithOne(i => i.ArrConfig)
.HasForeignKey(i => i.ArrConfigId)
.OnDelete(DeleteBehavior.Cascade);
});
// Configure new notification system relationships
modelBuilder.Entity<NotificationConfig>(entity =>
{
entity.HasOne(p => p.NotifiarrConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<NotifiarrConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.AppriseConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<AppriseConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.NtfyConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<NtfyConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.PushoverConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<PushoverConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.TelegramConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<TelegramConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.DiscordConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<DiscordConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.GotifyConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<GotifyConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(p => p.Name).IsUnique();
});
// Configure PushoverConfig List<string> conversions
modelBuilder.Entity<PushoverConfig>(entity =>
{
entity.Property(p => p.Devices)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
entity.Property(p => p.Tags)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
});
modelBuilder.Entity<SeekerInstanceConfig>(entity =>
{
entity.HasOne(s => s.ArrInstance)
.WithMany()
.HasForeignKey(s => s.ArrInstanceId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(s => s.ArrInstanceId).IsUnique();
});
// Configure per-client seeding rule relationships and JSON list converters
var jsonListConverter = new JsonStringListConverter();
modelBuilder.Entity<QBitSeedingRule>(entity =>
{
entity.HasOne(s => s.DownloadClientConfig)
.WithMany()
.HasForeignKey(s => s.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
entity.Property(s => s.TagsAny).HasConversion(jsonListConverter);
entity.Property(s => s.TagsAll).HasConversion(jsonListConverter);
});
modelBuilder.Entity<DelugeSeedingRule>(entity =>
{
entity.HasOne(s => s.DownloadClientConfig)
.WithMany()
.HasForeignKey(s => s.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
});
modelBuilder.Entity<TransmissionSeedingRule>(entity =>
{
entity.HasOne(s => s.DownloadClientConfig)
.WithMany()
.HasForeignKey(s => s.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
entity.Property(s => s.TagsAny).HasConversion(jsonListConverter);
entity.Property(s => s.TagsAll).HasConversion(jsonListConverter);
});
modelBuilder.Entity<UTorrentSeedingRule>(entity =>
{
entity.HasOne(s => s.DownloadClientConfig)
.WithMany()
.HasForeignKey(s => s.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
});
modelBuilder.Entity<RTorrentSeedingRule>(entity =>
{
entity.HasOne(s => s.DownloadClientConfig)
.WithMany()
.HasForeignKey(s => s.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
});
// Configure per-client unlinked config relationship
modelBuilder.Entity<UnlinkedConfig>(entity =>
{
entity.HasOne(u => u.DownloadClientConfig)
.WithMany()
.HasForeignKey(u => u.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(u => u.DownloadClientConfigId).IsUnique();
});
// Configure per-client dead torrent config relationship
modelBuilder.Entity<DeadTorrentConfig>(entity =>
{
entity.HasOne(d => d.DownloadClientConfig)
.WithMany()
.HasForeignKey(d => d.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(d => d.DownloadClientConfigId).IsUnique();
entity.Property(d => d.Categories).HasConversion(jsonListConverter);
});
// Configure per-client orphaned files config relationship
modelBuilder.Entity<OrphanedFilesConfig>(entity =>
{
entity.HasOne(c => c.DownloadClientConfig)
.WithMany()
.HasForeignKey(c => c.DownloadClientConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(c => c.DownloadClientConfigId).IsUnique();
entity.Property(c => c.ScanDirectories).HasConversion(jsonListConverter);
entity.Property(c => c.ExcludePatterns).HasConversion(jsonListConverter);
});
// Configure BlacklistSyncState relationships and indexes
modelBuilder.Entity<BlacklistSyncHistory>(entity =>
{
// FK to DownloadClientConfig by DownloadClientId with cascade on delete
entity.HasOne(s => s.DownloadClient)
.WithMany()
.HasForeignKey(s => s.DownloadClientId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(s => new { s.Hash, DownloadClientId = s.DownloadClientId }).IsUnique();
entity.HasIndex(s => s.Hash);
});
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
// Use OriginalString for Uri properties to preserve the exact input (including embedded credentials)
foreach (var property in entityType.GetProperties().Where(p => p.ClrType == typeof(Uri)))
{
property.SetValueConverter(
new ValueConverter<Uri, string>(
v => v.OriginalString,
v => new Uri(v, UriKind.RelativeOrAbsolute)));
property.SetValueComparer(new ValueComparer<Uri>(
(u1, u2) => u1 != null && u2 != null
? u1.OriginalString == u2.OriginalString
: u1 == null && u2 == null,
u => u == null ? 0 : u.OriginalString.GetHashCode(),
u => u == null ? null! : new Uri(u.OriginalString, UriKind.RelativeOrAbsolute)));
}
}
modelBuilder.ApplyLowercaseEnumConversions();
}
private void SetDbContextOptions(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}
_provider.ConfigureContext(optionsBuilder, DbContextKind.Data);
}
}