From 5d4a15aad01bf03da401d5f2bb65f4ad4f014176 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 21 Apr 2026 14:50:30 +0200 Subject: [PATCH] Fixed a settings save bug In some cases the database update can fail, and this could leave the transaction open. Eventually, it will be collected and the transaction disposed so it will be sorted, but in the meantime attempts to save settings will fail as SQLite does not support multiple active transactions. This PR adds failure dispose of the transactions in a few places to ensure failures does not cascade outside the original failure. --- .../Library/RestAPI/Database/Connection.cs | 180 ++++++++++-------- 1 file changed, 100 insertions(+), 80 deletions(-) diff --git a/Duplicati/Library/RestAPI/Database/Connection.cs b/Duplicati/Library/RestAPI/Database/Connection.cs index d1791323e..d081c1f40 100644 --- a/Duplicati/Library/RestAPI/Database/Connection.cs +++ b/Duplicati/Library/RestAPI/Database/Connection.cs @@ -266,21 +266,26 @@ namespace Duplicati.Server.Database lock (m_lock) { var tr = transaction ?? m_connection.BeginTransaction(); - OverwriteAndUpdateDb( - tr, - cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @Id") - .SetParameterValue("@Id", id), - values ?? new Dictionary(), - cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (@BackupId, @Name, @Value)"), - (cmd, f) => cmd.SetParameterValue("@BackupId", id) - .SetParameterValue("@Name", f.Key) - .SetParameterValue("@Value", f.Value) - ); - - if (transaction == null) + try { - tr.Commit(); - tr.Dispose(); + OverwriteAndUpdateDb( + tr, + cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @Id") + .SetParameterValue("@Id", id), + values ?? new Dictionary(), + cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (@BackupId, @Name, @Value)"), + (cmd, f) => cmd.SetParameterValue("@BackupId", id) + .SetParameterValue("@Name", f.Key) + .SetParameterValue("@Value", f.Value) + ); + + if (transaction == null) + tr.Commit(); + } + finally + { + if (transaction == null) + tr.Dispose(); } } } @@ -305,22 +310,27 @@ namespace Duplicati.Server.Database lock (m_lock) { var tr = transaction ?? m_connection.BeginTransaction(); - OverwriteAndUpdateDb( - tr, - cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Filter"" WHERE ""BackupID"" = @Id") - .SetParameterValue("@Id", id), - values, - cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (@Id, @Order, @Include, @Expression)"), - (cmd, f) => cmd.SetParameterValue("@Id", id) - .SetParameterValue("@Order", f.Order) - .SetParameterValue("@Include", f.Include) - .SetParameterValue("@Expression", f.Expression) - ); - - if (transaction == null) + try { - tr.Commit(); - tr.Dispose(); + OverwriteAndUpdateDb( + tr, + cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Filter"" WHERE ""BackupID"" = @Id") + .SetParameterValue("@Id", id), + values, + cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (@Id, @Order, @Include, @Expression)"), + (cmd, f) => cmd.SetParameterValue("@Id", id) + .SetParameterValue("@Order", f.Order) + .SetParameterValue("@Include", f.Include) + .SetParameterValue("@Expression", f.Expression) + ); + + if (transaction == null) + tr.Commit(); + } + finally + { + if (transaction == null) + tr.Dispose(); } } } @@ -369,36 +379,41 @@ namespace Duplicati.Server.Database lock (m_lock) { var tr = transaction ?? m_connection.BeginTransaction(); - if (m_encryptSensitiveFields) - values = values.Select(x => new Setting - { - Filter = x.Filter, - Name = x.Name, - Value = EncryptSensitiveFields(x.Name, x.Value, m_key) - }).ToList(); - - OverwriteAndUpdateDb( - tr, - cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Option"" WHERE ""BackupID"" = @Id") - .SetParameterValue("@Id", id), - values, - cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)"), - (cmd, f) => - { - if (IsPasswordPlaceholder(f.Value)) - throw new Exception("Attempted to save a property with the placeholder password"); - - cmd.SetParameterValue("@BackupId", id) - .SetParameterValue("@Filter", f.Filter ?? "") - .SetParameterValue("@Name", f.Name ?? "") - .SetParameterValue("@Value", f.Value ?? ""); - } - ); - - if (transaction == null) + try { - tr.Commit(); - tr.Dispose(); + if (m_encryptSensitiveFields) + values = values.Select(x => new Setting + { + Filter = x.Filter, + Name = x.Name, + Value = EncryptSensitiveFields(x.Name, x.Value, m_key) + }).ToList(); + + OverwriteAndUpdateDb( + tr, + cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Option"" WHERE ""BackupID"" = @Id") + .SetParameterValue("@Id", id), + values, + cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)"), + (cmd, f) => + { + if (IsPasswordPlaceholder(f.Value)) + throw new Exception("Attempted to save a property with the placeholder password"); + + cmd.SetParameterValue("@BackupId", id) + .SetParameterValue("@Filter", f.Filter ?? "") + .SetParameterValue("@Name", f.Name ?? "") + .SetParameterValue("@Value", f.Value ?? ""); + } + ); + + if (transaction == null) + tr.Commit(); + } + finally + { + if (transaction == null) + tr.Dispose(); } } @@ -445,29 +460,34 @@ namespace Duplicati.Server.Database lock (m_lock) { var tr = transaction ?? m_connection.BeginTransaction(); - OverwriteAndUpdateDb( - tr, - cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Source"" WHERE ""BackupID"" = @Id") - .SetParameterValue("@Id", id), - values, - cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (@BackupId, @Path)"), - (cmd, f) => - { - if (SourceMasking.IsSpecialSource(f) && UrlContainsPasswordPlaceholder(SourceMasking.ExtractUrl(f))) - throw new Exception("Attempted to save a source with a placeholder password"); - - cmd.SetParameterValue("@BackupId", id) - .SetParameterValue("@Path", - m_encryptSensitiveFields && SourceMasking.IsSpecialSource(f) - ? EncryptedFieldHelper.Encrypt(f, m_key) - : f); - } - ); - - if (transaction == null) + try { - tr.Commit(); - tr.Dispose(); + OverwriteAndUpdateDb( + tr, + cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Source"" WHERE ""BackupID"" = @Id") + .SetParameterValue("@Id", id), + values, + cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (@BackupId, @Path)"), + (cmd, f) => + { + if (SourceMasking.IsSpecialSource(f) && UrlContainsPasswordPlaceholder(SourceMasking.ExtractUrl(f))) + throw new Exception("Attempted to save a source with a placeholder password"); + + cmd.SetParameterValue("@BackupId", id) + .SetParameterValue("@Path", + m_encryptSensitiveFields && SourceMasking.IsSpecialSource(f) + ? EncryptedFieldHelper.Encrypt(f, m_key) + : f); + } + ); + + if (transaction == null) + tr.Commit(); + } + finally + { + if (transaction == null) + tr.Dispose(); } } }