From 5d37c8587efea927770c4dd168a4df5ffa03a29f Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Thu, 8 Jan 2026 18:38:52 +0100 Subject: [PATCH] Added schedules to the RemoteSynchronizationModule --- .../Builtin/RemoteSynchronizationModule.cs | 109 ++++++++++++++++++ Duplicati/Library/Modules/Builtin/Strings.cs | 6 + 2 files changed, 115 insertions(+) diff --git a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs index e46ab4603..0fab18bfb 100644 --- a/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs +++ b/Duplicati/Library/Modules/Builtin/RemoteSynchronizationModule.cs @@ -21,14 +21,35 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Text.RegularExpressions; using Duplicati.Library.Interface; +using Duplicati.Library.SQLiteHelper; using Duplicati.Library.Utility; using RemoteSynchronization; namespace Duplicati.Library.Modules.Builtin; +/// +/// Trigger modes for remote synchronization. +/// +public enum RemoteSyncTriggerMode +{ + /// + /// Trigger after every successful backup. + /// + Inline, + /// + /// Trigger based on a schedule. + /// + Scheduled, + /// + /// Trigger after a certain number of backups. + /// + Counting +} + /// /// Module for synchronizing backup data to a remote destination after a successful backup operation. /// @@ -46,12 +67,18 @@ public class RemoteSynchronizationModule : IGenericCallbackModule private const string OPTION_RETENTION = "remote-sync-retention"; private const string OPTION_BACKEND_RETRIES = "remote-sync-backend-retries"; private const string OPTION_RETRY = "remote-sync-retry"; + private const string OPTION_MODE = "remote-sync-mode"; + private const string OPTION_SCHEDULE = "remote-sync-schedule"; + private const string OPTION_COUNT = "remote-sync-count"; private IReadOnlyDictionary m_options = new Dictionary(); private string m_source; private string m_destination; private string m_operationName; private bool m_enabled; + private RemoteSyncTriggerMode m_mode = RemoteSyncTriggerMode.Inline; + private TimeSpan m_schedule = TimeSpan.Zero; + private int m_count = 0; /// /// Gets the key identifier for this module. @@ -80,6 +107,9 @@ public class RemoteSynchronizationModule : IGenericCallbackModule new CommandLineArgument(OPTION_RETENTION, CommandLineArgument.ArgumentType.Boolean, Strings.RemoteSynchronization.RetentionShort, Strings.RemoteSynchronization.RetentionLong, "false"), new CommandLineArgument(OPTION_BACKEND_RETRIES, CommandLineArgument.ArgumentType.Integer, Strings.RemoteSynchronization.BackendRetriesShort, Strings.RemoteSynchronization.BackendRetriesLong, "3"), new CommandLineArgument(OPTION_RETRY, CommandLineArgument.ArgumentType.Integer, Strings.RemoteSynchronization.RetryShort, Strings.RemoteSynchronization.RetryLong, "3"), + new CommandLineArgument(OPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.RemoteSynchronization.ModeShort, Strings.RemoteSynchronization.ModeLong, "inline", null, ["inline", "scheduled", "counting"]), + new CommandLineArgument(OPTION_SCHEDULE, CommandLineArgument.ArgumentType.Timespan, Strings.RemoteSynchronization.ScheduleShort, Strings.RemoteSynchronization.ScheduleLong), + new CommandLineArgument(OPTION_COUNT, CommandLineArgument.ArgumentType.Integer, Strings.RemoteSynchronization.CountShort, Strings.RemoteSynchronization.CountLong), ]; /// @@ -91,6 +121,21 @@ public class RemoteSynchronizationModule : IGenericCallbackModule m_options = commandlineOptions.AsReadOnly(); commandlineOptions.TryGetValue(OPTION_BACKEND_DST, out m_destination); m_enabled = !string.IsNullOrWhiteSpace(m_destination); + + if (commandlineOptions.TryGetValue(OPTION_MODE, out var modeStr) && !string.IsNullOrWhiteSpace(modeStr)) + { + if (Enum.TryParse(modeStr, true, out var mode)) + m_mode = mode; + } + + if (commandlineOptions.TryGetValue(OPTION_SCHEDULE, out var scheduleStr) && !string.IsNullOrWhiteSpace(scheduleStr)) + { + if (TimeSpan.TryParse(scheduleStr, out var schedule)) + m_schedule = schedule; + } + + if (commandlineOptions.TryGetValue(OPTION_COUNT, out var countStr) && int.TryParse(countStr, out var count)) + m_count = count; } /// @@ -141,6 +186,14 @@ public class RemoteSynchronizationModule : IGenericCallbackModule return; } + if (!ShouldTriggerSync()) + { + Logging.Log.WriteInformationMessage(LOGTAG, "RemoteSyncSkipped", "Remote synchronization skipped due to trigger mode conditions not met."); + return; + } + + RecordSyncOperation(); + var args = BuildArguments(); try { @@ -154,6 +207,62 @@ public class RemoteSynchronizationModule : IGenericCallbackModule } } + /// + /// Checks if remote synchronization should be triggered based on the configured mode. + /// + /// True if synchronization should be triggered. + private bool ShouldTriggerSync() + { + if (!m_options.TryGetValue("dbpath", out var dbpath) || string.IsNullOrWhiteSpace(dbpath)) + return false; + + using var db = SQLiteLoader.LoadConnection(dbpath); + using var cmd = db.CreateCommand(); + + switch (m_mode) + { + case RemoteSyncTriggerMode.Inline: + return true; + case RemoteSyncTriggerMode.Scheduled: + // Find last remote sync operation + cmd.CommandText = "SELECT Timestamp FROM Operation WHERE Description = 'Remote Synchronization' ORDER BY Timestamp DESC LIMIT 1"; + var lastSync = cmd.ExecuteScalar(); + if (lastSync == null) + return true; // No previous sync, trigger + var lastSyncTime = DateTime.FromFileTimeUtc((long)lastSync); + var now = DateTime.UtcNow; + return (now - lastSyncTime) >= m_schedule; + case RemoteSyncTriggerMode.Counting: + // Count backup operations since last sync + cmd.CommandText = @" + SELECT COUNT(*) FROM Operation + WHERE Description = 'Backup' + AND Timestamp > COALESCE( + (SELECT Timestamp FROM Operation WHERE Description = 'Remote Synchronization' ORDER BY Timestamp DESC LIMIT 1), + 0 + )"; + var count = (long)cmd.ExecuteScalar(); + return count >= m_count; + default: + return false; + } + } + + /// + /// Records a remote synchronization operation in the database. + /// + private void RecordSyncOperation() + { + if (!m_options.TryGetValue("dbpath", out var dbpath) || string.IsNullOrWhiteSpace(dbpath)) + return; + + using var db = SQLiteLoader.LoadConnection(dbpath); + using var cmd = db.CreateCommand(); + cmd.CommandText = "INSERT INTO Operation (Description, Timestamp) VALUES ('Remote Synchronization', @timestamp)"; + cmd.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.ToFileTimeUtc()); + cmd.ExecuteNonQuery(); + } + /// /// Builds the arguments for the remote synchronization command. /// diff --git a/Duplicati/Library/Modules/Builtin/Strings.cs b/Duplicati/Library/Modules/Builtin/Strings.cs index 399f5662d..989a67adc 100644 --- a/Duplicati/Library/Modules/Builtin/Strings.cs +++ b/Duplicati/Library/Modules/Builtin/Strings.cs @@ -97,6 +97,12 @@ namespace Duplicati.Library.Modules.Builtin.Strings public static string VerifyContentsLong { get { return LC.L(@"Verify file contents before overwriting destination files."); } } public static string VerifyGetAfterPutShort { get { return LC.L(@"Verify after upload"); } } public static string VerifyGetAfterPutLong { get { return LC.L(@"Verify files after uploading them."); } } + public static string ModeShort { get { return LC.L(@"Remote sync trigger mode"); } } + public static string ModeLong { get { return LC.L(@"Specify the trigger mode for remote synchronization: inline (default, after every backup), scheduled (after schedule has passed), counting (after N backups)."); } } + public static string ScheduleShort { get { return LC.L(@"Remote sync schedule"); } } + public static string ScheduleLong { get { return LC.L(@"Specify the schedule interval for remote synchronization, e.g. '7.00:00:00' for weekly."); } } + public static string CountShort { get { return LC.L(@"Remote sync backup count"); } } + public static string CountLong { get { return LC.L(@"Specify the number of backups after which to trigger remote synchronization."); } } } internal static class RunScript {