Added schedules to the RemoteSynchronizationModule

This commit is contained in:
Carl Johnsen
2026-01-08 18:38:52 +01:00
parent 8378e65d97
commit 5d37c8587e
2 changed files with 115 additions and 0 deletions
@@ -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;
/// <summary>
/// Trigger modes for remote synchronization.
/// </summary>
public enum RemoteSyncTriggerMode
{
/// <summary>
/// Trigger after every successful backup.
/// </summary>
Inline,
/// <summary>
/// Trigger based on a schedule.
/// </summary>
Scheduled,
/// <summary>
/// Trigger after a certain number of backups.
/// </summary>
Counting
}
/// <summary>
/// Module for synchronizing backup data to a remote destination after a successful backup operation.
/// </summary>
@@ -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<string, string> m_options = new Dictionary<string, string>();
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;
/// <summary>
/// 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),
];
/// <summary>
@@ -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<RemoteSyncTriggerMode>(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;
}
/// <summary>
@@ -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
}
}
/// <summary>
/// Checks if remote synchronization should be triggered based on the configured mode.
/// </summary>
/// <returns>True if synchronization should be triggered.</returns>
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;
}
}
/// <summary>
/// Records a remote synchronization operation in the database.
/// </summary>
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();
}
/// <summary>
/// Builds the arguments for the remote synchronization command.
/// </summary>
@@ -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
{