From 20d91a4cb67874848bbd97a3aacb126f11d0f4ea Mon Sep 17 00:00:00 2001 From: Fabian Arndt Date: Sun, 21 Dec 2025 10:15:05 +0100 Subject: [PATCH] DB: Fix for empty category name / move childs / rebuild paths --- .../20250819221618_Add-Path-To-DavItem.cs | 5 + ...220004937_Fix-Empty-Categories.Designer.cs | 386 ++++++++++++++++++ .../20251220004937_Fix-Empty-Categories.cs | 72 ++++ 3 files changed, 463 insertions(+) create mode 100644 backend/Database/Migrations/20251220004937_Fix-Empty-Categories.Designer.cs create mode 100644 backend/Database/Migrations/20251220004937_Fix-Empty-Categories.cs diff --git a/backend/Database/Migrations/20250819221618_Add-Path-To-DavItem.cs b/backend/Database/Migrations/20250819221618_Add-Path-To-DavItem.cs index f3d2fd37..cdaed293 100644 --- a/backend/Database/Migrations/20250819221618_Add-Path-To-DavItem.cs +++ b/backend/Database/Migrations/20250819221618_Add-Path-To-DavItem.cs @@ -17,6 +17,11 @@ protected override void Up(MigrationBuilder migrationBuilder) nullable: false, defaultValue: ""); + BuildFullPath(migrationBuilder); + } + + public static void BuildFullPath(MigrationBuilder migrationBuilder) + { // Populate the Path column for every existing DavItem // * The root DavItem is given path `/` // * Every other DavItem is given path `{PARENT_PATH}/{NAME}` diff --git a/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.Designer.cs b/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.Designer.cs new file mode 100644 index 00000000..c3a607b2 --- /dev/null +++ b/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.Designer.cs @@ -0,0 +1,386 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NzbWebDAV.Database; + +#nullable disable + +namespace NzbWebDAV.Database.Migrations +{ + [DbContext(typeof(DavDatabaseContext))] + [Migration("20251220004937_Fix-Empty-Categories")] + partial class FixEmptyCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("NzbWebDAV.Database.Models.Account", b => + { + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RandomSalt") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Type", "Username"); + + b.ToTable("Accounts", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.ConfigItem", b => + { + b.Property("ConfigName") + .HasColumnType("TEXT"); + + b.Property("ConfigValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ConfigName"); + + b.ToTable("ConfigItems", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("IdPrefix") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastHealthCheck") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("NextHealthCheck") + .HasColumnType("INTEGER"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("IdPrefix", "Type"); + + b.HasIndex("ParentId", "Name") + .IsUnique(); + + b.HasIndex("Type", "NextHealthCheck", "ReleaseDate", "Id"); + + b.ToTable("DavItems", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavMultipartFile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DavMultipartFiles", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavNzbFile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("SegmentIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DavNzbFiles", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavRarFile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("RarParts") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DavRarFiles", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.HealthCheckResult", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DavItemId") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepairStatus") + .HasColumnType("INTEGER"); + + b.Property("Result") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DavItemId") + .HasFilter("\"RepairStatus\" = 3"); + + b.HasIndex("Result", "RepairStatus", "CreatedAt"); + + b.ToTable("HealthCheckResults", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.HealthCheckStat", b => + { + b.Property("DateStartInclusive") + .HasColumnType("INTEGER"); + + b.Property("DateEndExclusive") + .HasColumnType("INTEGER"); + + b.Property("Result") + .HasColumnType("INTEGER"); + + b.Property("RepairStatus") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.HasKey("DateStartInclusive", "DateEndExclusive", "Result", "RepairStatus"); + + b.ToTable("HealthCheckStats", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.HistoryItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DownloadDirId") + .HasColumnType("TEXT"); + + b.Property("DownloadStatus") + .HasColumnType("INTEGER"); + + b.Property("DownloadTimeSeconds") + .HasColumnType("INTEGER"); + + b.Property("FailMessage") + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TotalSegmentBytes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Category", "CreatedAt"); + + b.HasIndex("Category", "DownloadDirId"); + + b.ToTable("HistoryItems", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.QueueItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("NzbFileSize") + .HasColumnType("INTEGER"); + + b.Property("PauseUntil") + .HasColumnType("TEXT"); + + b.Property("PostProcessing") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TotalSegmentBytes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FileName") + .IsUnique(); + + b.HasIndex("Priority"); + + b.HasIndex("Priority", "CreatedAt"); + + b.HasIndex("Category", "Priority", "CreatedAt"); + + b.ToTable("QueueItems", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.QueueNzbContents", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("NzbContents") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("QueueNzbContents", (string)null); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavItem", b => + { + b.HasOne("NzbWebDAV.Database.Models.DavItem", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavMultipartFile", b => + { + b.HasOne("NzbWebDAV.Database.Models.DavItem", "DavItem") + .WithOne() + .HasForeignKey("NzbWebDAV.Database.Models.DavMultipartFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DavItem"); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavNzbFile", b => + { + b.HasOne("NzbWebDAV.Database.Models.DavItem", "DavItem") + .WithOne() + .HasForeignKey("NzbWebDAV.Database.Models.DavNzbFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DavItem"); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavRarFile", b => + { + b.HasOne("NzbWebDAV.Database.Models.DavItem", "DavItem") + .WithOne() + .HasForeignKey("NzbWebDAV.Database.Models.DavRarFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DavItem"); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.QueueNzbContents", b => + { + b.HasOne("NzbWebDAV.Database.Models.QueueItem", "QueueItem") + .WithOne() + .HasForeignKey("NzbWebDAV.Database.Models.QueueNzbContents", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("QueueItem"); + }); + + modelBuilder.Entity("NzbWebDAV.Database.Models.DavItem", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.cs b/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.cs new file mode 100644 index 00000000..6e922ddd --- /dev/null +++ b/backend/Database/Migrations/20251220004937_Fix-Empty-Categories.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NzbWebDAV.Database.Migrations +{ + /// + public partial class FixEmptyCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Function for fixing tables with empty category strings + // Ensures, that configured category exists + // Fallback to 'uncategorized' + var fix_empty_category = (string table) => migrationBuilder.Sql(@$" + UPDATE {table} + SET Category = COALESCE( + ( + SELECT d.Name + FROM ConfigItems c + JOIN DavItems d + ON d.Name = c.ConfigValue + WHERE c.ConfigName = 'api.manual-category' + ), + 'uncategorized' + ) + WHERE Category = ''; + "); + + // Fix HistoryItems + fix_empty_category("HistoryItems"); + + // Fix QueueItems + fix_empty_category("QueueItems"); + + // Fix DavItems + // Move items to configured category, "uncategorized" or "content"-root + // Use 'UPDATE OR IGNORE' to prevent potential duplicates + migrationBuilder.Sql(@" + UPDATE OR IGNORE DavItems + SET ParentId = COALESCE( + ( + SELECT Id + FROM DavItems + WHERE Name = COALESCE( + (SELECT ConfigValue FROM ConfigItems WHERE ConfigName = 'api.manual-category'), + 'uncategorized' + ) + ), + '00000000-0000-0000-0000-000000000002' + ) + WHERE ParentId = (SELECT Id FROM DavItems WHERE Name = ''); + "); + + // Remove empty parent + // Previous duplicates will be removed due to 'ON DELETE CASCADE' + migrationBuilder.Sql(@" + DELETE FROM DavItems WHERE Name = '' + "); + + // Rebuild 'Path' column + AddPathToDavItem.BuildFullPath(migrationBuilder); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Intentionally left blank + } + } +}