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
{