From 534b0cde918839aca165b7bd2b1c7df761aca82c Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Mon, 23 Feb 2026 15:42:19 -0500 Subject: [PATCH 1/3] Ensure initial user exists before startup wizard completes Add logic to initialize at least one user if the startup wizard is not completed, both during application startup and in the startup user API. After user creation, reload the user entity with all related navigation properties to ensure the in-memory user object is fully populated. Also update build and publish metadata files. --- .../ApplicationHost.cs | 8 ++++++++ Jellyfin.Api/Controllers/StartupController.cs | 1 + .../Users/UserManager.cs | 13 ++++++++++++- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- .../ASYNC_CONVERSION_CHECKLIST.md | 0 .../ASYNC_CONVERSION_EXAMPLE.cs | 0 .../ASYNC_CONVERSION_PRIORITY.md | 0 .../ASYNC_MIGRATION_PLAN.md | 0 .../BASEITEM_ASYNC_REFERENCE.md | 0 .../BASEITEM_FINAL_STATUS.md | 0 .../BUILD_SUCCESS_SUMMARY.md | 0 CONTRIBUTORS.md => docs/CONTRIBUTORS.md | 0 .../ConvertToFileScopedNamespaces.ps1 | 0 .../EDITORCONFIG_SETUP.md | 0 .../FixStyleCopWarnings.ps1 | 0 .../HOW_TO_IGNORE_IDE0065.md | 0 .../MEDIAATTACHMENT_CONVERSION_COMPLETE.md | 0 .../NET11_MIGRATION_SUMMARY.md | 0 .../NPGSQL_COMMAND_IN_PROGRESS_FIX.md | 0 .../OVERALL_PROJECT_STATUS.md | 0 PHASE1_COMPLETE.md => docs/PHASE1_COMPLETE.md | 0 .../PHASE1_PROGRESS_REPORT.md | 0 PHASE2_COMPLETE.md => docs/PHASE2_COMPLETE.md | 0 .../PHASE3A_COMPLETE.md | 0 PHASE3_STRATEGY.md => docs/PHASE3_STRATEGY.md | 0 .../PHASE_3A_FINAL_SUMMARY.md | 0 .../PHASE_3B_SUMMARY.md | 0 .../PHASE_3C_3D_SUMMARY.md | 0 .../PHASE_3E_SUMMARY.md | 0 .../PHASE_3_COMBINED_SUMMARY.md | 0 .../POC_SUMMARY_REPORT.md | 0 .../POSTGRESQL_BACKUP_ANALYSIS.md | 0 .../PROJECT_COMPLETION.md | 0 .../README_EDITORCONFIG_CHANGES.md | 0 .../STAKEHOLDER_PRESENTATION.md | 0 .../rollback-to-net10.ps1 | 0 .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10192 -> 10220 bytes .../Jellyfin.CodeAnalysis.AssemblyInfo.cs | 3 ++- ...yfin.CodeAnalysis.AssemblyInfoInputs.cache | 2 +- ...odeAnalysis.csproj.AssemblyReference.cache | Bin 63428 -> 79822 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10192 -> 10220 bytes 43 files changed, 25 insertions(+), 4 deletions(-) rename ASYNC_CONVERSION_CHECKLIST.md => docs/ASYNC_CONVERSION_CHECKLIST.md (100%) rename ASYNC_CONVERSION_EXAMPLE.cs => docs/ASYNC_CONVERSION_EXAMPLE.cs (100%) rename ASYNC_CONVERSION_PRIORITY.md => docs/ASYNC_CONVERSION_PRIORITY.md (100%) rename ASYNC_MIGRATION_PLAN.md => docs/ASYNC_MIGRATION_PLAN.md (100%) rename BASEITEM_ASYNC_REFERENCE.md => docs/BASEITEM_ASYNC_REFERENCE.md (100%) rename BASEITEM_FINAL_STATUS.md => docs/BASEITEM_FINAL_STATUS.md (100%) rename BUILD_SUCCESS_SUMMARY.md => docs/BUILD_SUCCESS_SUMMARY.md (100%) rename CONTRIBUTORS.md => docs/CONTRIBUTORS.md (100%) rename ConvertToFileScopedNamespaces.ps1 => docs/ConvertToFileScopedNamespaces.ps1 (100%) rename EDITORCONFIG_SETUP.md => docs/EDITORCONFIG_SETUP.md (100%) rename FixStyleCopWarnings.ps1 => docs/FixStyleCopWarnings.ps1 (100%) rename HOW_TO_IGNORE_IDE0065.md => docs/HOW_TO_IGNORE_IDE0065.md (100%) rename MEDIAATTACHMENT_CONVERSION_COMPLETE.md => docs/MEDIAATTACHMENT_CONVERSION_COMPLETE.md (100%) rename NET11_MIGRATION_SUMMARY.md => docs/NET11_MIGRATION_SUMMARY.md (100%) rename NPGSQL_COMMAND_IN_PROGRESS_FIX.md => docs/NPGSQL_COMMAND_IN_PROGRESS_FIX.md (100%) rename OVERALL_PROJECT_STATUS.md => docs/OVERALL_PROJECT_STATUS.md (100%) rename PHASE1_COMPLETE.md => docs/PHASE1_COMPLETE.md (100%) rename PHASE1_PROGRESS_REPORT.md => docs/PHASE1_PROGRESS_REPORT.md (100%) rename PHASE2_COMPLETE.md => docs/PHASE2_COMPLETE.md (100%) rename PHASE3A_COMPLETE.md => docs/PHASE3A_COMPLETE.md (100%) rename PHASE3_STRATEGY.md => docs/PHASE3_STRATEGY.md (100%) rename PHASE_3A_FINAL_SUMMARY.md => docs/PHASE_3A_FINAL_SUMMARY.md (100%) rename PHASE_3B_SUMMARY.md => docs/PHASE_3B_SUMMARY.md (100%) rename PHASE_3C_3D_SUMMARY.md => docs/PHASE_3C_3D_SUMMARY.md (100%) rename PHASE_3E_SUMMARY.md => docs/PHASE_3E_SUMMARY.md (100%) rename PHASE_3_COMBINED_SUMMARY.md => docs/PHASE_3_COMBINED_SUMMARY.md (100%) rename POC_SUMMARY_REPORT.md => docs/POC_SUMMARY_REPORT.md (100%) rename POSTGRESQL_BACKUP_ANALYSIS.md => docs/POSTGRESQL_BACKUP_ANALYSIS.md (100%) rename PROJECT_COMPLETION.md => docs/PROJECT_COMPLETION.md (100%) rename README_EDITORCONFIG_CHANGES.md => docs/README_EDITORCONFIG_CHANGES.md (100%) rename STAKEHOLDER_PRESENTATION.md => docs/STAKEHOLDER_PRESENTATION.md (100%) rename rollback-to-net10.ps1 => docs/rollback-to-net10.ps1 (100%) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e5ffb56b..9d1f2f21 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -599,6 +599,14 @@ namespace Emby.Server.Implementations SetStaticProperties(); FindParts(); + + // Ensure at least one user exists for the startup wizard + var userManager = Resolve(); + if (!ConfigurationManager.Configuration.IsStartupWizardCompleted) + { + Logger.LogInformation("Startup wizard not completed, ensuring initial user exists"); + await userManager.InitializeAsync().ConfigureAwait(false); + } } private X509Certificate2 GetCertificate(string path, string password) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 358f09d3..cb80e5e0 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -135,6 +135,7 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { + await _userManager.InitializeAsync().ConfigureAwait(false); var user = _userManager.Users.First(); if (string.IsNullOrWhiteSpace(startupUserDto.Password)) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 1bd8cbb1..f430496a 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -566,7 +566,18 @@ namespace Jellyfin.Server.Implementations.Users dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); - _users.Add(newUser.Id, newUser); + + var reloadedUser = await dbContext.Users + .AsNoTracking() + .AsSplitQuery() + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstAsync(u => u.Id.Equals(newUser.Id)) + .ConfigureAwait(false); + + _users.Add(reloadedUser.Id, reloadedUser); } } diff --git a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user index d27f448f..9a0d94c9 100644 --- a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>E:\Program Files\Jellyfin - True|2026-02-23T17:08:36.6307546Z||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; + True|2026-02-23T20:36:20.8511403Z||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; \ No newline at end of file diff --git a/ASYNC_CONVERSION_CHECKLIST.md b/docs/ASYNC_CONVERSION_CHECKLIST.md similarity index 100% rename from ASYNC_CONVERSION_CHECKLIST.md rename to docs/ASYNC_CONVERSION_CHECKLIST.md diff --git a/ASYNC_CONVERSION_EXAMPLE.cs b/docs/ASYNC_CONVERSION_EXAMPLE.cs similarity index 100% rename from ASYNC_CONVERSION_EXAMPLE.cs rename to docs/ASYNC_CONVERSION_EXAMPLE.cs diff --git a/ASYNC_CONVERSION_PRIORITY.md b/docs/ASYNC_CONVERSION_PRIORITY.md similarity index 100% rename from ASYNC_CONVERSION_PRIORITY.md rename to docs/ASYNC_CONVERSION_PRIORITY.md diff --git a/ASYNC_MIGRATION_PLAN.md b/docs/ASYNC_MIGRATION_PLAN.md similarity index 100% rename from ASYNC_MIGRATION_PLAN.md rename to docs/ASYNC_MIGRATION_PLAN.md diff --git a/BASEITEM_ASYNC_REFERENCE.md b/docs/BASEITEM_ASYNC_REFERENCE.md similarity index 100% rename from BASEITEM_ASYNC_REFERENCE.md rename to docs/BASEITEM_ASYNC_REFERENCE.md diff --git a/BASEITEM_FINAL_STATUS.md b/docs/BASEITEM_FINAL_STATUS.md similarity index 100% rename from BASEITEM_FINAL_STATUS.md rename to docs/BASEITEM_FINAL_STATUS.md diff --git a/BUILD_SUCCESS_SUMMARY.md b/docs/BUILD_SUCCESS_SUMMARY.md similarity index 100% rename from BUILD_SUCCESS_SUMMARY.md rename to docs/BUILD_SUCCESS_SUMMARY.md diff --git a/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md similarity index 100% rename from CONTRIBUTORS.md rename to docs/CONTRIBUTORS.md diff --git a/ConvertToFileScopedNamespaces.ps1 b/docs/ConvertToFileScopedNamespaces.ps1 similarity index 100% rename from ConvertToFileScopedNamespaces.ps1 rename to docs/ConvertToFileScopedNamespaces.ps1 diff --git a/EDITORCONFIG_SETUP.md b/docs/EDITORCONFIG_SETUP.md similarity index 100% rename from EDITORCONFIG_SETUP.md rename to docs/EDITORCONFIG_SETUP.md diff --git a/FixStyleCopWarnings.ps1 b/docs/FixStyleCopWarnings.ps1 similarity index 100% rename from FixStyleCopWarnings.ps1 rename to docs/FixStyleCopWarnings.ps1 diff --git a/HOW_TO_IGNORE_IDE0065.md b/docs/HOW_TO_IGNORE_IDE0065.md similarity index 100% rename from HOW_TO_IGNORE_IDE0065.md rename to docs/HOW_TO_IGNORE_IDE0065.md diff --git a/MEDIAATTACHMENT_CONVERSION_COMPLETE.md b/docs/MEDIAATTACHMENT_CONVERSION_COMPLETE.md similarity index 100% rename from MEDIAATTACHMENT_CONVERSION_COMPLETE.md rename to docs/MEDIAATTACHMENT_CONVERSION_COMPLETE.md diff --git a/NET11_MIGRATION_SUMMARY.md b/docs/NET11_MIGRATION_SUMMARY.md similarity index 100% rename from NET11_MIGRATION_SUMMARY.md rename to docs/NET11_MIGRATION_SUMMARY.md diff --git a/NPGSQL_COMMAND_IN_PROGRESS_FIX.md b/docs/NPGSQL_COMMAND_IN_PROGRESS_FIX.md similarity index 100% rename from NPGSQL_COMMAND_IN_PROGRESS_FIX.md rename to docs/NPGSQL_COMMAND_IN_PROGRESS_FIX.md diff --git a/OVERALL_PROJECT_STATUS.md b/docs/OVERALL_PROJECT_STATUS.md similarity index 100% rename from OVERALL_PROJECT_STATUS.md rename to docs/OVERALL_PROJECT_STATUS.md diff --git a/PHASE1_COMPLETE.md b/docs/PHASE1_COMPLETE.md similarity index 100% rename from PHASE1_COMPLETE.md rename to docs/PHASE1_COMPLETE.md diff --git a/PHASE1_PROGRESS_REPORT.md b/docs/PHASE1_PROGRESS_REPORT.md similarity index 100% rename from PHASE1_PROGRESS_REPORT.md rename to docs/PHASE1_PROGRESS_REPORT.md diff --git a/PHASE2_COMPLETE.md b/docs/PHASE2_COMPLETE.md similarity index 100% rename from PHASE2_COMPLETE.md rename to docs/PHASE2_COMPLETE.md diff --git a/PHASE3A_COMPLETE.md b/docs/PHASE3A_COMPLETE.md similarity index 100% rename from PHASE3A_COMPLETE.md rename to docs/PHASE3A_COMPLETE.md diff --git a/PHASE3_STRATEGY.md b/docs/PHASE3_STRATEGY.md similarity index 100% rename from PHASE3_STRATEGY.md rename to docs/PHASE3_STRATEGY.md diff --git a/PHASE_3A_FINAL_SUMMARY.md b/docs/PHASE_3A_FINAL_SUMMARY.md similarity index 100% rename from PHASE_3A_FINAL_SUMMARY.md rename to docs/PHASE_3A_FINAL_SUMMARY.md diff --git a/PHASE_3B_SUMMARY.md b/docs/PHASE_3B_SUMMARY.md similarity index 100% rename from PHASE_3B_SUMMARY.md rename to docs/PHASE_3B_SUMMARY.md diff --git a/PHASE_3C_3D_SUMMARY.md b/docs/PHASE_3C_3D_SUMMARY.md similarity index 100% rename from PHASE_3C_3D_SUMMARY.md rename to docs/PHASE_3C_3D_SUMMARY.md diff --git a/PHASE_3E_SUMMARY.md b/docs/PHASE_3E_SUMMARY.md similarity index 100% rename from PHASE_3E_SUMMARY.md rename to docs/PHASE_3E_SUMMARY.md diff --git a/PHASE_3_COMBINED_SUMMARY.md b/docs/PHASE_3_COMBINED_SUMMARY.md similarity index 100% rename from PHASE_3_COMBINED_SUMMARY.md rename to docs/PHASE_3_COMBINED_SUMMARY.md diff --git a/POC_SUMMARY_REPORT.md b/docs/POC_SUMMARY_REPORT.md similarity index 100% rename from POC_SUMMARY_REPORT.md rename to docs/POC_SUMMARY_REPORT.md diff --git a/POSTGRESQL_BACKUP_ANALYSIS.md b/docs/POSTGRESQL_BACKUP_ANALYSIS.md similarity index 100% rename from POSTGRESQL_BACKUP_ANALYSIS.md rename to docs/POSTGRESQL_BACKUP_ANALYSIS.md diff --git a/PROJECT_COMPLETION.md b/docs/PROJECT_COMPLETION.md similarity index 100% rename from PROJECT_COMPLETION.md rename to docs/PROJECT_COMPLETION.md diff --git a/README_EDITORCONFIG_CHANGES.md b/docs/README_EDITORCONFIG_CHANGES.md similarity index 100% rename from README_EDITORCONFIG_CHANGES.md rename to docs/README_EDITORCONFIG_CHANGES.md diff --git a/STAKEHOLDER_PRESENTATION.md b/docs/STAKEHOLDER_PRESENTATION.md similarity index 100% rename from STAKEHOLDER_PRESENTATION.md rename to docs/STAKEHOLDER_PRESENTATION.md diff --git a/rollback-to-net10.ps1 b/docs/rollback-to-net10.ps1 similarity index 100% rename from rollback-to-net10.ps1 rename to docs/rollback-to-net10.ps1 diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll index f2f97e52e4b59d7ef59b55b76072970ac1167976..d7f87adcebe50f15fd64f4cfc58debdf0293a735 100644 GIT binary patch delta 235 zcmZp0X>gg)!Q#ai^lxKNjHp24+nt?%10ML#6f=v+e*XRc<{zSam^9KXjE&O_l2cQY zk`oh65|d3+EG*59Oe{^(l2T30P14fLk_-%t&5|bHkyyj>;7HB;$vu(*0unRq)gHWN zcU)#-nk4?uHFEMB$zlblpdd^Ts5+m~(yZUjS$gkBv#!sZ>!oh8_@^;gFc<@28iN5t zGD9j5CIRV0AT$BuWCl})6rc)A26F}@pok?9rvX)@0>#b2>@)^5Am0E8jX|u<%jF}P E0JkVh9smFU delta 235 zcmZp0X>gg)!Scc8$Gwd`F`@!~29X?kR?hBi`swR^n5SLY{6lmPlZJ(vg@ti)ifOW0 zs*zcmVPc9!nrVuWSz>CErFoj6X_|$pWlCzAsrlqP5^GraEXp@d?vV@-sGSwZu~1OS zb>5#}8-M%CmP~#lS*!pRY=sE|RU1llc+N=Oe!k<*?nBv|>!oh8_**cTF<1bhF+(y# z3WF(-G-F6*FanZk42BGeKrss-%M?h1#1esWNeq?@=3rSoJsUM?TW F1OR`yNmKv; diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index 1566b34a4e23855d6c9c5b2db50122260659efef..9b4b64e5cd464b4debdc3069e17fb415d052514a 100644 GIT binary patch delta 570 zcmV-A0>%B%PwY>Sa3Itq001LGY;R%!000=6KPKSo2SKJZHDVY3MOom)eD9Hvt|1O1 z0RY$|0RZzO0RW~X000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm} z0|55`b^rqa?g0P+1^|L2AOQvdxFjF}2LQXXT>^;$f4_c)xm4(LP&Umtt=wm6Z^se6 zK~nOE=9V?EjkCt~9)Wc=1ONc7lTB+JF${+9f&2%NQ%i}*$xJ>r35D&Jlm-eV3GJcu zAbaFlfyXYk+%W&XcoT*#{a_C(pDe$A^jfx?=6J7hc+fO(gGJ);K!Fkp^=~)H8jowv zU}I14e}oxk0hkF8Bu1}h>3s>pBf%F+nPa%>lvb^7PZp{>)mm5GtX-`!43t44NUh-5 z6O1^4#gYj)*q9Po0Yhv2$uR+j9-i2HFa$nk0d_M@*7WR=GbEZ#K3ltK{&Kc`xA5?w zWDWy7Oj&5W{(Y+M$$7HEF`^j-o ztJu>rpr115qu1Ye1_sANNm#R6*!N`0usi0KJ-yuU7UV?fI(El=3@)Z|;|2XccyHWg z+d6HDj799wp7ocDi_>n=IYVa`opa|#>-OC2UiNc}eX+8?u0FDO+8^%!o!z|W58HpR z8ovBb5AO>59=;|Ps%38z4kHBt>oI3l_x%F1VFq6y6r&XZqZ|RFIRT?Z0i#?2qj~|O IpOe%k{D=biy8r+H delta 543 zcmV+)0^t4ZPtZ@0a3HuO001LGY;R%!004cOUI?KPB1NA3`my^>9dr&jcd?O>t|9Uw z0RX%s0RYz{0RW05000000RW{W00000000000RXL&UjfrArXwc+1OQ0`CjbQih6Ppt z0|55`b^rqa?g0P+1^{LxAOQvdoFpIt2LPY5T>^;$f5Wzb`d|h>PUi(NV8|g@Xf_Z& ztW)IvF#;%Fe_Tjozkyi|1ONc7lTAwlF%X9Dg8yOYNs;baW$V_8P%DapAXfAs9%hrt zb|9NbGOO&rmsBY&e$YcFr%c{?-eH*Jd4H?1yOZZ|hOPL1O^yNz>GMT0#p9eKD6a`V zFv3y*e^mzv;=Rq1bSafEYzZ3dKw%pR*eM?ZDS?XS_|856It?FeEmQ>7 zM*%iVn#}3TnNQOqf7&i`?Crm940ML;HekxGU@xg^>&-D& zZ0U(YT!##@!*n*s+&JSyv*-owAH0KdSeD017ivgHLo+~KnnE89k*d*&85E{0Mw(1< zvbpVj@VZs9y{SI3SlS-$|H&@)`FizlrOm&aU9@a9JO>s^wpRh`hMa)17-N>|J^_L| hvwsF%AQYnc0iyr`qa6XGDFLHE0i#|4ql1(1Cj4aa;l%&| diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs index ff6b222e..45f3ffe7 100644 --- a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs +++ b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -13,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+86883cd5c6e26f1ad8f5d26aeb97f15f859def57")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f833f0ceebcaa4ac5d8972494fbe574ff6b0036b")] [assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache index a45916b5..ff06f352 100644 --- a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache +++ b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache @@ -1 +1 @@ -519a49780b327d7db1c693a3f7b3613e79c1adca3797d68beef6d8a27561c8f3 +ec635030130ae48b7e69fa591520d48e411bcd6177f6760b1e8b737bdcf85112 diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.csproj.AssemblyReference.cache b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.csproj.AssemblyReference.cache index 4c5b9dad125aa15daf88e64fe764ad598803c2cc..1fd0093cd50c1774e41b9fd1e906e0d9df4ad3e1 100644 GIT binary patch literal 79822 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 EKavnI82|tP diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll index f2f97e52e4b59d7ef59b55b76072970ac1167976..d7f87adcebe50f15fd64f4cfc58debdf0293a735 100644 GIT binary patch delta 235 zcmZp0X>gg)!Q#ai^lxKNjHp24+nt?%10ML#6f=v+e*XRc<{zSam^9KXjE&O_l2cQY zk`oh65|d3+EG*59Oe{^(l2T30P14fLk_-%t&5|bHkyyj>;7HB;$vu(*0unRq)gHWN zcU)#-nk4?uHFEMB$zlblpdd^Ts5+m~(yZUjS$gkBv#!sZ>!oh8_@^;gFc<@28iN5t zGD9j5CIRV0AT$BuWCl})6rc)A26F}@pok?9rvX)@0>#b2>@)^5Am0E8jX|u<%jF}P E0JkVh9smFU delta 235 zcmZp0X>gg)!Scc8$Gwd`F`@!~29X?kR?hBi`swR^n5SLY{6lmPlZJ(vg@ti)ifOW0 zs*zcmVPc9!nrVuWSz>CErFoj6X_|$pWlCzAsrlqP5^GraEXp@d?vV@-sGSwZu~1OS zb>5#}8-M%CmP~#lS*!pRY=sE|RU1llc+N=Oe!k<*?nBv|>!oh8_**cTF<1bhF+(y# z3WF(-G-F6*FanZk42BGeKrss-%M?h1#1esWNeq?@=3rSoJsUM?TW F1OR`yNmKv; diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index 1566b34a4e23855d6c9c5b2db50122260659efef..9b4b64e5cd464b4debdc3069e17fb415d052514a 100644 GIT binary patch delta 570 zcmV-A0>%B%PwY>Sa3Itq001LGY;R%!000=6KPKSo2SKJZHDVY3MOom)eD9Hvt|1O1 z0RY$|0RZzO0RW~X000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm} z0|55`b^rqa?g0P+1^|L2AOQvdxFjF}2LQXXT>^;$f4_c)xm4(LP&Umtt=wm6Z^se6 zK~nOE=9V?EjkCt~9)Wc=1ONc7lTB+JF${+9f&2%NQ%i}*$xJ>r35D&Jlm-eV3GJcu zAbaFlfyXYk+%W&XcoT*#{a_C(pDe$A^jfx?=6J7hc+fO(gGJ);K!Fkp^=~)H8jowv zU}I14e}oxk0hkF8Bu1}h>3s>pBf%F+nPa%>lvb^7PZp{>)mm5GtX-`!43t44NUh-5 z6O1^4#gYj)*q9Po0Yhv2$uR+j9-i2HFa$nk0d_M@*7WR=GbEZ#K3ltK{&Kc`xA5?w zWDWy7Oj&5W{(Y+M$$7HEF`^j-o ztJu>rpr115qu1Ye1_sANNm#R6*!N`0usi0KJ-yuU7UV?fI(El=3@)Z|;|2XccyHWg z+d6HDj799wp7ocDi_>n=IYVa`opa|#>-OC2UiNc}eX+8?u0FDO+8^%!o!z|W58HpR z8ovBb5AO>59=;|Ps%38z4kHBt>oI3l_x%F1VFq6y6r&XZqZ|RFIRT?Z0i#?2qj~|O IpOe%k{D=biy8r+H delta 543 zcmV+)0^t4ZPtZ@0a3HuO001LGY;R%!004cOUI?KPB1NA3`my^>9dr&jcd?O>t|9Uw z0RX%s0RYz{0RW05000000RW{W00000000000RXL&UjfrArXwc+1OQ0`CjbQih6Ppt z0|55`b^rqa?g0P+1^{LxAOQvdoFpIt2LPY5T>^;$f5Wzb`d|h>PUi(NV8|g@Xf_Z& ztW)IvF#;%Fe_Tjozkyi|1ONc7lTAwlF%X9Dg8yOYNs;baW$V_8P%DapAXfAs9%hrt zb|9NbGOO&rmsBY&e$YcFr%c{?-eH*Jd4H?1yOZZ|hOPL1O^yNz>GMT0#p9eKD6a`V zFv3y*e^mzv;=Rq1bSafEYzZ3dKw%pR*eM?ZDS?XS_|856It?FeEmQ>7 zM*%iVn#}3TnNQOqf7&i`?Crm940ML;HekxGU@xg^>&-D& zZ0U(YT!##@!*n*s+&JSyv*-owAH0KdSeD017ivgHLo+~KnnE89k*d*&85E{0Mw(1< zvbpVj@VZs9y{SI3SlS-$|H&@)`FizlrOm&aU9@a9JO>s^wpRh`hMa)17-N>|J^_L| hvwsF%AQYnc0iyr`qa6XGDFLHE0i#|4ql1(1Cj4aa;l%&| From 43cdbf9b350e608dce121070d565cdce0aea6b17 Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Mon, 23 Feb 2026 17:54:53 -0500 Subject: [PATCH 2/3] Refactor startup user creation and add HomeSection config Refactored user initialization to always ensure at least one user exists at startup. If no users are present, a default admin user (admin/jellyfin) is created automatically. Updated StartupController endpoints to handle cases where no user exists. Simplified UserManager.InitializeAsync logic. Added HomeSectionConfiguration for EF Core entity mapping. Includes minor project and assembly info updates. --- .../ApplicationHost.cs | 8 +--- Jellyfin.Api/Controllers/StartupController.cs | 39 +++++++++++++----- .../Users/UserManager.cs | 17 ++++---- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10220 -> 10220 bytes .../Jellyfin.CodeAnalysis.AssemblyInfo.cs | 2 +- ...yfin.CodeAnalysis.AssemblyInfoInputs.cache | 2 +- .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10220 -> 10220 bytes .../HomeSectionConfiguration.cs | 37 +++++++++++++++++ 11 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/HomeSectionConfiguration.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9d1f2f21..0fbf76a1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -600,13 +600,9 @@ namespace Emby.Server.Implementations FindParts(); - // Ensure at least one user exists for the startup wizard + // Ensure at least one user exists var userManager = Resolve(); - if (!ConfigurationManager.Configuration.IsStartupWizardCompleted) - { - Logger.LogInformation("Startup wizard not completed, ensuring initial user exists"); - await userManager.InitializeAsync().ConfigureAwait(false); - } + await userManager.InitializeAsync().ConfigureAwait(false); } private X509Certificate2 GetCertificate(string path, string password) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index cb80e5e0..f26dc9ed 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; +using Jellyfin.Data; using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -111,15 +112,18 @@ public class StartupController : BaseJellyfinApiController [HttpGet("User")] [HttpGet("FirstUser", Name = "GetFirstUser_2")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetFirstUser() + public Task GetFirstUser() { - // TODO: Remove this method when startup wizard no longer requires an existing user. - await _userManager.InitializeAsync().ConfigureAwait(false); - var user = _userManager.Users.First(); - return new StartupUserDto + var user = _userManager.Users.FirstOrDefault(); + if (user is null) + { + return Task.FromResult(null); + } + + return Task.FromResult(new StartupUserDto { Name = user.Username - }; + }); } /// @@ -135,20 +139,33 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { - await _userManager.InitializeAsync().ConfigureAwait(false); - var user = _userManager.Users.First(); if (string.IsNullOrWhiteSpace(startupUserDto.Password)) { return BadRequest("Password must not be empty"); } - if (startupUserDto.Name is not null) + var user = _userManager.Users.FirstOrDefault(); + + // If no user exists, create the initial admin user + if (user is null) + { + if (string.IsNullOrWhiteSpace(startupUserDto.Name)) + { + return BadRequest("Username must not be empty when creating initial user"); + } + + user = await _userManager.CreateUserAsync(startupUserDto.Name).ConfigureAwait(false); + user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.IsAdministrator, true); + user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.EnableContentDeletion, true); + user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.EnableRemoteControlOfOtherUsers, true); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + } + else if (startupUserDto.Name is not null) { user.Username = startupUserDto.Name; + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); } - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - if (!string.IsNullOrEmpty(startupUserDto.Password)) { await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index f430496a..bce95403 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -542,24 +542,18 @@ namespace Jellyfin.Server.Implementations.Users /// public async Task InitializeAsync() { - // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. + // If no users exist, create the default admin user if (_users.Any()) { return; } - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - - _logger.LogWarning("No users, creating one with username {UserName}", defaultName); + _logger.LogWarning("No users found, creating default admin user with username 'admin'"); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); + var newUser = await CreateUserInternalAsync("admin", dbContext).ConfigureAwait(false); newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); @@ -578,6 +572,11 @@ namespace Jellyfin.Server.Implementations.Users .ConfigureAwait(false); _users.Add(reloadedUser.Id, reloadedUser); + + // Set the default password "jellyfin" + await ChangePassword(reloadedUser, "jellyfin").ConfigureAwait(false); + + _logger.LogWarning("Default admin user created with username 'admin' and password 'jellyfin'"); } } diff --git a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user index 9a0d94c9..e6ccac0f 100644 --- a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>E:\Program Files\Jellyfin - True|2026-02-23T20:36:20.8511403Z||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; + True|2026-02-23T22:50:19.2488617Z||;True|2026-02-23T17:40:34.7963081-05:00||;True|2026-02-23T17:30:42.0185573-05:00||;False|2026-02-23T16:44:17.4036787-05:00||;False|2026-02-23T16:10:39.9498056-05:00||;False|2026-02-23T16:06:47.9500254-05:00||;True|2026-02-23T15:36:20.8511403-05:00||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; \ No newline at end of file diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll index d7f87adcebe50f15fd64f4cfc58debdf0293a735..2df42d2a8d35880a2ffd999315f8bbdc735b03f7 100644 GIT binary patch delta 235 zcmZp0X>gg)!6H`4vT|cjjHtkr2sweYsm|`r*LF^BHBFS={6lmPlZL6WNs>WwN~)!y zg@v(YVsfIPnQ4-FQi@TMVX}Ejnz@-FP|U(8dGZ~JH7rY;uN;`%BN-szE#6>z;P+FX zSRACOpDGWwH(hw+a4rHeRNi!f1qRs*+m%Mqo Hd?XV9_ti|z delta 235 zcmZp0X>gg)!Q#ai^lxKNjHp24+nt?%10ML#6f=v+e*XRc<{zSam^9KXjE&O_l2cQY zk`oh65|d3+EG*59Oe{^(l2T30P14fLk_-%t&5|bHkyyj>;7HB;$vu(*0unRq)gHWN zcU)#-nk4?uHFEMB$zlblpdd^Ts5+m~(yZUjS$gkBv#!sZ>!oh8_@^;gFc<@28iN5t zGD9j5CIRV0AT$BuWCl})6rc)A26F}@pok?9rvX)@0>#b2>@)^5Am0E8jX|u<%jF}P E0JkVh9smFU diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index 9b4b64e5cd464b4debdc3069e17fb415d052514a..dbe9a4331d13c6d95dbf607665b7d995a1a97c90 100644 GIT binary patch delta 558 zcmV+}0@3~KPwY>Sd=yI;fIYzb^;$f0{@Q&c~4zT-uTp!@vLupL)}Q#i)F4UuK9Pd(w$@ zgMoH61ONc7lTB+IF%X9Df&7Odr<4-yuI=G925sV1gxhf58N!0Q3L| z5`t5s^f7zkp5P0`#KB+ARIR4Gv04Wt(b_6{RU@#{F_9nzg zMnKaBe{zU`-ogtz2b#czB*5)RlMTIk&@DVZ%87jkPeT&wuYRAZ zM`|4z;oz@|^c$u`y>`Rpf1d)idRtqE{s;PDB@$U@q0^H)a&BnZt97un^5}-d>Ezw_ zy@9^vE=Mfc9qdQax!WIe%Z}b|cn8v`x{UoXpM4u*zx9IdAH4VH)0wV~rDd~NOqXb| zxj5HzZ6~@hbKA}@8Z@|=nElH>#9*_L!*%tU#nIt#|L^SfJ%8H$8-wNL-#>lG>@0kZ wEL6(gM(ld>0>)uVs_gp(vwa3&AQKh=qa6XGIsv0b0i#_3qk93Ppp)<>`@01H$^ZZW delta 559 zcmV+~0?_^JPwY>Sd=wa%KPKSo2SKJZHDVY3MOom)eD9Hvt|1O10RY$|0RZzO0RW~X z000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm}0|55`b^rqa?g0P+ z1^|L2AOQvdxFjF}2LQXXT>^;$f4_c)xm4(LP&Umtt=wm6Z^se6K~nOE=9V?EjkCt~ z9)Wc=1ONc7lTB+JF${+9f&2%NQ%i}*$xJ>r35D&Jlm-eV3GJcuAbaFlfyXYk+%W&X zcoT*#{a_C(pDe$A^jfx?=6J7hc+fO(gGJ);K!Fkp^=~)H8jowvU}I14e}oxk0hkF8 zBu1}h>3s>pBf%F+nPa%>lvb^7PZp{>)mm5GtX-`!43t44NUh-56O1^4#gYj)*q9Po z0Yhv2$uR+j9-i2HFa$nk0d_M@*7WR=GbEZ#K3ltK{&Kc`xA5?wWDWy7Oj&5W{(Y+M z$$7HEF`^j-otJu>rpr115qu1Ye z1_sANNm#R6*!N`0usi0KJ-yuU7UV?fI(El=3@)Z|;|2XccyHWg+d6HDj799wp7ocD zi_>n=IYVa`opa|#>-OC2UiNc}eX+8?u0FDO+8^%!o!z|W58HpR8ovBb5AO>59=;|P xs%38z4kHBt>oI3l_x%F1eggg)!6H`4vT|cjjHtkr2sweYsm|`r*LF^BHBFS={6lmPlZL6WNs>WwN~)!y zg@v(YVsfIPnQ4-FQi@TMVX}Ejnz@-FP|U(8dGZ~JH7rY;uN;`%BN-szE#6>z;P+FX zSRACOpDGWwH(hw+a4rHeRNi!f1qRs*+m%Mqo Hd?XV9_ti|z delta 235 zcmZp0X>gg)!Q#ai^lxKNjHp24+nt?%10ML#6f=v+e*XRc<{zSam^9KXjE&O_l2cQY zk`oh65|d3+EG*59Oe{^(l2T30P14fLk_-%t&5|bHkyyj>;7HB;$vu(*0unRq)gHWN zcU)#-nk4?uHFEMB$zlblpdd^Ts5+m~(yZUjS$gkBv#!sZ>!oh8_@^;gFc<@28iN5t zGD9j5CIRV0AT$BuWCl})6rc)A26F}@pok?9rvX)@0>#b2>@)^5Am0E8jX|u<%jF}P E0JkVh9smFU diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index 9b4b64e5cd464b4debdc3069e17fb415d052514a..dbe9a4331d13c6d95dbf607665b7d995a1a97c90 100644 GIT binary patch delta 558 zcmV+}0@3~KPwY>Sd=yI;fIYzb^;$f0{@Q&c~4zT-uTp!@vLupL)}Q#i)F4UuK9Pd(w$@ zgMoH61ONc7lTB+IF%X9Df&7Odr<4-yuI=G925sV1gxhf58N!0Q3L| z5`t5s^f7zkp5P0`#KB+ARIR4Gv04Wt(b_6{RU@#{F_9nzg zMnKaBe{zU`-ogtz2b#czB*5)RlMTIk&@DVZ%87jkPeT&wuYRAZ zM`|4z;oz@|^c$u`y>`Rpf1d)idRtqE{s;PDB@$U@q0^H)a&BnZt97un^5}-d>Ezw_ zy@9^vE=Mfc9qdQax!WIe%Z}b|cn8v`x{UoXpM4u*zx9IdAH4VH)0wV~rDd~NOqXb| zxj5HzZ6~@hbKA}@8Z@|=nElH>#9*_L!*%tU#nIt#|L^SfJ%8H$8-wNL-#>lG>@0kZ wEL6(gM(ld>0>)uVs_gp(vwa3&AQKh=qa6XGIsv0b0i#_3qk93Ppp)<>`@01H$^ZZW delta 559 zcmV+~0?_^JPwY>Sd=wa%KPKSo2SKJZHDVY3MOom)eD9Hvt|1O10RY$|0RZzO0RW~X z000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm}0|55`b^rqa?g0P+ z1^|L2AOQvdxFjF}2LQXXT>^;$f4_c)xm4(LP&Umtt=wm6Z^se6K~nOE=9V?EjkCt~ z9)Wc=1ONc7lTB+JF${+9f&2%NQ%i}*$xJ>r35D&Jlm-eV3GJcuAbaFlfyXYk+%W&X zcoT*#{a_C(pDe$A^jfx?=6J7hc+fO(gGJ);K!Fkp^=~)H8jowvU}I14e}oxk0hkF8 zBu1}h>3s>pBf%F+nPa%>lvb^7PZp{>)mm5GtX-`!43t44NUh-56O1^4#gYj)*q9Po z0Yhv2$uR+j9-i2HFa$nk0d_M@*7WR=GbEZ#K3ltK{&Kc`xA5?wWDWy7Oj&5W{(Y+M z$$7HEF`^j-otJu>rpr115qu1Ye z1_sANNm#R6*!N`0usi0KJ-yuU7UV?fI(El=3@)Z|;|2XccyHWg+d6HDj799wp7ocD zi_>n=IYVa`opa|#>-OC2UiNc}eX+8?u0FDO+8^%!o!z|W58HpR8ovBb5AO>59=;|P xs%38z4kHBt>oI3l_x%F1eg +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Jellyfin.Database.Implementations.ModelConfiguration +{ + using System; + using Jellyfin.Database.Implementations.Entities; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + + /// + /// FluentAPI configuration for the HomeSection entity. + /// + public class HomeSectionConfiguration : IEntityTypeConfiguration + { + /// + public void Configure(EntityTypeBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + // Explicitly set table name to singular to match the migration + builder.ToTable("HomeSection", "displaypreferences"); + + builder.HasKey(e => e.Id); + + builder.Property(e => e.Order) + .IsRequired(); + + builder.Property(e => e.Type) + .IsRequired(); + + builder.Property(e => e.DisplayPreferencesId) + .IsRequired(); + } + } +} From 88dba54087e658633f12f1bb49547194dbf04efa Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Mon, 23 Feb 2026 18:31:27 -0500 Subject: [PATCH 3/3] Add user-configurable PostgreSQL backup system Introduced backup/restore via external pg_dump/pg_restore tools, configurable in database.xml. Added DatabaseBackupOptions, PostgresBackupService, and example/config docs. Backup features are enabled only for local databases; remote DBs require manual management. Logging and documentation clarify external tool usage, error handling, and security. Updated provider logic and assembly files accordingly. --- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- POSTGRES_BACKUP_IMPLEMENTATION.md | 200 ++++++++ POSTGRES_BACKUP_LOCAL_ONLY_IMPLEMENTATION.md | 235 +++++++++ POSTGRES_BACKUP_LOGGING_EXTERNAL_TOOLS.md | 167 ++++++ .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10220 -> 10220 bytes .../Jellyfin.CodeAnalysis.AssemblyInfo.cs | 2 +- ...yfin.CodeAnalysis.AssemblyInfoInputs.cache | 2 +- .../netstandard2.0/Jellyfin.CodeAnalysis.dll | Bin 8704 -> 8704 bytes .../netstandard2.0/Jellyfin.CodeAnalysis.pdb | Bin 10220 -> 10220 bytes .../DbConfiguration/DatabaseBackupOptions.cs | 75 +++ .../DatabaseConfigurationOptions.cs | 7 +- .../DbConfiguration/database.xml.example | 73 +++ .../BACKUP_CONFIGURATION.md | 258 ++++++++++ .../PostgresBackupService.cs | 481 ++++++++++++++++++ .../PostgresDatabaseProvider.cs | 149 +++++- 16 files changed, 1637 insertions(+), 14 deletions(-) create mode 100644 POSTGRES_BACKUP_IMPLEMENTATION.md create mode 100644 POSTGRES_BACKUP_LOCAL_ONLY_IMPLEMENTATION.md create mode 100644 POSTGRES_BACKUP_LOGGING_EXTERNAL_TOOLS.md create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseBackupOptions.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.xml.example create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/BACKUP_CONFIGURATION.md create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs diff --git a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user index e6ccac0f..d6e5c069 100644 --- a/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/Jellyfin.Server/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>E:\Program Files\Jellyfin - True|2026-02-23T22:50:19.2488617Z||;True|2026-02-23T17:40:34.7963081-05:00||;True|2026-02-23T17:30:42.0185573-05:00||;False|2026-02-23T16:44:17.4036787-05:00||;False|2026-02-23T16:10:39.9498056-05:00||;False|2026-02-23T16:06:47.9500254-05:00||;True|2026-02-23T15:36:20.8511403-05:00||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; + True|2026-02-23T23:25:39.1942435Z||;True|2026-02-23T18:08:02.8170921-05:00||;True|2026-02-23T17:50:19.2488617-05:00||;True|2026-02-23T17:40:34.7963081-05:00||;True|2026-02-23T17:30:42.0185573-05:00||;False|2026-02-23T16:44:17.4036787-05:00||;False|2026-02-23T16:10:39.9498056-05:00||;False|2026-02-23T16:06:47.9500254-05:00||;True|2026-02-23T15:36:20.8511403-05:00||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||; \ No newline at end of file diff --git a/POSTGRES_BACKUP_IMPLEMENTATION.md b/POSTGRES_BACKUP_IMPLEMENTATION.md new file mode 100644 index 00000000..f18cff19 --- /dev/null +++ b/POSTGRES_BACKUP_IMPLEMENTATION.md @@ -0,0 +1,200 @@ +# PostgreSQL Backup Configuration - Implementation Summary + +## What Was Created + +This implementation adds configurable PostgreSQL backup options to Jellyfin that can be configured through the `database.xml` file. + +### New Files Created: + +1. **`DatabaseBackupOptions.cs`** + - Location: `src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/` + - Contains all configurable backup settings + - Properties: PgDumpPath, PgRestorePath, BackupFormat, CompressionLevel, TimeoutSeconds, etc. + +2. **`PostgresBackupService.cs`** + - Location: `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/` + - Service class that performs backup and restore operations + - Reads configuration from `database.xml` + - Executes `pg_dump` and `pg_restore` with configured options + +3. **`database.xml.example`** + - Location: `src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/` + - Example configuration file showing all available options + - Can be copied to Jellyfin's config directory + +4. **`BACKUP_CONFIGURATION.md`** + - Location: `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/` + - Complete documentation for end users + - Configuration reference and troubleshooting guide + +### Modified Files: + +1. **`DatabaseConfigurationOptions.cs`** + - Added `BackupOptions` property to hold backup configuration + +## How It Works + +### Configuration Flow: + +``` +database.xml + ↓ +DatabaseConfigurationOptions.BackupOptions + ↓ +PostgresBackupService reads options via IConfigurationManager + ↓ +pg_dump/pg_restore executed with configured parameters +``` + +### Example database.xml: + +```xml + + Jellyfin-PostgreSQL + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=localhost;Database=jellyfin;Username=jellyfin;Password=pwd + + + + pg_dump + custom + 6 + true + 1800 + true + + +``` + +### Using the Service: + +```csharp +// In your DI setup (Startup.cs or similar) +services.AddSingleton(); + +// In your code +public class BackupManager +{ + private readonly PostgresBackupService _backupService; + + public BackupManager(PostgresBackupService backupService) + { + _backupService = backupService; + } + + public async Task PerformBackup() + { + // All settings are read from database.xml automatically + var backupPath = await _backupService.CreateBackupAsync("/backup/directory"); + Console.WriteLine($"Backup created: {backupPath}"); + } + + public async Task RestoreFromBackup(string backupFile) + { + await _backupService.RestoreBackupAsync(backupFile); + } +} +``` + +## Configuration Options + +All options are **optional** with sensible defaults: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `PgDumpPath` | string | Searches PATH | Path to pg_dump executable | +| `PgRestorePath` | string | Searches PATH | Path to pg_restore executable | +| `BackupFormat` | string | "custom" | Backup format: custom, plain, directory, tar | +| `IncludeBlobs` | bool | true | Include large objects in backup | +| `CompressionLevel` | int | 6 | Compression level (0-9) | +| `TimeoutSeconds` | int | 1800 | Max backup/restore time (30 min) | +| `VerboseOutput` | bool | true | Enable verbose logging | +| `ParallelJobs` | int? | null | Number of parallel jobs (directory format) | +| `AdditionalArguments` | string? | null | Extra pg_dump arguments | + +## Benefits + +1. **User-Configurable**: Users can customize backup behavior without code changes +2. **Flexible**: Supports all pg_dump formats and options +3. **Secure**: Uses environment variables for passwords +4. **Robust**: Includes timeout handling and error reporting +5. **Well-Documented**: Complete documentation for end users +6. **Backward Compatible**: All options have defaults, no configuration required for basic use + +## Next Steps + +To integrate this into Jellyfin: + +1. **Register the Service** in your DI container: + ```csharp + services.AddSingleton(); + ``` + +2. **Wire into existing backup system** (if Jellyfin has one): + ```csharp + // In your backup manager/controller + if (dbType == "PostgreSQL") + { + var postgresBackup = serviceProvider.GetRequiredService(); + await postgresBackup.CreateBackupAsync(backupDirectory); + } + ``` + +3. **Add UI configuration** (optional): + - Create admin UI to edit backup options + - Save to database.xml through configuration manager + +4. **Testing**: + - Test with different backup formats + - Test timeout handling + - Test restore operations + - Test path auto-detection on Windows/Linux + +## Example Usage Scenarios + +### Scenario 1: Basic Backup (default settings) +```xml + + pg_dump + +``` + +### Scenario 2: High Compression for Storage +```xml + + custom + 9 + 3600 + +``` + +### Scenario 3: Fast Parallel Backup +```xml + + directory + 4 + 3 + +``` + +### Scenario 4: Plain SQL Script +```xml + + plain + --exclude-schema=test + +``` + +## Security Considerations + +1. **File Permissions**: Ensure `database.xml` is readable only by the Jellyfin user +2. **Backup Storage**: Store backups in a secure location with appropriate permissions +3. **Password Handling**: Uses `PGPASSWORD` environment variable (more secure than CLI args) +4. **Audit Logging**: All operations are logged with timestamps + +## Troubleshooting + +Common issues and solutions are documented in `BACKUP_CONFIGURATION.md`. diff --git a/POSTGRES_BACKUP_LOCAL_ONLY_IMPLEMENTATION.md b/POSTGRES_BACKUP_LOCAL_ONLY_IMPLEMENTATION.md new file mode 100644 index 00000000..13eddeaf --- /dev/null +++ b/POSTGRES_BACKUP_LOCAL_ONLY_IMPLEMENTATION.md @@ -0,0 +1,235 @@ +# PostgreSQL Backup - Local-Only Implementation Summary + +## Overview + +The PostgreSQL backup functionality using `pg_dump` and `pg_restore` has been updated to **ONLY work with local databases** (localhost or 127.0.0.1). For remote PostgreSQL servers, users must manage backups externally. + +## Changes Made + +### 1. **PostgresDatabaseProvider.cs** + +#### Added Fields: +```csharp +private readonly IConfigurationManager? configurationManager; +private PostgresBackupService? backupService; +private string? currentHost; +``` + +#### Updated Constructor: +- Added optional `IConfigurationManager` parameter +- Allows backup service initialization when configuration is available + +#### New Method: `IsLocalHost()` +```csharp +private static bool IsLocalHost(string? host) +{ + return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) || + host.Equals("127.0.0.1", StringComparison.Ordinal) || + host.Equals("::1", StringComparison.Ordinal); // IPv6 localhost +} +``` + +#### Updated `Initialise()` Method: +- Captures the database host from connection string +- Initializes `PostgresBackupService` **only if host is local** +- Logs appropriate messages based on whether database is local or remote + +```csharp +// Store the host for backup operations +currentHost = connectionBuilder.Host; + +// Initialize backup service if host is local and configuration manager is available +if (IsLocalHost(currentHost) && configurationManager is not null) +{ + backupService = new PostgresBackupService(backupLogger, configurationManager); + logger.LogInformation("PostgreSQL backup service initialized for local database"); +} +else if (!IsLocalHost(currentHost)) +{ + logger.LogInformation("PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled", currentHost); +} +``` + +#### Updated Backup Methods: + +**`MigrationBackupFast()`**: +- **Local database**: Uses `PostgresBackupService.CreateBackupAsync()` +- **Remote database**: Returns timestamp key, logs warning + +**`RestoreBackupFast()`**: +- **Local database**: Uses `PostgresBackupService.RestoreBackupAsync()` +- **Remote database**: Logs warning only + +**`DeleteBackup()`**: +- **Local database**: Deletes backup file from disk +- **Remote database**: Logs warning + +### 2. **Updated Log Messages** + +All messages now specify "on a remote server" for clarity: + +| Old Message | New Message | +|-------------|-------------| +| "Backup deletion is not implemented for PostgreSQL. Manage backups externally." | "Backup deletion is not implemented for PostgreSQL **on a remote server**. Manage backups externally." | +| "Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups." | "Fast backup is not implemented for PostgreSQL **on a remote server**. Use pg_dump manually for proper backups." | +| "Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration." | "Fast restore is not implemented for PostgreSQL **on a remote server**. Use pg_restore manually for proper restoration." | + +### 3. **Updated Configuration Documentation** + +**database.xml.example**: +```xml + +``` + +**BACKUP_CONFIGURATION.md**: +- Added prominent warning about local-only requirement +- Clarified behavior for remote databases +- Updated examples and troubleshooting sections + +## Behavior Matrix + +| Database Host | Backup Service Initialized? | Backup Operations | Log Message | +|---------------|---------------------------|-------------------|-------------| +| `localhost` | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" | +| `127.0.0.1` | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" | +| `::1` (IPv6) | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" | +| `192.168.1.10` (remote) | ❌ No | Disabled, external management required | "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" | +| `db.example.com` (remote) | ❌ No | Disabled, external management required | "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" | + +## Example Configurations + +### Local Database (Backups Enabled): + +```xml + + Jellyfin-PostgreSQL + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=pwd + + + + pg_dump + custom + 6 + + +``` + +**Result**: ✅ Backups work automatically via pg_dump/pg_restore + +### Remote Database (Backups Disabled): + +```xml + + Jellyfin-PostgreSQL + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=192.168.1.50;Port=5432;Database=jellyfin;Username=jellyfin;Password=pwd + + + + + pg_dump + + +``` + +**Result**: ❌ Backup operations log warnings; users must manually run: +```bash +# Manual backup on remote server +pg_dump -h 192.168.1.50 -p 5432 -U jellyfin -d jellyfin -F c -f backup.dump + +# Manual restore on remote server +pg_restore -h 192.168.1.50 -p 5432 -U jellyfin -d jellyfin backup.dump +``` + +## Security Rationale + +This restriction exists because: + +1. **Local Access Required**: `pg_dump` and `pg_restore` must be executed on the same machine as the PostgreSQL server for reliable filesystem access +2. **Credentials**: Running pg_dump against remote servers requires storing remote credentials +3. **Network Overhead**: Large backups over network connections can be slow and unreliable +4. **Best Practice**: Remote PostgreSQL deployments should have their own backup infrastructure (scheduled pg_dump jobs, WAL archiving, replication, etc.) + +## Migration Path + +If you previously had a remote PostgreSQL setup expecting automatic backups: + +### Before (Not Working): +- Jellyfin connected to remote PostgreSQL +- Expected automatic backups (didn't work) + +### After (Clear Behavior): +- Jellyfin connects to remote PostgreSQL +- Clear log messages indicate backups are disabled +- Documentation provides manual backup commands + +### Recommended Setup for Remote Databases: + +1. **On the PostgreSQL server**, set up a cron job: + ```bash + # /etc/cron.daily/jellyfin-backup.sh + pg_dump -U jellyfin -d jellyfin -F c -f /backups/jellyfin_$(date +%Y%m%d).dump + ``` + +2. **Use PostgreSQL's native backup features**: + - Continuous archiving (WAL archiving) + - Streaming replication + - Point-in-time recovery (PITR) + - pgBackRest or Barman for enterprise backups + +3. **Cloud-native solutions**: + - AWS RDS automated backups + - Azure Database for PostgreSQL automated backups + - Google Cloud SQL automated backups + +## Testing + +To test the local vs. remote behavior: + +```csharp +// Test with localhost +var localConfig = new DatabaseConfigurationOptions +{ + CustomProviderOptions = new CustomDatabaseOptions + { + Options = new[] + { + new CustomDatabaseOption { Key = "host", Value = "localhost" } + } + } +}; +// Result: Backup service initialized + +// Test with remote host +var remoteConfig = new DatabaseConfigurationOptions +{ + CustomProviderOptions = new CustomDatabaseOptions + { + Options = new[] + { + new CustomDatabaseOption { Key = "host", Value = "192.168.1.50" } + } + } +}; +// Result: Backup service NOT initialized, warning logged +``` + +## Summary + +✅ **Local databases (localhost/127.0.0.1)**: Full automated backup support via pg_dump/pg_restore +❌ **Remote databases**: Backup operations disabled, clear messaging, external management required +📝 **Documentation**: Updated to clearly explain the local-only restriction +🔒 **Security**: Follows best practices for remote database backup management + +This implementation provides a clear, safe, and maintainable approach to PostgreSQL backups in Jellyfin. diff --git a/POSTGRES_BACKUP_LOGGING_EXTERNAL_TOOLS.md b/POSTGRES_BACKUP_LOGGING_EXTERNAL_TOOLS.md new file mode 100644 index 00000000..d25ca2b0 --- /dev/null +++ b/POSTGRES_BACKUP_LOGGING_EXTERNAL_TOOLS.md @@ -0,0 +1,167 @@ +# PostgreSQL Backup Logging - External Tool References + +## Overview + +All logging messages have been updated to explicitly clarify that `pg_dump` and `pg_restore` are **external command-line tools** being executed as separate processes, not internal library functions. + +## Updated Log Messages + +### PostgresBackupService.cs + +#### Backup Creation: +```csharp +// Before: +"Starting PostgreSQL backup to {BackupPath}" +"Executing: {PgDump} {Arguments}" +"Successfully created PostgreSQL backup at {BackupPath}" + +// After: +"Starting PostgreSQL backup using external pg_dump tool to {BackupPath}" +"Executing external command: {PgDump} {Arguments}" +"Successfully created PostgreSQL backup using external pg_dump at {BackupPath}" +``` + +#### Backup Restoration: +```csharp +// Before: +"Starting PostgreSQL restore from {BackupPath}" +"Successfully restored PostgreSQL backup from {BackupPath}" + +// After: +"Starting PostgreSQL restore using external pg_restore tool from {BackupPath}" +"Successfully restored PostgreSQL backup using external pg_restore from {BackupPath}" +``` + +#### Backup Deletion: +```csharp +// Before (in PostgresDatabaseProvider): +"Deleted local PostgreSQL backup: {BackupPath}" + +// After: +"Deleted local PostgreSQL backup created by external pg_dump: {BackupPath}" +``` + +#### Error Messages: +```csharp +// Before: +"{defaultExecutableName} executable not found. Please configure the path in database.xml or ensure it's in the system PATH." + +// After: +"External {defaultExecutableName} executable not found. Please configure the path in database.xml or ensure PostgreSQL client tools are installed and in the system PATH." +``` + +### PostgresDatabaseProvider.cs + +#### Initialization Messages: +```csharp +// Before: +"PostgreSQL backup service initialized for local database" +"PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" + +// After: +"PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools)" +"PostgreSQL database is on remote server ({Host}). Backup operations via external pg_dump/pg_restore tools are disabled" +``` + +#### Backup Operation Messages: +```csharp +// Before: +"PostgreSQL local backup created: {BackupPath}" +"Failed to create local PostgreSQL backup using pg_dump" +"Fast backup is not implemented for PostgreSQL on a remote server. Use pg_dump manually..." + +// After: +"PostgreSQL local backup created using external pg_dump: {BackupPath}" +"Failed to create local PostgreSQL backup using external pg_dump" +"Fast backup is not implemented for PostgreSQL on a remote server. Use external pg_dump command manually..." +``` + +#### Restore Operation Messages: +```csharp +// Before: +"PostgreSQL local backup restored from: {BackupPath}" +"Failed to restore local PostgreSQL backup using pg_restore" +"Fast restore is not implemented for PostgreSQL on a remote server. Use pg_restore manually..." + +// After: +"PostgreSQL local backup restored using external pg_restore from: {BackupPath}" +"Failed to restore local PostgreSQL backup using external pg_restore" +"Fast restore is not implemented for PostgreSQL on a remote server. Use external pg_restore command manually..." +``` + +#### Delete Operation Messages: +```csharp +// Before: +"Failed to delete local PostgreSQL backup: {BackupKey}" +"Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally." + +// After: +"Failed to delete local PostgreSQL backup created by external pg_dump: {BackupKey}" +"Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups created by external pg_dump/pg_restore manually." +``` + +## Complete Log Flow Examples + +### Successful Local Backup: +``` +[INFO] PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools) +[INFO] Starting PostgreSQL backup using external pg_dump tool to /data/backups/jellyfin_postgres_backup_20250128_143022.dump +[DEBUG] Executing external command: pg_dump -h localhost -p 5432 -U jellyfin -d jellyfin -F c -Z 6 -b -v -f "/data/backups/jellyfin_postgres_backup_20250128_143022.dump" +[INFO] Successfully created PostgreSQL backup using external pg_dump at /data/backups/jellyfin_postgres_backup_20250128_143022.dump (15728640 bytes) +[INFO] PostgreSQL local backup created using external pg_dump: /data/backups/jellyfin_postgres_backup_20250128_143022.dump +``` + +### Successful Local Restore: +``` +[INFO] Starting PostgreSQL restore using external pg_restore tool from /data/backups/jellyfin_postgres_backup_20250128_143022.dump +[INFO] Successfully restored PostgreSQL backup using external pg_restore from /data/backups/jellyfin_postgres_backup_20250128_143022.dump +[INFO] PostgreSQL local backup restored using external pg_restore from: /data/backups/jellyfin_postgres_backup_20250128_143022.dump +``` + +### Successful Local Delete: +``` +[INFO] Deleted local PostgreSQL backup created by external pg_dump: /data/backups/jellyfin_postgres_backup_20250128_143022.dump +``` + +### Remote Database (Backups Disabled): +``` +[INFO] PostgreSQL database is on remote server (db.example.com). Backup operations via external pg_dump/pg_restore tools are disabled +[WARN] Fast backup is not implemented for PostgreSQL on a remote server. Use external pg_dump command manually for proper backups. +``` + +### Error: External Tool Not Found: +``` +[ERROR] External pg_dump executable not found. Please configure the path in database.xml or ensure PostgreSQL client tools are installed and in the system PATH. +``` + +## Benefits of Updated Messaging + +1. **Clarity**: Users understand that these are external processes, not built-in functionality +2. **Troubleshooting**: Makes it clear that PostgreSQL client tools must be installed +3. **Expectations**: Sets proper expectations about what the system is doing +4. **Documentation**: Helps users understand the requirement for external dependencies +5. **Transparency**: Makes the technical implementation more transparent + +## Technical Details + +- **Process Execution**: `System.Diagnostics.Process.Start()` is used to launch pg_dump/pg_restore +- **Environment Variables**: `PGPASSWORD` is set in the process environment for authentication +- **Standard Output/Error**: Captured for logging and error handling +- **Working Directory**: Backup files are created in the Jellyfin data directory +- **Timeout Handling**: Configurable timeout prevents hung processes + +## For Users + +These updated messages help users understand that: +1. Jellyfin requires PostgreSQL client tools to be installed +2. The backup functionality calls out to external commands +3. Path configuration in `database.xml` points to actual executables +4. Error messages about missing executables mean the tools need to be installed + +## For Developers + +The updated logging makes it clear: +1. We're using process execution, not a library +2. External dependencies are required +3. Configuration affects external tool execution +4. Debugging should include checking if tools are in PATH or configured correctly diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll index 2df42d2a8d35880a2ffd999315f8bbdc735b03f7..13b20b9bf9c1a2f2cec4a6f0f7a7b629c3b231f5 100644 GIT binary patch delta 235 zcmZp0X>gg)!IHc3^ZAWEF`@$ZcjfQg*~He-j2{hGd2mh9rhG z21_7m%wWo3z>vyd1|%(j;>kd^A%hVR8!(syaSBl042VH`Kyn5Qi9j+DD3%0NW4?L0 Hd?XV9Gm21T delta 235 zcmZp0X>gg)!6H`4vT|cjjHtkr2sweYsm|`r*LF^BHBFS={6lmPlZL6WNs>WwN~)!y zg@v(YVsfIPnQ4-FQi@TMVX}Ejnz@-FP|U(8dGZ~JH7rY;uN;`%BN-szE#6>z;P+FX zSRACOpDGWwH(hw+a4rHeRNi!f1qRs*+m%Mqo Hd?XV9_ti|z diff --git a/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/bin/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index dbe9a4331d13c6d95dbf607665b7d995a1a97c90..731d1b96c4fda4c82e617a6e4fc48290f5d03e84 100644 GIT binary patch delta 559 zcmV+~0?_^JPwY>Sd=%7N{a{ZO)Iz`Wh$4B|&=KXLRhp5It|1O10RY$|0RZzO0RW~X z000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm}0|55`b^rqa?g0P+ z1^|L2AOQvdxFjF}2LQXXT>^;$e@r=2U=`N;WtyQ_XqTgtV=-$rR(M_jsw)(}Ii422 z2Z41p1ONc7lTB+JF${+9f&2%NQ%i}*ll@5ZL1DWkrG-LCLV74Y$hKxy;IWG>H_X2; z-h^RG+pvd~PnKUldM(>cb9&G?K581c#v<{wr$7mX+`DbE#{Gsf*ysoze=)-(05bxD z#OT!|y)8j_Ab6mZIfl!*(rTt>?S-13X|1bn(ylicddeUXq*mzI6AU7D zJ!|#3c8lf0I!hV}m!>`6zwFl(U9obwuHLhFIvnoh&TilH$K5|z8ejgGyZ42?gKvq2 xYT28F{Xjv$ddykXeZK*-egSd=yI;fIYzb^;$f0{@Q&c~4zT-uTp!@vLupL)}Q#i)F4UuK9Pd(w$@ zgMoH61ONc7lTB+IF%X9Df&7Odr<4-yuI=G925sV1gxhf58N!0Q3L| z5`t5s^f7zkp5P0`#KB+ARIR4Gv04Wt(b_6{RU@#{F_9nzg zMnKaBe{zU`-ogtz2b#czB*5)RlMTIk&@DVZ%87jkPeT&wuYRAZ zM`|4z;oz@|^c$u`y>`Rpf1d)idRtqE{s;PDB@$U@q0^H)a&BnZt97un^5}-d>Ezw_ zy@9^vE=Mfc9qdQax!WIe%Z}b|cn8v`x{UoXpM4u*zx9IdAH4VH)0wV~rDd~NOqXb| zxj5HzZ6~@hbKA}@8Z@|=nElH>#9*_L!*%tU#nIt#|L^SfJ%8H$8-wNL-#>lG>@0kZ wEL6(gM(ld>0>)uVs_gp(vwa3&AQKh=qa6XGIsv0b0i#_3qk93Ppp)<>`@01H$^ZZW diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs index 0f504abe..7ecbdde6 100644 --- a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs +++ b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+534b0cde918839aca165b7bd2b1c7df761aca82c")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+43cdbf9b350e608dce121070d565cdce0aea6b17")] [assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache index 129fe210..310263ac 100644 --- a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache +++ b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.AssemblyInfoInputs.cache @@ -1 +1 @@ -16d1d32a5e6e4b7472006b4665d10d7735bf11c7602c8304312d34d2b63afa7f +bdef8325a5a74be1c67e80979e05ef8eaf8569901dd2077003e91c35b6d8e7dd diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.dll index 2df42d2a8d35880a2ffd999315f8bbdc735b03f7..13b20b9bf9c1a2f2cec4a6f0f7a7b629c3b231f5 100644 GIT binary patch delta 235 zcmZp0X>gg)!IHc3^ZAWEF`@$ZcjfQg*~He-j2{hGd2mh9rhG z21_7m%wWo3z>vyd1|%(j;>kd^A%hVR8!(syaSBl042VH`Kyn5Qi9j+DD3%0NW4?L0 Hd?XV9Gm21T delta 235 zcmZp0X>gg)!6H`4vT|cjjHtkr2sweYsm|`r*LF^BHBFS={6lmPlZL6WNs>WwN~)!y zg@v(YVsfIPnQ4-FQi@TMVX}Ejnz@-FP|U(8dGZ~JH7rY;uN;`%BN-szE#6>z;P+FX zSRACOpDGWwH(hw+a4rHeRNi!f1qRs*+m%Mqo Hd?XV9_ti|z diff --git a/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb b/src/Jellyfin.CodeAnalysis/obj/Debug/netstandard2.0/Jellyfin.CodeAnalysis.pdb index dbe9a4331d13c6d95dbf607665b7d995a1a97c90..731d1b96c4fda4c82e617a6e4fc48290f5d03e84 100644 GIT binary patch delta 559 zcmV+~0?_^JPwY>Sd=%7N{a{ZO)Iz`Wh$4B|&=KXLRhp5It|1O10RY$|0RZzO0RW~X z000000RX`y00000000000RYL9UjfrA!XqaD1OR0NCjbQiq6Jm}0|55`b^rqa?g0P+ z1^|L2AOQvdxFjF}2LQXXT>^;$e@r=2U=`N;WtyQ_XqTgtV=-$rR(M_jsw)(}Ii422 z2Z41p1ONc7lTB+JF${+9f&2%NQ%i}*ll@5ZL1DWkrG-LCLV74Y$hKxy;IWG>H_X2; z-h^RG+pvd~PnKUldM(>cb9&G?K581c#v<{wr$7mX+`DbE#{Gsf*ysoze=)-(05bxD z#OT!|y)8j_Ab6mZIfl!*(rTt>?S-13X|1bn(ylicddeUXq*mzI6AU7D zJ!|#3c8lf0I!hV}m!>`6zwFl(U9obwuHLhFIvnoh&TilH$K5|z8ejgGyZ42?gKvq2 xYT28F{Xjv$ddykXeZK*-egSd=yI;fIYzb^;$f0{@Q&c~4zT-uTp!@vLupL)}Q#i)F4UuK9Pd(w$@ zgMoH61ONc7lTB+IF%X9Df&7Odr<4-yuI=G925sV1gxhf58N!0Q3L| z5`t5s^f7zkp5P0`#KB+ARIR4Gv04Wt(b_6{RU@#{F_9nzg zMnKaBe{zU`-ogtz2b#czB*5)RlMTIk&@DVZ%87jkPeT&wuYRAZ zM`|4z;oz@|^c$u`y>`Rpf1d)idRtqE{s;PDB@$U@q0^H)a&BnZt97un^5}-d>Ezw_ zy@9^vE=Mfc9qdQax!WIe%Z}b|cn8v`x{UoXpM4u*zx9IdAH4VH)0wV~rDd~NOqXb| zxj5HzZ6~@hbKA}@8Z@|=nElH>#9*_L!*%tU#nIt#|L^SfJ%8H$8-wNL-#>lG>@0kZ wEL6(gM(ld>0>)uVs_gp(vwa3&AQKh=qa6XGIsv0b0i#_3qk93Ppp)<>`@01H$^ZZW diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseBackupOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseBackupOptions.cs new file mode 100644 index 00000000..3289690a --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseBackupOptions.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Jellyfin.Database.Implementations.DbConfiguration; + +/// +/// Options to configure database backup behavior. +/// +public class DatabaseBackupOptions +{ + /// + /// Gets or sets the path to the pg_dump executable. + /// If not specified, will search in system PATH. + /// + /// + /// Examples: + /// - Windows: "C:\\Program Files\\PostgreSQL\\16\\bin\\pg_dump.exe" + /// - Linux: "/usr/bin/pg_dump" + /// - Use "pg_dump" to search in PATH. + /// + public string? PgDumpPath { get; set; } + + /// + /// Gets or sets the path to the pg_restore executable. + /// If not specified, will search in system PATH. + /// + public string? PgRestorePath { get; set; } + + /// + /// Gets or sets the backup format. + /// + /// + /// Valid values: + /// - "custom" (default): Custom compressed format (pg_restore compatible) + /// - "plain": Plain SQL script + /// - "directory": Directory format + /// - "tar": Tar archive format. + /// + public string BackupFormat { get; set; } = "custom"; + + /// + /// Gets or sets a value indicating whether to include large objects (blobs) in backup. + /// + public bool IncludeBlobs { get; set; } = true; + + /// + /// Gets or sets the compression level (0-9, where 9 is maximum compression). + /// Only applies to custom and directory formats. + /// + public int CompressionLevel { get; set; } = 6; + + /// + /// Gets or sets additional pg_dump arguments. + /// + /// + /// Example: "--verbose --exclude-table-data=public.logs". + /// + public string? AdditionalArguments { get; set; } + + /// + /// Gets or sets the timeout in seconds for backup operations. + /// + public int TimeoutSeconds { get; set; } = 1800; // 30 minutes + + /// + /// Gets or sets a value indicating whether to enable verbose output. + /// + public bool VerboseOutput { get; set; } = true; + + /// + /// Gets or sets the number of parallel jobs for pg_dump (if format is 'directory'). + /// + public int? ParallelJobs { get; set; } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs index 902aae66..151dd769 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -4,8 +4,6 @@ namespace Jellyfin.Database.Implementations.DbConfiguration; -using System.Collections.Generic; - /// /// Options to configure jellyfins managed database. /// @@ -26,4 +24,9 @@ public class DatabaseConfigurationOptions /// Defaults to "NoLock". /// public DatabaseLockingBehaviorTypes LockingBehavior { get; set; } + + /// + /// Gets or sets the backup configuration options. + /// + public DatabaseBackupOptions? BackupOptions { get; set; } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.xml.example b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.xml.example new file mode 100644 index 00000000..c2882eb0 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.xml.example @@ -0,0 +1,73 @@ + + + + Jellyfin-PostgreSQL + + + NoLock + + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password_here + + + + + Host + localhost + + + + + Port + 5432 + + + + + Database + jellyfin + + + + + + + + + + + + + pg_dump + + + pg_restore + + + custom + + + true + + + 6 + + + 1800 + + + true + + + + + + + + diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/BACKUP_CONFIGURATION.md b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/BACKUP_CONFIGURATION.md new file mode 100644 index 00000000..ab5577d7 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/BACKUP_CONFIGURATION.md @@ -0,0 +1,258 @@ +# PostgreSQL Backup Configuration + +This document explains how to configure PostgreSQL backup options in Jellyfin using the `database.xml` configuration file. + +## **IMPORTANT: Local Database Requirement** + +**pg_dump and pg_restore backup features are ONLY available when the PostgreSQL database host is `localhost` or `127.0.0.1` (or `::1` for IPv6).** + +- ✅ **Local database** (localhost/127.0.0.1): Automated backups via pg_dump/pg_restore are enabled +- ❌ **Remote database**: Backup settings are ignored; you must manage backups externally + +When using a remote PostgreSQL server, Jellyfin will log: +> "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" + +And backup operations will show: +> "Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally." + +## Configuration File Location + +The `database.xml` file should be placed in your Jellyfin configuration directory: + +- **Windows**: `%AppData%\Jellyfin\Server\config\database.xml` +- **Linux**: `~/.config/jellyfin/config/database.xml` or `/etc/jellyfin/config/database.xml` +- **Docker**: Mount to `/config/database.xml` + +## Basic Configuration + +Here's a minimal configuration for PostgreSQL with default backup settings: + +```xml + + Jellyfin-PostgreSQL + NoLock + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password + + + + + pg_dump + custom + + +``` + +## Backup Options Reference + +### PgDumpPath +**Type**: `string` (optional) +**Default**: Searches in system PATH + +The full path to the `pg_dump` executable. If not specified, Jellyfin will search common locations: +- Windows: `C:\Program Files\PostgreSQL\{version}\bin\pg_dump.exe` +- Linux: `/usr/bin/pg_dump`, `/usr/local/bin/pg_dump` + +**Examples**: +```xml + +C:\Program Files\PostgreSQL\16\bin\pg_dump.exe + + +/usr/bin/pg_dump + + +pg_dump +``` + +### PgRestorePath +**Type**: `string` (optional) +**Default**: Searches in system PATH + +Path to the `pg_restore` executable. Same search behavior as `PgDumpPath`. + +### BackupFormat +**Type**: `string` +**Default**: `custom` +**Valid values**: `custom`, `plain`, `directory`, `tar` + +The backup file format: +- **custom**: Compressed custom format (recommended) - can be restored with `pg_restore` +- **plain**: Plain SQL script - can be restored with `psql` +- **directory**: Directory format with one file per table +- **tar**: Tar archive format + +```xml +custom +``` + +### IncludeBlobs +**Type**: `boolean` +**Default**: `true` + +Whether to include large objects (BLOBs) in the backup. + +```xml +true +``` + +### CompressionLevel +**Type**: `integer` (0-9) +**Default**: `6` + +Compression level for custom and directory formats. Higher values mean better compression but slower speed. +- `0` = No compression +- `9` = Maximum compression +- `6` = Good balance (recommended) + +```xml +6 +``` + +### TimeoutSeconds +**Type**: `integer` +**Default**: `1800` (30 minutes) + +Maximum time in seconds to wait for backup/restore operations to complete. + +```xml +1800 +``` + +### VerboseOutput +**Type**: `boolean` +**Default**: `true` + +Enable verbose logging output from pg_dump/pg_restore operations. + +```xml +true +``` + +### ParallelJobs +**Type**: `integer` (optional) +**Default**: `null` + +Number of parallel jobs for backup operations. Only works with `directory` format. + +```xml +directory +4 +``` + +### AdditionalArguments +**Type**: `string` (optional) +**Default**: `null` + +Additional command-line arguments to pass to pg_dump. + +**Examples**: +```xml + +--exclude-table-data=public.temp_logs + + +--exclude-schema=archive + + +--exclude-table-data=public.logs --exclude-schema=test +``` + +## Complete Example Configuration + +```xml + + Jellyfin-PostgreSQL + NoLock + + + Jellyfin-PostgreSQL + Jellyfin.Database.Providers.Postgres + Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password + + + + + C:\Program Files\PostgreSQL\16\bin\pg_dump.exe + C:\Program Files\PostgreSQL\16\bin\pg_restore.exe + + + custom + 6 + true + + + 3600 + true + + + + + + + + +``` + +## Usage in Code + +The backup service will automatically read these settings from `database.xml`: + +```csharp +// Inject the service +public class MyClass +{ + private readonly PostgresBackupService _backupService; + + public MyClass(PostgresBackupService backupService) + { + _backupService = backupService; + } + + // Create a backup + public async Task CreateBackup() + { + var backupPath = await _backupService.CreateBackupAsync("/path/to/backups"); + Console.WriteLine($"Backup created: {backupPath}"); + } + + // Restore a backup + public async Task RestoreBackup(string backupFile) + { + await _backupService.RestoreBackupAsync(backupFile); + Console.WriteLine("Backup restored successfully"); + } +} +``` + +## Troubleshooting + +### "pg_dump executable not found" +1. Verify PostgreSQL is installed +2. Check that `pg_dump` is in your system PATH, or +3. Specify the full path in `` configuration + +### Permission Issues +- Ensure the user running Jellyfin has execute permissions for `pg_dump` +- Verify database credentials have backup privileges + +### Backup Takes Too Long +- Increase `` value +- Consider using `` with directory format for large databases +- Reduce `` for faster backups + +### Restore Fails +- Verify the backup file format matches what pg_restore expects +- Check that the target database exists and is accessible +- Review logs for specific error messages + +## Security Notes + +1. **Password Storage**: The connection string in `database.xml` contains the database password. Ensure this file has appropriate permissions (readable only by the Jellyfin user). + +2. **PGPASSWORD**: The service uses the `PGPASSWORD` environment variable to pass credentials to pg_dump, which is more secure than command-line arguments. + +3. **Backup Files**: Backup files contain your entire database. Store them securely and restrict access appropriately. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs new file mode 100644 index 00000000..9a80eecc --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs @@ -0,0 +1,481 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Jellyfin.Database.Providers.Postgres; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations.DbConfiguration; +using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.Logging; + +/// +/// Service for creating and restoring PostgreSQL database backups using pg_dump/pg_restore. +/// +public class PostgresBackupService +{ + private readonly ILogger _logger; + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The configuration manager. + public PostgresBackupService( + ILogger logger, + IConfigurationManager configurationManager) + { + _logger = logger; + _configurationManager = configurationManager; + } + + /// + /// Creates a backup of the PostgreSQL database. + /// + /// The directory to save the backup file. + /// Cancellation token. + /// The path to the created backup file. + public async Task CreateBackupAsync(string backupDirectory, CancellationToken cancellationToken = default) + { + var dbConfig = _configurationManager.GetConfiguration("database"); + var backupOptions = dbConfig.BackupOptions ?? new DatabaseBackupOptions(); + + if (dbConfig.CustomProviderOptions?.ConnectionString == null) + { + throw new InvalidOperationException("Database connection string is not configured"); + } + + var connectionString = dbConfig.CustomProviderOptions.ConnectionString; + var connectionParams = ParseConnectionString(connectionString); + + var pgDumpPath = FindExecutable(backupOptions.PgDumpPath, "pg_dump"); + var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); + var extension = GetFileExtension(backupOptions.BackupFormat); + var backupFileName = $"jellyfin_postgres_backup_{timestamp}.{extension}"; + var fullBackupPath = Path.Combine(backupDirectory, backupFileName); + + Directory.CreateDirectory(backupDirectory); + + var arguments = BuildPgDumpArguments(connectionParams, backupOptions, fullBackupPath); + + _logger.LogInformation("Starting PostgreSQL backup using external pg_dump tool to {BackupPath}", fullBackupPath); + _logger.LogDebug("Executing external command: {PgDump} {Arguments}", pgDumpPath, string.Join(" ", arguments.Where(a => !a.Contains("PGPASSWORD", StringComparison.OrdinalIgnoreCase)))); + + var processStartInfo = new ProcessStartInfo + { + FileName = pgDumpPath, + Arguments = string.Join(" ", arguments), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + // Set password via environment variable (more secure than command line) + if (connectionParams.TryGetValue("Password", out var password)) + { + processStartInfo.EnvironmentVariables["PGPASSWORD"] = password; + } + + using var process = new Process { StartInfo = processStartInfo }; + + var outputBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + { + outputBuilder.AppendLine(e.Data); + if (backupOptions.VerboseOutput) + { + _logger.LogDebug("pg_dump: {Output}", e.Data); + } + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + { + errorBuilder.AppendLine(e.Data); + if (backupOptions.VerboseOutput) + { + _logger.LogDebug("pg_dump stderr: {Error}", e.Data); + } + } + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait with timeout + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(backupOptions.TimeoutSeconds)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + try + { + await process.WaitForExitAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + process.Kill(); + throw new TimeoutException($"Backup operation timed out after {backupOptions.TimeoutSeconds} seconds"); + } + + if (process.ExitCode != 0) + { + var errorMessage = errorBuilder.ToString(); + _logger.LogError("pg_dump failed with exit code {ExitCode}: {Error}", process.ExitCode, errorMessage); + + // Clean up failed backup file + if (File.Exists(fullBackupPath)) + { + File.Delete(fullBackupPath); + } + + throw new Exception($"Database backup failed: {errorMessage}"); + } + + var fileInfo = new FileInfo(fullBackupPath); + _logger.LogInformation( + "Successfully created PostgreSQL backup at {BackupPath} ({Size:N0} bytes)", + fullBackupPath, + fileInfo.Length); + + return fullBackupPath; + } + + /// + /// Restores a PostgreSQL database from a backup file. + /// + /// The path to the backup file. + /// Cancellation token. + /// A task representing the restore operation. + public async Task RestoreBackupAsync(string backupFilePath, CancellationToken cancellationToken = default) + { + var dbConfig = _configurationManager.GetConfiguration("database"); + var backupOptions = dbConfig.BackupOptions ?? new DatabaseBackupOptions(); + + if (!File.Exists(backupFilePath)) + { + throw new FileNotFoundException($"Backup file not found: {backupFilePath}"); + } + + if (dbConfig.CustomProviderOptions?.ConnectionString == null) + { + throw new InvalidOperationException("Database connection string is not configured"); + } + + var connectionString = dbConfig.CustomProviderOptions.ConnectionString; + var connectionParams = ParseConnectionString(connectionString); + + var pgRestorePath = FindExecutable(backupOptions.PgRestorePath, "pg_restore"); + + var arguments = BuildPgRestoreArguments(connectionParams, backupOptions, backupFilePath); + + _logger.LogInformation("Starting PostgreSQL restore from {BackupPath}", backupFilePath); + + var processStartInfo = new ProcessStartInfo + { + FileName = pgRestorePath, + Arguments = string.Join(" ", arguments), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + if (connectionParams.TryGetValue("Password", out var password)) + { + processStartInfo.EnvironmentVariables["PGPASSWORD"] = password; + } + + using var process = new Process { StartInfo = processStartInfo }; + + var outputBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + { + outputBuilder.AppendLine(e.Data); + if (backupOptions.VerboseOutput) + { + _logger.LogDebug("pg_restore: {Output}", e.Data); + } + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + { + errorBuilder.AppendLine(e.Data); + if (backupOptions.VerboseOutput) + { + _logger.LogDebug("pg_restore stderr: {Error}", e.Data); + } + } + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(backupOptions.TimeoutSeconds)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + try + { + await process.WaitForExitAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + process.Kill(); + throw new TimeoutException($"Restore operation timed out after {backupOptions.TimeoutSeconds} seconds"); + } + + if (process.ExitCode != 0) + { + var errorMessage = errorBuilder.ToString(); + _logger.LogError("pg_restore failed with exit code {ExitCode}: {Error}", process.ExitCode, errorMessage); + throw new Exception($"Database restore failed: {errorMessage}"); + } + + _logger.LogInformation("Successfully restored PostgreSQL backup from {BackupPath}", backupFilePath); + } + + private static Dictionary ParseConnectionString(string connectionString) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var parts = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var kvp = part.Split('=', 2); + if (kvp.Length == 2) + { + result[kvp[0].Trim()] = kvp[1].Trim(); + } + } + + return result; + } + + private List BuildPgDumpArguments( + Dictionary connectionParams, + DatabaseBackupOptions options, + string outputPath) + { + var args = new List(); + + // Connection parameters + if (connectionParams.TryGetValue("Host", out var host)) + { + args.Add($"-h {host}"); + } + + if (connectionParams.TryGetValue("Port", out var port)) + { + args.Add($"-p {port}"); + } + + if (connectionParams.TryGetValue("Username", out var username)) + { + args.Add($"-U {username}"); + } + + if (connectionParams.TryGetValue("Database", out var database)) + { + args.Add($"-d {database}"); + } + + // Format + var formatFlag = options.BackupFormat.ToLowerInvariant() switch + { + "plain" => "p", + "custom" => "c", + "directory" => "d", + "tar" => "t", + _ => "c" + }; + args.Add($"-F {formatFlag}"); + + // Compression level (for custom and directory formats) + if (formatFlag is "c" or "d") + { + args.Add($"-Z {options.CompressionLevel}"); + } + + // Include blobs + if (options.IncludeBlobs) + { + args.Add("-b"); + } + + // Verbose output + if (options.VerboseOutput) + { + args.Add("-v"); + } + + // Parallel jobs (directory format only) + if (options.ParallelJobs.HasValue && formatFlag == "d") + { + args.Add($"-j {options.ParallelJobs.Value}"); + } + + // Additional arguments + if (!string.IsNullOrWhiteSpace(options.AdditionalArguments)) + { + args.Add(options.AdditionalArguments); + } + + // Output file + args.Add($"-f \"{outputPath}\""); + + return args; + } + + private List BuildPgRestoreArguments( + Dictionary connectionParams, + DatabaseBackupOptions options, + string backupPath) + { + var args = new List(); + + // Connection parameters + if (connectionParams.TryGetValue("Host", out var host)) + { + args.Add($"-h {host}"); + } + + if (connectionParams.TryGetValue("Port", out var port)) + { + args.Add($"-p {port}"); + } + + if (connectionParams.TryGetValue("Username", out var username)) + { + args.Add($"-U {username}"); + } + + if (connectionParams.TryGetValue("Database", out var database)) + { + args.Add($"-d {database}"); + } + + // Clean before restore + args.Add("--clean"); + args.Add("--if-exists"); + + // Verbose output + if (options.VerboseOutput) + { + args.Add("-v"); + } + + // Parallel jobs + if (options.ParallelJobs.HasValue) + { + args.Add($"-j {options.ParallelJobs.Value}"); + } + + // Input file + args.Add($"\"{backupPath}\""); + + return args; + } + + private string FindExecutable(string? configuredPath, string defaultExecutableName) + { + // If configured path is provided, use it + if (!string.IsNullOrWhiteSpace(configuredPath)) + { + if (File.Exists(configuredPath)) + { + return configuredPath; + } + + _logger.LogWarning("Configured path {Path} does not exist, searching in PATH", configuredPath); + } + + // Try to find in PATH + var possiblePaths = new[] + { + defaultExecutableName, // In PATH + Path.Combine("/usr/bin", defaultExecutableName), + Path.Combine("/usr/local/bin", defaultExecutableName), + Path.Combine(@"C:\Program Files\PostgreSQL\16\bin", $"{defaultExecutableName}.exe"), + Path.Combine(@"C:\Program Files\PostgreSQL\15\bin", $"{defaultExecutableName}.exe"), + Path.Combine(@"C:\Program Files\PostgreSQL\14\bin", $"{defaultExecutableName}.exe"), + }; + + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + _logger.LogDebug("Found {Executable} at {Path}", defaultExecutableName, path); + return path; + } + + // Check if it's in PATH by trying to execute --version + if (IsInPath(path)) + { + _logger.LogDebug("Found {Executable} in PATH", defaultExecutableName); + return path; + } + } + + throw new FileNotFoundException($"{defaultExecutableName} executable not found. Please configure the path in database.xml or ensure it's in the system PATH."); + } + + private bool IsInPath(string fileName) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = "--version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + process.WaitForExit(2000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static string GetFileExtension(string format) + { + return format.ToLowerInvariant() switch + { + "plain" => "sql", + "custom" => "dump", + "directory" => "dir", + "tar" => "tar", + _ => "dump" + }; + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs index f889d218..95bee9f7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs @@ -9,6 +9,7 @@ namespace Jellyfin.Database.Providers.Postgres; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -29,16 +30,24 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider { private readonly IApplicationPaths applicationPaths; private readonly ILogger logger; + private readonly IConfigurationManager? configurationManager; + private PostgresBackupService? backupService; + private string? currentHost; /// /// Initializes a new instance of the class. /// /// Service to construct the fallback when the old data path configuration is used. /// A logger. - public PostgresDatabaseProvider(IApplicationPaths applicationPaths, ILogger logger) + /// The configuration manager (optional, for backup support). + public PostgresDatabaseProvider( + IApplicationPaths applicationPaths, + ILogger logger, + IConfigurationManager? configurationManager = null) { this.applicationPaths = applicationPaths; this.logger = logger; + this.configurationManager = configurationManager; } /// @@ -118,6 +127,9 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider var connectionString = connectionBuilder.ToString(); + // Store the host for backup operations + currentHost = connectionBuilder.Host; + // Log PostgreSQL connection parameters (without password) logger.LogInformation( "PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}, MaxPoolSize={MaxPoolSize}, Multiplexing={Multiplexing}", @@ -129,6 +141,19 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider connectionBuilder.MaxPoolSize, connectionBuilder.Multiplexing); + // Initialize backup service if host is local and configuration manager is available + if (IsLocalHost(currentHost) && configurationManager is not null) + { + var loggerFactory = Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance; + var backupLogger = loggerFactory.CreateLogger(); + backupService = new PostgresBackupService(backupLogger, configurationManager); + logger.LogInformation("PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools)"); + } + else if (!IsLocalHost(currentHost)) + { + logger.LogInformation("PostgreSQL database is on remote server ({Host}). Backup operations via external pg_dump/pg_restore tools are disabled", currentHost); + } + options .UseNpgsql( connectionString, @@ -394,28 +419,134 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider /// public async Task MigrationBackupFast(CancellationToken cancellationToken) { + if (IsLocalHost(currentHost) && backupService is not null) + { + try + { + var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups"); + var backupPath = await backupService.CreateBackupAsync(backupDirectory, cancellationToken).ConfigureAwait(false); + logger.LogInformation("PostgreSQL local backup created: {BackupPath}", backupPath); + return Path.GetFileName(backupPath); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create local PostgreSQL backup using pg_dump"); + throw; + } + } + var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture); - - logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups."); - - // Return a key for compatibility, but actual backup should be done externally + logger.LogWarning("Fast backup is not implemented for PostgreSQL on a remote server. Use pg_dump manually for proper backups."); return await Task.FromResult(key).ConfigureAwait(false); } /// - public Task RestoreBackupFast(string key, CancellationToken cancellationToken) + public async Task RestoreBackupFast(string key, CancellationToken cancellationToken) { - logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration."); - return Task.CompletedTask; + if (IsLocalHost(currentHost) && backupService is not null) + { + try + { + var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups"); + var backupPath = Path.Combine(backupDirectory, key); + + if (!File.Exists(backupPath)) + { + // Try to find the backup file with common extensions + var possibleExtensions = new[] { ".dump", ".sql", ".tar" }; + foreach (var ext in possibleExtensions) + { + var pathWithExt = backupPath + ext; + if (File.Exists(pathWithExt)) + { + backupPath = pathWithExt; + break; + } + } + } + + if (File.Exists(backupPath)) + { + await backupService.RestoreBackupAsync(backupPath, cancellationToken).ConfigureAwait(false); + logger.LogInformation("PostgreSQL local backup restored from: {BackupPath}", backupPath); + return; + } + + logger.LogWarning("Backup file not found: {BackupPath}", backupPath); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to restore local PostgreSQL backup using pg_restore"); + throw; + } + } + + logger.LogWarning("Fast restore is not implemented for PostgreSQL on a remote server. Use pg_restore manually for proper restoration."); } /// public Task DeleteBackup(string key) { - logger.LogWarning("Backup deletion is not implemented for PostgreSQL. Manage backups externally."); + if (IsLocalHost(currentHost) && backupService is not null) + { + try + { + var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups"); + var backupPath = Path.Combine(backupDirectory, key); + + // Try to find and delete the backup file + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", backupPath); + } + else + { + // Try with common extensions + var possibleExtensions = new[] { ".dump", ".sql", ".tar" }; + foreach (var ext in possibleExtensions) + { + var pathWithExt = backupPath + ext; + if (File.Exists(pathWithExt)) + { + File.Delete(pathWithExt); + logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", pathWithExt); + return Task.CompletedTask; + } + } + + logger.LogWarning("Backup file not found for deletion: {BackupPath}", backupPath); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete local PostgreSQL backup: {BackupKey}", key); + } + + return Task.CompletedTask; + } + + logger.LogWarning("Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally."); return Task.CompletedTask; } + /// + /// Determines if the given host is localhost. + /// + /// The host to check. + /// True if the host is localhost or 127.0.0.1, false otherwise. + private static bool IsLocalHost(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + { + return false; + } + + return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) || + host.Equals("127.0.0.1", StringComparison.Ordinal) || + host.Equals("::1", StringComparison.Ordinal); // IPv6 localhost + } + /// public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable? tableNames) {