From 7ce54a615c6d73fccd9b9d0b01914682ecf8e42d Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Tue, 21 Apr 2026 10:11:39 +0200 Subject: [PATCH 1/4] Moved ShouldTrigger check inside the try/catch to prevent failure of one destination to result in all failing. --- .../Main/Operation/RemoteSynchronizationHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Duplicati/Library/Main/Operation/RemoteSynchronizationHandler.cs b/Duplicati/Library/Main/Operation/RemoteSynchronizationHandler.cs index e21799f47..dc162735c 100644 --- a/Duplicati/Library/Main/Operation/RemoteSynchronizationHandler.cs +++ b/Duplicati/Library/Main/Operation/RemoteSynchronizationHandler.cs @@ -313,14 +313,14 @@ internal class RemoteSynchronizationHandler : IDisposable if (string.IsNullOrWhiteSpace(dest.Config.Dst)) continue; - if (!ShouldTriggerSync(i, dest)) - { - Logging.Log.WriteInformationMessage(LOGTAG, "RemoteSyncSkipped", "Remote synchronization to {0} skipped due to trigger mode conditions not met.", dest); - continue; - } - try { + if (!ShouldTriggerSync(i, dest)) + { + Logging.Log.WriteInformationMessage(LOGTAG, "RemoteSyncSkipped", "Remote synchronization to {0} skipped due to trigger mode conditions not met.", dest); + continue; + } + // Update progress to show sync is in progress m_results.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_RemoteSynchronization); m_results.OperationProgressUpdater.UpdateRemoteSyncDestination(i + 1, m_destinations.Count); From b26782ddf18c0a21ae8f3281f14acbb4f3721ec5 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 21 Apr 2026 14:47:03 +0200 Subject: [PATCH 2/4] Improve config speed from console This PR updates the remove configuration to receive configuration directly on connection, instead of waiting for the server to send it. This also adds a hibernation setting that will let the server tell the client to stop connecting for a while which can reduce resource consumption at the cost of not being always available. --- Duplicati/Agent/Program.cs | 6 ++ .../RemoteControl/ExchangeDataTypes.cs | 4 + .../RemoteControl/KeepRemoteConnection.cs | 88 ++++++++++++++++--- .../RemoteControl/RegisterForRemote.cs | 6 +- .../Library/RemoteControl/SharedTypes.cs | 4 +- .../Abstractions/IRemoteController.cs | 3 +- Duplicati/WebserverCore/DuplicatiWebserver.cs | 2 +- .../Endpoints/V1/RemoteControl.cs | 2 +- .../Services/RemoteControllerHandler.cs | 38 ++++++++ .../RemoteControllerRegistrationService.cs | 15 +++- .../Services/RemoteControllerService.cs | 4 +- 11 files changed, 152 insertions(+), 20 deletions(-) diff --git a/Duplicati/Agent/Program.cs b/Duplicati/Agent/Program.cs index fdc415620..08b41f513 100644 --- a/Duplicati/Agent/Program.cs +++ b/Duplicati/Agent/Program.cs @@ -319,6 +319,8 @@ public static class Program settings.JWT, settings.CertificateUrl, settings.ServerCertificates, + refreshSettingsBy: null, + forceConnect: true, cts.Token, OnConnect, m => ReKey(applicationSettings, m, agentConfig), @@ -450,6 +452,10 @@ public static class Program }; settings.Save(); + + // If settings are present in the claim data, apply them via the control handler + if (claimedClientData.Settings != null && claimedClientData.Settings.Count > 0) + await OnControl(KeepRemoteConnection.ControlMessage.CreateSettingsControlMessage(claimedClientData.Settings)); } } diff --git a/Duplicati/Library/RemoteControl/ExchangeDataTypes.cs b/Duplicati/Library/RemoteControl/ExchangeDataTypes.cs index 919b28b4e..541dba7c1 100644 --- a/Duplicati/Library/RemoteControl/ExchangeDataTypes.cs +++ b/Duplicati/Library/RemoteControl/ExchangeDataTypes.cs @@ -124,6 +124,10 @@ public sealed record ControlRequestMessage(string Command, Dictionary public const string SettingsVersionKey = "settingsversion"; + /// + /// The key that contains the refresh settings by key + /// + public const string RefreshSettingsByKey = "refreshsettingsby"; } /// diff --git a/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs b/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs index 247dae380..97b2f8eb1 100644 --- a/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs +++ b/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs @@ -174,6 +174,10 @@ public class KeepRemoteConnection : IDisposable /// The last time a message was received /// private DateTimeOffset _lastMessageReceived = DateTimeOffset.MinValue; + /// + /// The time to refresh the settings by, or null if keeping a persistent connection + /// + private DateTimeOffset _refreshSettingsBy = DateTimeOffset.MinValue; /// /// Creates a new connection to the remote server @@ -182,6 +186,8 @@ public class KeepRemoteConnection : IDisposable /// The JWT token to use /// The certificate url to use /// The server keys to use + /// The time to refresh the settings by, or null if keeping a persistent connection + /// If the connection should be force enabled, ignoring re-connect delays /// The token to cancel the connection /// The callback to call when connecting /// The callback to call when rekeying @@ -192,6 +198,8 @@ public class KeepRemoteConnection : IDisposable string JWT, string certificateUrl, IEnumerable serverKeys, + DateTimeOffset? refreshSettingsBy, + bool forceConnect, CancellationToken cancellationToken, Func, Task>> onConnect, Func onReKey, @@ -207,15 +215,17 @@ public class KeepRemoteConnection : IDisposable _onReKey = onReKey; _onControl = onControl; _onMessage = onMessage; + _refreshSettingsBy = refreshSettingsBy ?? DateTimeOffset.MinValue; _client = new Websocket.Client.WebsocketClient(new Uri(serverUrl)); - _runnerTask = RunMainLoop(); + _runnerTask = RunMainLoop(forceConnect); } /// /// Runs the inner loop of the connection /// - private async Task RunMainLoop() + /// If the connection should be force enabled, ignoring re-connect delays + private async Task RunMainLoop(bool forceConnect) { _client.ReconnectTimeout = NoResponseTimeout; _client.LostReconnectTimeout = ReconnectInterval; @@ -236,8 +246,8 @@ public class KeepRemoteConnection : IDisposable TimeSpan.FromSeconds(1), _ => { - // Reconnect if we have disconnected - if (!_client.IsRunning) + // Reconnect if we have disconnected (but not if auto-reconnect is disabled) + if (!_client.IsRunning && !IsAutoReconnectDisabled()) { reconnectHelper.Signal(); } @@ -274,8 +284,15 @@ public class KeepRemoteConnection : IDisposable _state = ConnectionState.NotConnected; _serverCertificate = null; _serverPublicKey = null; - reconnectHelper.Signal(); - Log.WriteMessage(LogMessageType.Warning, LogTag, "WebsocketDisconnect", "Disconnected from the server"); + if (!IsAutoReconnectDisabled()) + { + reconnectHelper.Signal(); + Log.WriteMessage(LogMessageType.Warning, LogTag, "WebsocketDisconnect", "Disconnected from the server"); + } + else + { + Log.WriteMessage(LogMessageType.Information, LogTag, "WebsocketDisconnect", "Disconnected from the server. Auto-reconnect disabled until {0}", _refreshSettingsBy.ToLocalTime()); + } }); _client.MessageReceived.Subscribe(async msg => @@ -402,7 +419,12 @@ public class KeepRemoteConnection : IDisposable case MessageType.Control: await _onControl(new ControlMessage( envelope.GetPayload(), - response => SendEnvelope(envelope.RespondWith(response)) + response => SendEnvelope(envelope.RespondWith(response)), + refreshSettingsBy => + { + _refreshSettingsBy = DateTimeOffset.FromUnixTimeMilliseconds(Math.Max(refreshSettingsBy.ToUnixTimeMilliseconds(), DateTimeOffset.UtcNow.AddSeconds(30).ToUnixTimeMilliseconds())); + _client.Stop(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Disconnect requested"); + } )); break; @@ -428,7 +450,8 @@ public class KeepRemoteConnection : IDisposable }); // Start the connection - reconnectHelper.Signal(); + if (forceConnect || !IsAutoReconnectDisabled()) + reconnectHelper.Signal(); var t = await Task.WhenAny( heartbeatHelper.RunLoopAsync(), @@ -456,6 +479,8 @@ public class KeepRemoteConnection : IDisposable /// The JWT to use /// The certificate url to use /// The server keys to use + /// The time to refresh settings by + /// If the connection should be force enabled, ignoring re-connect delays /// The token to cancel the connection /// The callback to call when connecting /// The callback to call when rekeying @@ -467,6 +492,8 @@ public class KeepRemoteConnection : IDisposable string JWT, string certificateUrl, IEnumerable serverKeys, + DateTimeOffset? refreshSettingsBy, + bool forceConnect, CancellationToken cancellationToken, Func, Task>> onConnect, Func onReKey, @@ -474,7 +501,7 @@ public class KeepRemoteConnection : IDisposable Func onMessage) => Task.Run(async () => { - using var connection = new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onConnect, onReKey, onControl, onMessage); + using var connection = new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, refreshSettingsBy, forceConnect, cancellationToken, onConnect, onReKey, onControl, onMessage); await connection._runnerTask; }); @@ -537,6 +564,13 @@ public class KeepRemoteConnection : IDisposable /// public ConnectionState State => _state; + /// + /// Checks if automatic reconnection should be disabled based on the RefreshSettingsBy timestamp. + /// + /// True if automatic reconnect is disabled; otherwise, false. + public bool IsAutoReconnectDisabled() + => DateTimeOffset.UtcNow < _refreshSettingsBy; + /// /// Creates a new connection to the remote server /// @@ -544,6 +578,9 @@ public class KeepRemoteConnection : IDisposable /// The JWT token to use /// The certificate url to use /// The server keys to use + /// The timestamp to disable automatic reconnect + /// If the connection should be force enabled, ignoring re-connect delays + /// The cancellation token to use /// The callback to call when connecting /// The callback to call when rekeying /// The callback to call when a control message is received @@ -554,12 +591,14 @@ public class KeepRemoteConnection : IDisposable string JWT, string certificateUrl, IEnumerable serverKeys, + DateTimeOffset? refreshSettingsBy, + bool forceConnect, CancellationToken cancellationToken, Func, Task>> onConnect, Func onReKey, Func onControl, Func onMessage) - => new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onConnect, onReKey, onControl, onMessage); + => new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, refreshSettingsBy, forceConnect, cancellationToken, onConnect, onReKey, onControl, onMessage); /// /// Requests a certificate refresh @@ -678,6 +717,10 @@ public class KeepRemoteConnection : IDisposable /// private readonly Func _respondCommand; /// + /// The callback method to request disconnect after responding + /// + private readonly Action? _requestDisconnect; + /// /// The command request message /// public ControlRequestMessage ControlRequestMessage { get; } @@ -687,10 +730,12 @@ public class KeepRemoteConnection : IDisposable /// /// The command request message /// The callback method that will receive the response - public ControlMessage(ControlRequestMessage controlRequestMessage, Func respondCommand) + /// Optional callback to request disconnect after responding + public ControlMessage(ControlRequestMessage controlRequestMessage, Func respondCommand, Action? requestDisconnect) { ControlRequestMessage = controlRequestMessage; _respondCommand = respondCommand; + _requestDisconnect = requestDisconnect; } /// @@ -700,5 +745,26 @@ public class KeepRemoteConnection : IDisposable /// True if the response was sent public bool Respond(ControlResponseMessage response) => _respondCommand(response); + + /// + /// Requests the connection to be closed after responding. + /// This is used when the server indicates the client should disconnect + /// + /// The time to refresh settings by + public void RequestDisconnect(DateTimeOffset refreshSettingsBy) + => _requestDisconnect?.Invoke(refreshSettingsBy); + + /// + /// Creates a control message for initial settings from ClaimedClientData. + /// This is used to apply settings that are returned when the machine is claimed. + /// + /// The settings dictionary from ClaimedClientData.Settings + /// A control message with the settings as parameters + public static ControlMessage CreateSettingsControlMessage(Dictionary settings) + => new( + new ControlRequestMessage(ControlRequestMessage.UpdateSettingsCommand, settings), + _ => true, // No-op response handler since there's no websocket connection for this + _ => { } // No-op disconnect handler since there's no websocket connection for this + ); } } diff --git a/Duplicati/Library/RemoteControl/RegisterForRemote.cs b/Duplicati/Library/RemoteControl/RegisterForRemote.cs index 3278b1a66..9d17fcf26 100644 --- a/Duplicati/Library/RemoteControl/RegisterForRemote.cs +++ b/Duplicati/Library/RemoteControl/RegisterForRemote.cs @@ -133,6 +133,7 @@ public class RegisterForRemote : IDisposable /// The URL for getting new server certificates /// The certificates for the remote server /// The encryption key for the local settings + /// The settings for the machine private sealed record EnvelopedClaimedClientData( bool Success, string StatusMessage, @@ -140,7 +141,8 @@ public class RegisterForRemote : IDisposable string ServerUrl, string CertificateUrl, IEnumerable ServerCertificates, - string? LocalEncryptionKey + string? LocalEncryptionKey, + Dictionary? Settings ); /// @@ -338,7 +340,7 @@ public class RegisterForRemote : IDisposable if (!result.Success) throw new Exception($"Failed to claim machine: {result.StatusMessage}"); - return new ClaimedClientData(result.JWT, result.ServerUrl, result.CertificateUrl, result.ServerCertificates, result.LocalEncryptionKey); + return new ClaimedClientData(result.JWT, result.ServerUrl, result.CertificateUrl, result.ServerCertificates, result.LocalEncryptionKey, result.Settings); } /// diff --git a/Duplicati/Library/RemoteControl/SharedTypes.cs b/Duplicati/Library/RemoteControl/SharedTypes.cs index 9de5a9f6f..643c8b334 100644 --- a/Duplicati/Library/RemoteControl/SharedTypes.cs +++ b/Duplicati/Library/RemoteControl/SharedTypes.cs @@ -47,12 +47,14 @@ public sealed record RegisterClientData( /// The URL for getting new server certificates /// The certificates for the remote server /// The encryption key for the local settings +/// The settings for the machine, same as in ControlRequestMessage.Parameters public sealed record ClaimedClientData( string JWT, string ServerUrl, string CertificateUrl, IEnumerable ServerCertificates, - string? LocalEncryptionKey + string? LocalEncryptionKey, + Dictionary? Settings = null ); /// diff --git a/Duplicati/WebserverCore/Abstractions/IRemoteController.cs b/Duplicati/WebserverCore/Abstractions/IRemoteController.cs index c0ef9f86e..ac28c7098 100644 --- a/Duplicati/WebserverCore/Abstractions/IRemoteController.cs +++ b/Duplicati/WebserverCore/Abstractions/IRemoteController.cs @@ -41,7 +41,8 @@ public interface IRemoteController /// /// Enable the remote controller /// - public void Enable(); + /// If the connection should be force enabled, ignoring re-connect delays + public void Enable(bool forceConnect); /// /// Disable the remote controller diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs index 89a49eb83..4ad90829d 100644 --- a/Duplicati/WebserverCore/DuplicatiWebserver.cs +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -398,7 +398,7 @@ public class DuplicatiWebserver }); if (connection.ApplicationSettings.RemoteControlEnabled) - app.Services.GetRequiredService().Enable(); + app.Services.GetRequiredService().Enable(false); // Preload static system info, for better first-load experience _ = Task.Run(() => app.Services.GetRequiredService().GetSystemInfo(null)); diff --git a/Duplicati/WebserverCore/Endpoints/V1/RemoteControl.cs b/Duplicati/WebserverCore/Endpoints/V1/RemoteControl.cs index 045b2fd91..de11adf49 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/RemoteControl.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/RemoteControl.cs @@ -106,7 +106,7 @@ public class RemoteControl : IEndpointV1 private static Dto.RemoteControlStatusOutput EnableRemoteControl(IRemoteControllerRegistration registration, IRemoteController remoteController) { - remoteController.Enable(); + remoteController.Enable(true); return GetStatus(registration, remoteController); } diff --git a/Duplicati/WebserverCore/Services/RemoteControllerHandler.cs b/Duplicati/WebserverCore/Services/RemoteControllerHandler.cs index 450676527..cc0800a5a 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerHandler.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerHandler.cs @@ -81,6 +81,7 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h await Task.CompletedTask; Dictionary? result = null; + var refreshSettingsBy = DateTimeOffset.MinValue; Log.WriteMessage(LogMessageType.Verbose, LOGTAG, "OnControl", "Received control message: {0}", message); try @@ -141,6 +142,36 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h connection.AddOrUpdateBackupAndSchedule(importData.Backup, importData.Schedule); } + + try + { + // Check if we need to delay the next reconnection + var refreshSettingsByValue = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.RefreshSettingsByKey); + if (!string.IsNullOrWhiteSpace(refreshSettingsByValue) && long.TryParse(refreshSettingsByValue, out var refreshSettingsByLong) && refreshSettingsByLong > 0) + { + var target = DateTimeOffset.FromUnixTimeSeconds(refreshSettingsByLong); + if (target > DateTimeOffset.UtcNow) + refreshSettingsBy = target; + + // Agent keeps its own config, and does not support connection delays + if (applicationSettings.Origin != "Agent") + { + var connstr = connection.ApplicationSettings.RemoteControlConfig; + if (!string.IsNullOrWhiteSpace(connstr)) + { + var prevCfg = JsonConvert.DeserializeObject(connstr); + if (prevCfg != null) + connection.ApplicationSettings.RemoteControlConfig = JsonConvert.SerializeObject( + prevCfg with { RefreshSettingsBy = target } + ); + } + } + } + } + catch (Exception ex) + { + Log.WriteMessage(LogMessageType.Error, LOGTAG, "OnControl", ex, "Failed to update refreshSettingsBy"); + } } } catch (Exception ex) @@ -150,7 +181,14 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h return; } + // Respond first, then disconnect if needed message.Respond(new ControlResponseMessage(true, result, null)); + + if (refreshSettingsBy > DateTimeOffset.UtcNow) + { + Log.WriteMessage(LogMessageType.Information, LOGTAG, "OnControl", "Requesting disconnect after responding to control message"); + message.RequestDisconnect(refreshSettingsBy); + } } diff --git a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs index ca660188c..1e48e9879 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs @@ -48,6 +48,10 @@ public sealed record RemoteControlConfig /// The trusted server certificates. /// public required IEnumerable ServerCertificates { get; init; } + /// + /// The time when the settings should be refreshed. + /// + public DateTimeOffset? RefreshSettingsBy { get; init; } } /// @@ -55,7 +59,9 @@ public sealed record RemoteControlConfig /// /// The database connection /// The HTTP client factory -public class RemoteControllerRegistrationService(Connection connection, IHttpClientFactory httpClientFactory, IRemoteController remoteController) : IRemoteControllerRegistration +/// The remote controller +/// The controller handler +public class RemoteControllerRegistrationService(Connection connection, IHttpClientFactory httpClientFactory, IRemoteController remoteController, IRemoteControllerHandler controllerHandler) : IRemoteControllerRegistration { /// /// The registration process controller this service is wrapping. @@ -117,9 +123,13 @@ public class RemoteControllerRegistrationService(Connection connection, IHttpCli ServerUrl = claimData.ServerUrl }); + // If settings are present in the claim data, apply them via the control handler + if (claimData.Settings != null && claimData.Settings.Count > 0) + await controllerHandler.OnControl(KeepRemoteConnection.ControlMessage.CreateSettingsControlMessage(claimData.Settings)); + // Automatically connect after we are registered if (remoteController.CanEnable) - remoteController.Enable(); + remoteController.Enable(false); }); } @@ -141,5 +151,6 @@ public class RemoteControllerRegistrationService(Connection connection, IHttpCli _operationCancellation?.Dispose(); _registerForRemote = null; _operationCancellation = null; + RegistrationUrl = null; } } diff --git a/Duplicati/WebserverCore/Services/RemoteControllerService.cs b/Duplicati/WebserverCore/Services/RemoteControllerService.cs index b0e9516ac..94c941fb7 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerService.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerService.cs @@ -63,7 +63,7 @@ public class RemoteControllerService(Connection connection, IRemoteControllerHan private KeepRemoteConnection? _keepRemoteConnection; /// - public void Enable() + public void Enable(bool forceConnect) { if (_keepRemoteConnection != null) return; @@ -79,6 +79,8 @@ public class RemoteControllerService(Connection connection, IRemoteControllerHan config.Token, config.CertificateUrl, config.ServerCertificates, + config.RefreshSettingsBy, + forceConnect, CancellationToken.None, controllerHandler.OnConnect, controllerHandler.ReKey, From 5d4a15aad01bf03da401d5f2bb65f4ad4f014176 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 21 Apr 2026 14:50:30 +0200 Subject: [PATCH 3/4] 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(); } } } From 41c11735d5f3aea18c3939db75ec11bb2fbdaff7 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 21 Apr 2026 14:52:40 +0200 Subject: [PATCH 4/4] Nump MailKit + MimeKit This PR updates MimeKit and MailKit to version 4.16.0. This closes #6846 --- .../Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj | 4 ++-- proprietary/Office365/Duplicati.Proprietary.Office365.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj b/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj index 85047a1fc..869701334 100644 --- a/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj +++ b/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/proprietary/Office365/Duplicati.Proprietary.Office365.csproj b/proprietary/Office365/Duplicati.Proprietary.Office365.csproj index 94d125c72..8a43f494b 100644 --- a/proprietary/Office365/Duplicati.Proprietary.Office365.csproj +++ b/proprietary/Office365/Duplicati.Proprietary.Office365.csproj @@ -12,7 +12,7 @@ - +