diff --git a/Duplicati/Library/RestAPI/FIXMEGlobal.cs b/Duplicati/Library/RestAPI/FIXMEGlobal.cs index 5a977f0c2..6b2491e70 100644 --- a/Duplicati/Library/RestAPI/FIXMEGlobal.cs +++ b/Duplicati/Library/RestAPI/FIXMEGlobal.cs @@ -48,12 +48,7 @@ namespace Duplicati.Library.RestAPI /// /// A delegate method for creating a copy of the current progress state /// - public static Func GenerateProgressState; - - /// - /// The status event signaler, used to control long polling of status updates - /// - public static EventPollNotify StatusEventNotifyer => Provider.GetRequiredService(); + public static Func GenerateProgressState; /// /// For keeping and incrementing last last events Ids of db save and last notification @@ -71,28 +66,16 @@ namespace Duplicati.Library.RestAPI /// public static string DataFolder; - /// - /// This is the scheduling thread - /// - public static ISchedulerService Scheduler => Provider.GetRequiredService(); - /// /// The log redirect handler /// public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); - /// - /// The update poll thread. - /// - public static UpdatePollThread UpdatePoller => Provider.GetRequiredService(); - - /// /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server) /// public static string Origin = "Server"; - /// /// The application exit event /// diff --git a/Duplicati/Library/RestAPI/Runner.cs b/Duplicati/Library/RestAPI/Runner.cs index 3480d1795..841454dcb 100644 --- a/Duplicati/Library/RestAPI/Runner.cs +++ b/Duplicati/Library/RestAPI/Runner.cs @@ -29,6 +29,8 @@ using Duplicati.Server.Serialization; using Duplicati.Library.RestAPI; using Duplicati.Library.Utility; using System.Threading.Tasks; +using Duplicati.Server.Database; +using Duplicati.Server.Serialization.Interface; namespace Duplicati.Server { @@ -52,6 +54,12 @@ namespace Duplicati.Server public Func? OnStarting { get; set; } public Func? OnFinished { get; set; } + /// + /// Callback to be executed when the task is finished. + /// Only supported for the delete operation for now. + /// + internal Action? AfterTaskFinished { get; set; } + public DuplicatiOperation Operation { get; internal set; } public Serialization.Interface.IBackup? Backup { get; internal set; } public IDictionary? ExtraOptions { get; internal set; } @@ -141,9 +149,6 @@ namespace Duplicati.Server { m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID); } - - public Task Execute() - => Task.Run(() => Runner.Run(this, true)); } private class CustomRunnerTask : RunnerData @@ -166,7 +171,7 @@ namespace Duplicati.Server return new CustomRunnerTask(runner); } - public static IRunnerData CreateTask(DuplicatiOperation operation, Serialization.Interface.IBackup backup, IDictionary? extraOptions = null, string[]? filterStrings = null, string[]? extraArguments = null, int pageSize = 0, int pageOffset = 0) + public static IRunnerData CreateTask(DuplicatiOperation operation, IBackup backup, IDictionary? extraOptions = null, string[]? filterStrings = null, string[]? extraArguments = null, int pageSize = 0, int pageOffset = 0) { return new RunnerData() { @@ -180,7 +185,18 @@ namespace Duplicati.Server }; } - public static IRunnerData CreateListTask(Serialization.Interface.IBackup backup, string[]? filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) + public static IRunnerData CreateDeleteTask(IBackup backup, IDictionary extraOptions, Action? afterTaskFinished = null) + { + return new RunnerData() + { + Operation = DuplicatiOperation.Delete, + Backup = backup, + ExtraOptions = extraOptions, + AfterTaskFinished = afterTaskFinished + }; + } + + public static IRunnerData CreateListTask(IBackup backup, string[]? filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) { var dict = new Dictionary(); if (onlyPrefix) @@ -199,7 +215,7 @@ namespace Duplicati.Server filters); } - public static IRunnerData CreateListFilesetsTask(Serialization.Interface.IBackup backup, Dictionary? extraOptions = null) + public static IRunnerData CreateListFilesetsTask(IBackup backup, Dictionary? extraOptions = null) { return CreateTask( DuplicatiOperation.ListFilesets, @@ -207,7 +223,7 @@ namespace Duplicati.Server extraOptions ?? new Dictionary()); } - public static IRunnerData CreateListFolderContents(Serialization.Interface.IBackup backup, string[] folders, DateTime time, int pageSize, int pageOffset) + public static IRunnerData CreateListFolderContents(IBackup backup, string[]? folders, DateTime time, int pageSize, int pageOffset) { var dict = new Dictionary(); if (time.Ticks > 0) @@ -222,7 +238,7 @@ namespace Duplicati.Server pageOffset: pageOffset); } - public static IRunnerData ListFileVersionsTask(Serialization.Interface.IBackup backup, string[] filepaths, int pageSize, int pageOffset) + public static IRunnerData ListFileVersionsTask(IBackup backup, string[]? filepaths, int pageSize, int pageOffset) { var dict = new Dictionary(); return CreateTask( @@ -234,7 +250,7 @@ namespace Duplicati.Server pageOffset: pageOffset); } - public static IRunnerData CreateSearchEntriesTask(Serialization.Interface.IBackup backup, string[] filters, string[] folders, DateTime time, int pageSize, int pageOffset) + public static IRunnerData CreateSearchEntriesTask(IBackup backup, string[]? filters, string[]? folders, DateTime time, int pageSize, int pageOffset) { var dict = new Dictionary(); if (time.Ticks > 0) @@ -251,7 +267,7 @@ namespace Duplicati.Server } - public static IRunnerData CreateRestoreTask(Serialization.Interface.IBackup backup, string[]? filters, + public static IRunnerData CreateRestoreTask(IBackup backup, string[]? filters, DateTime time, string? restoreTarget, bool overwrite, bool restore_permissions, bool skip_metadata, string? passphrase) { @@ -276,7 +292,7 @@ namespace Duplicati.Server } private class MessageSink : Library.Main.IMessageSink { - private class ProgressState : Serialization.Interface.IProgressEventData + private class ProgressState : IProgressEventData { private readonly string? m_backupID; private readonly long m_taskID; @@ -394,18 +410,18 @@ namespace Duplicati.Server #endregion } - public static string GetCommandLine(IRunnerData data) + public static string GetCommandLine(Connection databaseConnection, IRunnerData data) { var backup = data.Backup; if (backup == null) throw new ArgumentNullException(nameof(backup)); - var options = ApplyOptions(backup, GetCommonOptions()); + var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection)); if (data.ExtraOptions != null) foreach (var k in data.ExtraOptions) options[k.Key] = k.Value; - var cf = FIXMEGlobal.DataConnection.Filters; + var cf = databaseConnection.Filters; var bf = backup.Filters; var sources = @@ -440,18 +456,18 @@ namespace Duplicati.Server return cmd.ToString(); } - public static string[] GetCommandLineParts(IRunnerData data) + public static string[] GetCommandLineParts(Connection databaseConnection, IRunnerData data) { var backup = data.Backup; if (backup == null) throw new ArgumentNullException(nameof(backup)); - var options = ApplyOptions(backup, GetCommonOptions()); + var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection)); if (data.ExtraOptions != null) foreach (var k in data.ExtraOptions) options[k.Key] = k.Value; - var cf = FIXMEGlobal.DataConnection.Filters; + var cf = databaseConnection.Filters; var bf = backup.Filters; var sources = @@ -480,7 +496,15 @@ namespace Duplicati.Server return parts.ToArray(); } - public static IBasicResults? Run(IRunnerData data, bool fromQueue) + public static IBasicResults? Run(Connection databaseConnection, EventPollNotify eventPollNotify, IQueuedTask data, bool fromQueue) + { + if (data is IRunnerData runnerData) + return Run(databaseConnection, eventPollNotify, runnerData, fromQueue); + + throw new ArgumentException("Invalid task type", nameof(data)); + } + + private static IBasicResults? Run(Connection databaseConnection, EventPollNotify eventPollNotify, IRunnerData data, bool fromQueue) { data.TaskStarted = DateTime.Now; if (data is CustomRunnerTask task) @@ -489,13 +513,13 @@ namespace Duplicati.Server { var sink = new MessageSink(task.TaskID, null); FIXMEGlobal.GenerateProgressState = sink.Copy; - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + eventPollNotify.SignalNewEvent(); task.Run(sink); } catch (Exception ex) { - FIXMEGlobal.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex); + databaseConnection.LogError(string.Empty, "Failed while executing custom task", ex); } finally { @@ -518,10 +542,10 @@ namespace Duplicati.Server if (fromQueue) { FIXMEGlobal.GenerateProgressState = () => sink.Copy(); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + eventPollNotify.SignalNewEvent(); } - var options = ApplyOptions(backup, GetCommonOptions()); + var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection)); if (data.ExtraOptions != null) foreach (var k in data.ExtraOptions) options[k.Key] = k.Value; @@ -529,7 +553,7 @@ namespace Duplicati.Server // Pack in the system or task config for easy restore if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config")) { - tempfolder = StoreTaskConfigAndGetTempFolder(data, options); + tempfolder = StoreTaskConfigAndGetTempFolder(databaseConnection, data, options); } // Attach a log scope that tags all messages to relay the TaskID and BackupID @@ -557,7 +581,7 @@ namespace Duplicati.Server catch { } ((RunnerData)data).Controller = controller; - var appSettings = FIXMEGlobal.DataConnection.ApplicationSettings; + var appSettings = databaseConnection.ApplicationSettings; data.UpdateThrottleSpeeds(appSettings.UploadSpeedLimit, appSettings.DownloadSpeedLimit); // Pass on the provider, will be replaced if configured in the backup @@ -573,7 +597,7 @@ namespace Duplicati.Server { case DuplicatiOperation.Backup: { - var filter = ApplyFilter(backup, GetCommonFilter()); + var filter = ApplyFilter(backup, GetCommonFilter(databaseConnection)); var sources = (from n in backup.Sources let p = SpecialFolders.ExpandEnvironmentVariables(n) @@ -581,62 +605,62 @@ namespace Duplicati.Server select p).ToArray(); var r = controller.Backup(sources, filter); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.List: { var r = controller.List(data.FilterStrings, null); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.Repair: { var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings)); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.RepairUpdate: { var r = controller.UpdateDatabaseWithVersions(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.Remove: { var r = controller.Delete(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.Restore: { var r = controller.Restore(data.FilterStrings); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.Verify: { var r = controller.Test(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.Compact: { var r = controller.Compact(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.CreateReport: { - using (var tf = new TempFile()) + using (var tf = new Library.Utility.TempFile()) { var r = controller.CreateLogDatabase(tf); - var tempid = FIXMEGlobal.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); + var tempid = databaseConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); if (string.Equals(tf, r.TargetPath, Utility.ClientFilenameStringComparison)) tf.Protected = true; - FIXMEGlobal.DataConnection.RegisterNotification( + databaseConnection.RegisterNotification( NotificationType.Information, "Bugreport ready", "Bugreport is ready for download", @@ -656,7 +680,7 @@ namespace Duplicati.Server case DuplicatiOperation.ListRemote: { var r = controller.ListRemote(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } @@ -675,21 +699,21 @@ namespace Duplicati.Server System.IO.File.Delete(dbpath); } } - FIXMEGlobal.DataConnection.DeleteBackup(backup); - FIXMEGlobal.Scheduler.Reschedule(); + databaseConnection.DeleteBackup(backup); + (data as RunnerData)?.AfterTaskFinished?.Invoke(data); return null; } case DuplicatiOperation.Vacuum: { var r = controller.Vacuum(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.ListFilesets: { var r = controller.ListFilesets(); - UpdateMetadata(backup, r); + UpdateMetadataBase(databaseConnection, eventPollNotify, backup, r); return r; } case DuplicatiOperation.ListFolderContents: @@ -714,9 +738,9 @@ namespace Duplicati.Server } catch (Exception ex) { - FIXMEGlobal.DataConnection.LogError(data.Backup?.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup?.Name, data.Backup?.ID), ex); + databaseConnection.LogError(data.Backup?.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup?.Name, data.Backup?.ID), ex); if (data.Backup != null) - UpdateMetadataError(data.Backup, ex); + UpdateMetadataError(databaseConnection, data.Backup, ex); Library.UsageReporter.Reporter.Report(ex); if (!fromQueue) @@ -731,7 +755,7 @@ namespace Duplicati.Server } } - private static TempFolder? StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options) + private static TempFolder? StoreTaskConfigAndGetTempFolder(Connection databaseConnection, IRunnerData data, Dictionary options) { if (data.Backup == null) throw new ArgumentNullException(nameof(data.Backup)); @@ -746,13 +770,13 @@ namespace Duplicati.Server { tempfolder = new TempFolder(); var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json"); - using (var tempfile = TempFile.WrapExistingFile(temppath)) + using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath)) { object? taskdata = null; if (all_tasks) - taskdata = FIXMEGlobal.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => FIXMEGlobal.DataConnection.PrepareBackupForExport(FIXMEGlobal.DataConnection.GetBackup(x.ID)!)); + taskdata = databaseConnection.Backups.Where(x => !x.IsTemporary).Select(x => databaseConnection.PrepareBackupForExport(databaseConnection.GetBackup(x.ID)!)); else - taskdata = new[] { FIXMEGlobal.DataConnection.PrepareBackupForExport(data.Backup) }; + taskdata = new[] { databaseConnection.PrepareBackupForExport(data.Backup) }; using (var fs = System.IO.File.OpenWrite(tempfile)) using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8)) @@ -773,20 +797,20 @@ namespace Duplicati.Server return tempfolder; } - private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex) + private static void UpdateMetadataError(Connection databaseConnection, Serialization.Interface.IBackup backup, Exception ex) { - backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow); + backup.Metadata["LastErrorDate"] = Utility.SerializeDateTime(DateTime.UtcNow); backup.Metadata["LastErrorMessage"] = ex.Message; if (!backup.IsTemporary) - FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + databaseConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); string? messageid = null; if (ex is UserInformationException exception) messageid = exception.HelpID; FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); - FIXMEGlobal.DataConnection.RegisterNotification( + databaseConnection.RegisterNotification( NotificationType.Error, backup.IsTemporary ? "Error" : string.Format("Error while running {0}", backup.Name), @@ -804,31 +828,31 @@ namespace Duplicati.Server ); } - private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r) + private static void UpdateMetadataLastCompact(IBackup backup, ICompactResults r) { if (r != null) { backup.Metadata["LastCompactDuration"] = r.Duration.ToString(); - backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + backup.Metadata["LastCompactStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastCompactFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); } } - private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r) + private static void UpdateMetadataLastVacuum(IBackup backup, IVacuumResults r) { if (r != null) { backup.Metadata["LastVacuumDuration"] = r.Duration.ToString(); - backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + backup.Metadata["LastVacuumStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastVacuumFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); } } - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r) + private static void UpdateMetadataStatistics(IBackup backup, IParsedBackendStatistics r) { if (r != null) { - backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); + backup.Metadata["LastBackupDate"] = Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); backup.Metadata["BackupListCount"] = r.BackupListCount.ToString(); backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString(); backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString(); @@ -837,28 +861,28 @@ namespace Duplicati.Server backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString(); backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString(); backup.Metadata["TargetFilesetsCount"] = r.KnownFilesets.ToString(); - backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize); + backup.Metadata["TargetSizeString"] = Utility.FormatSizeString(r.KnownFileSize); } } - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result) + private static void UpdateMetadataBase(Connection databaseConnection, EventPollNotify eventPollNotify, Serialization.Interface.IBackup backup, IBasicResults result) { if (result is IRestoreResults r1) { backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString(); - backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); - backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); + backup.Metadata["LastRestoreStarted"] = Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); + backup.Metadata["LastRestoreFinished"] = Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); } if (result is IParsedBackendStatistics r2 && !result.Interrupted) { - UpdateMetadata(backup, r2); + UpdateMetadataStatistics(backup, r2); } if (result is IBackendStatsticsReporter r3 && !result.Interrupted) { if (r3.BackendStatistics is IParsedBackendStatistics statistics) - UpdateMetadata(backup, statistics); + UpdateMetadataStatistics(backup, statistics); } if (result is ICompactResults r4 && !result.Interrupted) @@ -881,8 +905,8 @@ namespace Duplicati.Server backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString(); backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString(); backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles); - backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + backup.Metadata["LastBackupStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastBackupFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); backup.Metadata["LastBackupDuration"] = r.Duration.ToString(); if (r.CompactResults != null) @@ -912,7 +936,7 @@ namespace Duplicati.Server titleType = "Warning"; } - FIXMEGlobal.DataConnection.RegisterNotification( + databaseConnection.RegisterNotification( r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", message, @@ -958,7 +982,7 @@ namespace Duplicati.Server else if (result.ParsedResult == ParsedResultType.Error && result.Errors.Count() == 1) message = $"Error: {result.Errors.Single()}"; - FIXMEGlobal.DataConnection.RegisterNotification( + databaseConnection.RegisterNotification( type, title, message, @@ -973,10 +997,10 @@ namespace Duplicati.Server } if (!backup.IsTemporary) - FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + databaseConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + eventPollNotify.SignalNewEvent(); } private static bool TestIfOptionApplies() @@ -998,7 +1022,7 @@ namespace Duplicati.Server options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase)); } - internal static Dictionary ApplyOptions(Serialization.Interface.IBackup backup, Dictionary options) + internal static Dictionary ApplyOptions(Connection databaseConnection, Serialization.Interface.IBackup backup, Dictionary options) { options["backup-name"] = backup.Name; options["dbpath"] = backup.DBPath; @@ -1018,7 +1042,7 @@ namespace Duplicati.Server DisableModule("console-password-input", options); // Patch in additional report urls - var additionalReportUrl = FIXMEGlobal.DataConnection.ApplicationSettings.AdditionalReportUrl; + var additionalReportUrl = databaseConnection.ApplicationSettings.AdditionalReportUrl; if (!string.IsNullOrWhiteSpace(additionalReportUrl)) { options["send-http-json-urls"] = string.Join(";", @@ -1032,7 +1056,7 @@ namespace Duplicati.Server return options; } - private static IFilter? ApplyFilter(Serialization.Interface.IBackup backup, IFilter? filter) + private static Library.Utility.IFilter? ApplyFilter(IBackup backup, Library.Utility.IFilter? filter) { var f2 = backup.Filters; if (f2 != null && f2.Length > 0) @@ -1044,7 +1068,7 @@ namespace Duplicati.Server ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression) : SpecialFolders.ExpandEnvironmentVariables(n.Expression) orderby n.Order - select (IFilter)new FilterExpression(exp, n.Include)) + select (Library.Utility.IFilter)new FilterExpression(exp, n.Include)) .Aggregate((a, b) => FilterExpression.Combine(a, b)); filter = FilterExpression.Combine(filter, nf); @@ -1053,17 +1077,17 @@ namespace Duplicati.Server return filter; } - public static Dictionary GetCommonOptions() + public static Dictionary GetCommonOptions(Connection databaseConnection) { return - (from n in FIXMEGlobal.DataConnection.Settings + (from n in databaseConnection.Settings where TestIfOptionApplies() select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => (string?)k.Value); } - private static IFilter? GetCommonFilter() + private static Library.Utility.IFilter? GetCommonFilter(Connection databaseConnection) { - var filters = FIXMEGlobal.DataConnection.Filters; + var filters = databaseConnection.Filters; if (filters == null || filters.Length == 0) return null; @@ -1071,7 +1095,7 @@ namespace Duplicati.Server (from n in filters orderby n.Order let exp = Environment.ExpandEnvironmentVariables(n.Expression) - select (IFilter)new FilterExpression(exp, n.Include)) + select (Library.Utility.IFilter)new FilterExpression(exp, n.Include)) .Aggregate((a, b) => FilterExpression.Combine(a, b)); } } diff --git a/Duplicati/Library/RestAPI/Scheduler.cs b/Duplicati/Library/RestAPI/Scheduler.cs index 388a850b7..bc1f175cc 100644 --- a/Duplicati/Library/RestAPI/Scheduler.cs +++ b/Duplicati/Library/RestAPI/Scheduler.cs @@ -29,9 +29,9 @@ using System.Text; using System.Linq; using System.Threading; using Duplicati.Library.Utility; -using Duplicati.Library.RestAPI; using System.Threading.Tasks; using Duplicati.WebserverCore.Abstractions; +using Duplicati.Server.Database; // TODO: Rewrite this class. // It should just signal what new backups to run, and not mix with the worker thread. @@ -69,6 +69,10 @@ namespace Duplicati.Server /// The queue runner service /// private readonly IQueueRunnerService m_queueRunnerService; + /// + /// The data connection + /// + private readonly Connection m_dataConnection; /// /// The currently scheduled items @@ -83,8 +87,11 @@ namespace Duplicati.Server /// /// Constructs a new scheduler /// - public Scheduler(IQueueRunnerService queueRunnerService) + /// The data connection + /// The queue runner service + public Scheduler(Connection connection, IQueueRunnerService queueRunnerService) { + m_dataConnection = connection; m_queueRunnerService = queueRunnerService; m_thread = new Thread(new ThreadStart(Runner)); m_schedule = []; @@ -99,7 +106,7 @@ namespace Duplicati.Server public IList> GetProposedSchedule() { return ( - from n in FIXMEGlobal.Scheduler.Schedule + from n in this.Schedule let backupid = (from t in n.Value.Tags where t != null && t.StartsWith("ID=", StringComparison.Ordinal) select t.Substring("ID=".Length)).FirstOrDefault() @@ -220,7 +227,7 @@ namespace Duplicati.Server { t.Item1.Time = t.Item2; t.Item1.LastRun = t.Item3; - FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1); + m_dataConnection.AddOrUpdateSchedule(t.Item1); } return Task.CompletedTask; @@ -256,8 +263,8 @@ namespace Duplicati.Server // to avoid frequent db lookups //Determine schedule list - var timeZoneInfo = FIXMEGlobal.DataConnection.ApplicationSettings.Timezone; - var lst = FIXMEGlobal.DataConnection.Schedules; + var timeZoneInfo = m_dataConnection.ApplicationSettings.Timezone; + var lst = m_dataConnection.Schedules; foreach (var sc in lst) { if (!string.IsNullOrEmpty(sc.Repeat)) @@ -291,7 +298,7 @@ namespace Duplicati.Server } catch (Exception ex) { - FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", + m_dataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); } @@ -300,7 +307,7 @@ namespace Duplicati.Server { var jobsToRun = new List(); //TODO: Cache this to avoid frequent lookups - foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct() + foreach (var id in m_dataConnection.GetBackupIDsForTags(sc.Tags).Distinct() .Select(x => x.ToString())) { //See if it is already queued @@ -315,11 +322,11 @@ namespace Duplicati.Server //If it is not already in queue, put it there if (!tmplst.Any(x => x == id)) { - var entry = FIXMEGlobal.DataConnection.GetBackup(id); + var entry = m_dataConnection.GetBackup(id); if (entry != null) { - var options = Server.Runner.GetCommonOptions(); - Server.Runner.ApplyOptions(entry, options); + var options = Server.Runner.GetCommonOptions(m_dataConnection); + Server.Runner.ApplyOptions(m_dataConnection, entry, options); if (new Library.Main.Options(options).DisableOnBattery && (Library.Utility.Power.PowerSupply.GetSource() == Library.Utility.Power.PowerSupply.Source.Battery)) @@ -364,7 +371,7 @@ namespace Duplicati.Server } catch (Exception ex) { - FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), + m_dataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); continue; } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs index cb95b681a..ccaa14a35 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs @@ -53,10 +53,6 @@ public interface IQueuedTask /// Func? OnFinished { get; set; } - /// - /// That action that performs the task. - /// - Task Execute(); /// /// Updates the throttle speeds for the task. /// diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 933cca135..794ceb955 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -99,11 +99,6 @@ namespace Duplicati.Server /// public static Database.Connection DataConnection { get => FIXMEGlobal.DataConnection; set => FIXMEGlobal.DataConnection = value; } - /// - /// This is the scheduling thread - /// - public static ISchedulerService Scheduler { get => FIXMEGlobal.Scheduler; } - /// /// The thread running the ping-pong handler /// @@ -137,20 +132,10 @@ namespace Duplicati.Server DuplicatiWebserver.Stop().GetAwaiter().GetResult(); } - /// - /// The update poll thread. - /// - public static UpdatePollThread UpdatePoller => FIXMEGlobal.UpdatePoller; - /// /// An event that is set once the server is ready to respond to requests /// - public static readonly System.Threading.ManualResetEvent ServerStartedEvent = new System.Threading.ManualResetEvent(false); - - /// - /// The status event signaler, used to control long polling of status updates - /// - public static EventPollNotify StatusEventNotifyer => FIXMEGlobal.Provider.GetRequiredService(); + public static readonly ManualResetEvent ServerStartedEvent = new ManualResetEvent(false); /// /// A delegate method for creating a copy of the current progress state @@ -244,6 +229,9 @@ namespace Duplicati.Server var crashed = false; var terminated = false; IQueueRunnerService queueRunner = null; + UpdatePollThread updatePollThread = null; + EventPollNotify eventPollNotify = null; + ISchedulerService scheduler = null; try { DataConnection = GetDatabaseConnection(commandlineOptions, silentConsole); @@ -267,14 +255,17 @@ namespace Duplicati.Server DuplicatiWebserver = StartWebServer(commandlineOptions, DataConnection).Await(); - queueRunner = DuplicatiWebserver.Provider.GetRequiredService(); DataConnection.SetServiceProvider(DuplicatiWebserver.Provider); + queueRunner = DuplicatiWebserver.Provider.GetRequiredService(); + updatePollThread = DuplicatiWebserver.Provider.GetRequiredService(); + eventPollNotify = DuplicatiWebserver.Provider.GetRequiredService(); + scheduler = DuplicatiWebserver.Provider.GetRequiredService(); - UpdatePoller.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION)); + updatePollThread.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION)); SetPurgeTempFilesTimer(commandlineOptions); - LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, DataConnection, StatusEventNotifyer, e); }; + LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, DataConnection, eventPollNotify, e); }; if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, PING_PONG_KEEPALIVE_OPTION)) { @@ -361,10 +352,10 @@ namespace Duplicati.Server Log.WriteInformationMessage(LOGTAG, "ServerStopping", Strings.Program.ServerStopping); var steps = new Action[] { - () => StatusEventNotifyer.SignalNewEvent(), - () => { if (ShutdownModernWebserver != null) ShutdownModernWebserver(); }, - () => UpdatePoller?.Terminate(), - () => Scheduler?.Terminate(true), + () => eventPollNotify?.SignalNewEvent(), + () => ShutdownModernWebserver(), + () => updatePollThread?.Terminate(), + () => scheduler?.Terminate(true), () => queueRunner?.Terminate(true), () => ApplicationInstance?.Dispose(), () => PurgeTempFilesTimer?.Dispose(), @@ -844,11 +835,6 @@ namespace Duplicati.Server Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter); } - private static void SignalNewEvent(object sender, EventArgs e) - { - StatusEventNotifyer.SignalNewEvent(); - } - /// /// This event handler updates the trayicon menu with the current state of the runner. /// diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs index acad0cfac..da2e33802 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs @@ -41,8 +41,8 @@ public class BackupGet : IEndpointV1 => ExecuteGet(connection, GetBackup(connection, id))) .RequireAuthorization(); - group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents) - => ExecuteGetFiles(GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary())) + group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents) + => ExecuteGetFiles(connection, eventPollNotify, GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary())) .RequireAuthorization(); group.MapGet("/backup/{id}/log", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize) @@ -53,16 +53,16 @@ public class BackupGet : IEndpointV1 => ExecuteGetRemotelog(connection, GetBackup(connection, id), offset, pagesize ?? 100)) .RequireAuthorization(); - group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly) - => ExecuteGetFilesets(GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false)) + group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly) + => ExecuteGetFilesets(connection, eventPollNotify, GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false)) .RequireAuthorization(); group.MapGet("/backup/{id}/export-argsonly", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase) - => ExecuteGetExportArgsOnly(GetBackup(connection, id), exportPasswords ?? false)) + => ExecuteGetExportArgsOnly(connection, GetBackup(connection, id), exportPasswords ?? false)) .RequireAuthorization(); group.MapGet("/backup/{id}/export-cmdline", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase) - => ExecuteGetExportCmdline(GetBackup(connection, id), exportPasswords ?? false)) + => ExecuteGetExportCmdline(connection, GetBackup(connection, id), exportPasswords ?? false)) .RequireAuthorization(); group.MapGet("/backup/{id}/export", ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IJWTTokenProvider jWTTokenProvider, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase, [FromQuery] string token, CancellationToken ct) => @@ -142,7 +142,7 @@ public class BackupGet : IEndpointV1 ); } - private static Dictionary SearchFiles(IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary extraValues) + private static Dictionary SearchFiles(Connection connection, EventPollNotify eventPollNotify, IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary extraValues) { if (string.IsNullOrWhiteSpace(timestring) && !allVersions) throw new BadRequestException("Invalid or missing time"); @@ -151,7 +151,7 @@ public class BackupGet : IEndpointV1 if (!allVersions) time = Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now); - var r = Runner.Run(Runner.CreateListTask(backup, filter == null ? null : [filter], prefixOnly, allVersions, folderContents, time), false) as Duplicati.Library.Interface.IListResults; + var r = Runner.Run(connection, eventPollNotify, Runner.CreateListTask(backup, filter == null ? null : [filter], prefixOnly, allVersions, folderContents, time), false) as IListResults; if (r == null) throw new ServerErrorException("No result from list operation"); @@ -170,8 +170,8 @@ public class BackupGet : IEndpointV1 return result; } - private static Dictionary ExecuteGetFiles(IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary extraValues) - => SearchFiles(bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues); + private static Dictionary ExecuteGetFiles(Connection connection, EventPollNotify eventPollNotify, IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary extraValues) + => SearchFiles(connection, eventPollNotify, bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues); private static List> ExecuteGetLog(Connection connection, IBackup bk, long? offset, long pagesize) { @@ -202,7 +202,7 @@ public class BackupGet : IEndpointV1 } } - private static IEnumerable ExecuteGetFilesets(IBackup bk, bool includeMetadata, bool fromRemoteOnly) + private static IEnumerable ExecuteGetFilesets(Connection connection, EventPollNotify eventPollNotify, IBackup bk, bool includeMetadata, bool fromRemoteOnly) { var extra = new Dictionary { @@ -218,7 +218,7 @@ public class BackupGet : IEndpointV1 try { - var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, bk, extra), false) as IListResults; + var r = Runner.Run(connection, eventPollNotify, Runner.CreateTask(DuplicatiOperation.List, bk, extra), false) as IListResults; if (r == null) throw new ServerErrorException("No result from list operation"); @@ -239,20 +239,20 @@ public class BackupGet : IEndpointV1 backup.SanitizeTargetUrl(); } - private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(IBackup backup, bool exportPasswords) + private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(Connection connection, IBackup backup, bool exportPasswords) { if (!exportPasswords) RemovePasswords(backup); - return new Dto.ExportCommandlineDto(Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup))); + return new Dto.ExportCommandlineDto(Runner.GetCommandLine(connection, Runner.CreateTask(DuplicatiOperation.Backup, backup))); } - private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(IBackup backup, bool exportPasswords) + private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(Connection connection, IBackup backup, bool exportPasswords) { if (!exportPasswords) RemovePasswords(backup); - var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup)); + var parts = Runner.GetCommandLineParts(connection, Runner.CreateTask(DuplicatiOperation.Backup, backup)); return new Dto.ExportArgsOnlyDto( parts.First(), parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)), diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs index 754cab235..77e763e42 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs @@ -36,9 +36,9 @@ public class BackupPutDelete : IEndpointV1 => ExecutePut(GetBackup(connection, id), connection, input)) .RequireAuthorization(); - group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery] bool? force) => + group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] ISchedulerService schedulerService, [FromServices] IQueueRunnerService queueRunnerService, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery] bool? force) => { - var res = ExecuteDelete(GetBackup(connection, id), queueRunnerService, liveControls, delete_remote_files ?? false, delete_local_db, force ?? false); + var res = ExecuteDelete(GetBackup(connection, id), schedulerService, queueRunnerService, liveControls, delete_remote_files ?? false, delete_local_db, force ?? false); if (res.Status != "OK" && httpContextAccessor.HttpContext != null) httpContextAccessor.HttpContext.Response.StatusCode = 500; return res; @@ -130,7 +130,7 @@ public class BackupPutDelete : IEndpointV1 } } - private static Dto.DeleteBackupOutputDto ExecuteDelete(IBackup backup, IQueueRunnerService queueRunnerService, LiveControls liveControls, bool delete_remote_files, bool? delete_local_db, bool force) + private static Dto.DeleteBackupOutputDto ExecuteDelete(IBackup backup, ISchedulerService schedulerService, IQueueRunnerService queueRunnerService, LiveControls liveControls, bool delete_remote_files, bool? delete_local_db, bool force) { try { @@ -179,7 +179,7 @@ public class BackupPutDelete : IEndpointV1 if (delete_remote_files) extra["delete-remote-files"] = "true"; - return new Dto.DeleteBackupOutputDto("OK", null, queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Delete, backup, extra))); + return new Dto.DeleteBackupOutputDto("OK", null, queueRunnerService.AddTask(Runner.CreateDeleteTask(backup, extra, (_) => schedulerService.Reschedule()))); } } diff --git a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs index 8771de22d..3abd2d4ef 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs @@ -22,6 +22,7 @@ using Duplicati.Library.Interface; using Duplicati.Library.Main; using Duplicati.Library.RestAPI; using Duplicati.Server; +using Duplicati.Server.Database; using Duplicati.WebserverCore.Abstractions; using Duplicati.WebserverCore.Exceptions; using Microsoft.AspNetCore.Mvc; @@ -38,8 +39,8 @@ namespace Duplicati.WebserverCore.Endpoints.V1 => ExecuteDbPath(input.path)) .RequireAuthorization(); - group.MapPost("/remoteoperation/test", ([FromQuery] bool? autocreate, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) - => ExecuteTest(input.path, autocreate ?? false, cancelToken)) + group.MapPost("/remoteoperation/test", ([FromServices] Connection connection, [FromQuery] bool? autocreate, [FromBody] RemoteOperationInput input, CancellationToken cancelToken) + => ExecuteTest(connection, input.path, autocreate ?? false, cancelToken)) .RequireAuthorization(); group.MapPost("/remoteoperation/create", ([FromBody] RemoteOperationInput input, CancellationToken cancelToken) @@ -53,11 +54,11 @@ namespace Duplicati.WebserverCore.Endpoints.V1 return new Dto.GetDbPathDto(!string.IsNullOrWhiteSpace(path), path); } - private static Dictionary ParseUrlOptions(Library.Utility.Uri uri) + private static Dictionary ParseUrlOptions(Connection connection, Library.Utility.Uri uri) { var qp = uri.QueryParameters; - var opts = Runner.GetCommonOptions(); + var opts = Runner.GetCommonOptions(connection); foreach (var k in qp.Keys.Cast()) opts[k] = qp[k]; @@ -87,10 +88,10 @@ namespace Duplicati.WebserverCore.Endpoints.V1 } } - private static async Task GetBackend(string url, CancellationToken cancelToken) + private static async Task GetBackend(Connection connection, string url, CancellationToken cancelToken) { var uri = new Library.Utility.Uri(url); - var opts = ParseUrlOptions(uri); + var opts = ParseUrlOptions(connection, uri); var tmp = new[] { uri }; await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancelToken); @@ -101,13 +102,13 @@ namespace Duplicati.WebserverCore.Endpoints.V1 return new TupleDisposeWrapper(backend, modules); } - private static async Task ExecuteTest(string url, bool autoCreate, CancellationToken cancelToken) + private static async Task ExecuteTest(Connection connection, string url, bool autoCreate, CancellationToken cancelToken) { TupleDisposeWrapper? wrapper = null; try { - wrapper = await GetBackend(url, cancelToken); + wrapper = await GetBackend(connection, url, cancelToken); using (var b = wrapper.Backend) { diff --git a/Duplicati/WebserverCore/Endpoints/V1/Updates.cs b/Duplicati/WebserverCore/Endpoints/V1/Updates.cs index 86e252dbf..029f60750 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Updates.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Updates.cs @@ -18,8 +18,9 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using Duplicati.Library.RestAPI; +using Duplicati.Server; using Duplicati.WebserverCore.Abstractions; +using Microsoft.AspNetCore.Mvc; namespace Duplicati.WebserverCore.Endpoints.V1; @@ -27,9 +28,11 @@ public class Updates : IEndpointV1 { public static void Map(RouteGroupBuilder group) { - group.MapPost("/updates/check", Execute).RequireAuthorization(); + group.MapPost("/updates/check", + ([FromServices] UpdatePollThread updatePollThread) => Execute(updatePollThread)) + .RequireAuthorization(); } - private static void Execute() - => FIXMEGlobal.UpdatePoller.CheckNow(); + private static void Execute(UpdatePollThread updatePollThread) + => updatePollThread.CheckNow(); } \ No newline at end of file diff --git a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs index 7c35c171f..20421f853 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/WebModules.cs @@ -22,6 +22,7 @@ using Duplicati.Library.Interface; using Duplicati.Library.Main; using Duplicati.Library.RestAPI; using Duplicati.Server; +using Duplicati.Server.Database; using Duplicati.WebserverCore.Abstractions; using Duplicati.WebserverCore.Exceptions; using Microsoft.AspNetCore.Mvc; @@ -33,18 +34,20 @@ public record WebModules : IEndpointV1 public static void Map(RouteGroupBuilder group) { group.MapGet("/webmodules", ExecuteGet).RequireAuthorization(); - group.MapPost("/webmodule/{modulekey}", ([FromRoute] string modulekey, [FromBody] Dictionary options, CancellationToken cancellationToken) => ExecutePost(modulekey, options, cancellationToken)).RequireAuthorization(); + group.MapPost("/webmodule/{modulekey}", ([FromServices] Connection connection, [FromRoute] string modulekey, [FromBody] Dictionary options, CancellationToken cancellationToken) + => ExecutePost(connection, modulekey, options, cancellationToken)) + .RequireAuthorization(); } private static IEnumerable ExecuteGet() => Library.DynamicLoader.WebLoader.Modules; - private static async Task ExecutePost(string modulekey, Dictionary inputOptions, CancellationToken cancellationToken) + private static async Task ExecutePost(Connection connection, string modulekey, Dictionary inputOptions, CancellationToken cancellationToken) { var m = Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(modulekey, StringComparison.OrdinalIgnoreCase)) ?? throw new NotFoundException("No such module found"); - var options = Runner.GetCommonOptions(); + var options = Runner.GetCommonOptions(connection); foreach (var k in inputOptions.Keys) options[k] = inputOptions[k]; diff --git a/Duplicati/WebserverCore/Endpoints/V2/BackupListing.cs b/Duplicati/WebserverCore/Endpoints/V2/BackupListing.cs index d013f73d5..5e301489e 100644 --- a/Duplicati/WebserverCore/Endpoints/V2/BackupListing.cs +++ b/Duplicati/WebserverCore/Endpoints/V2/BackupListing.cs @@ -33,33 +33,33 @@ public class BackupListing : IEndpointV2 { public static void Map(RouteGroupBuilder group) { - group.MapPost("/backup/list-filesets", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFilesetsRequestDto input) - => ExecuteGetFilesets(GetBackup(connection, input.BackupId))) + group.MapPost("/backup/list-filesets", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromBody] Dto.V2.ListFilesetsRequestDto input) + => ExecuteGetFilesets(connection, eventPollNotify, GetBackup(connection, input.BackupId))) .RequireAuthorization(); - group.MapPost("/backup/list-folder", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFolderContentRequestDto input) - => ExecuteListFolder(GetBackup(connection, input.BackupId), input)) + group.MapPost("/backup/list-folder", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromBody] Dto.V2.ListFolderContentRequestDto input) + => ExecuteListFolder(connection, eventPollNotify, GetBackup(connection, input.BackupId), input)) .RequireAuthorization(); - group.MapPost("/backup/list-versions", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFileVersionsRequestDto input) - => ExecuteListVersions(GetBackup(connection, input.BackupId), input)) + group.MapPost("/backup/list-versions", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromBody] Dto.V2.ListFileVersionsRequestDto input) + => ExecuteListVersions(connection, eventPollNotify, GetBackup(connection, input.BackupId), input)) .RequireAuthorization(); - group.MapPost("/backup/search", ([FromServices] Connection connection, [FromBody] Dto.V2.SearchEntriesRequestDto input) - => ExecuteSearch(GetBackup(connection, input.BackupId), input)) + group.MapPost("/backup/search", ([FromServices] Connection connection, [FromServices] EventPollNotify eventPollNotify, [FromBody] Dto.V2.SearchEntriesRequestDto input) + => ExecuteSearch(connection, eventPollNotify, GetBackup(connection, input.BackupId), input)) .RequireAuthorization(); } private static IBackup GetBackup(Connection connection, string id) => connection.GetBackup(id) ?? throw new NotFoundException("Backup not found"); - private static Dto.V2.ListFolderContentResponseDto ExecuteListFolder(IBackup bk, Dto.V2.ListFolderContentRequestDto input) + private static Dto.V2.ListFolderContentResponseDto ExecuteListFolder(Connection connection, EventPollNotify eventPollNotify, IBackup bk, Dto.V2.ListFolderContentRequestDto input) { var time = string.IsNullOrWhiteSpace(input.Time) ? new DateTime(0) : Library.Utility.Timeparser.ParseTimeInterval(input.Time, DateTime.Now); - var r = Runner.Run(Runner.CreateListFolderContents(bk, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFolderResults; + var r = Runner.Run(connection, eventPollNotify, Runner.CreateListFolderContents(bk, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFolderResults; if (r == null) throw new ServerErrorException("No result from list operation"); @@ -78,16 +78,16 @@ public class BackupListing : IEndpointV2 r.Entries.TotalCount); } - private static Dto.V2.ListFilesetsResponseDto ExecuteGetFilesets(IBackup bk) + private static Dto.V2.ListFilesetsResponseDto ExecuteGetFilesets(Connection connection, EventPollNotify eventPollNotify, IBackup bk) { - var extra = new Dictionary(); + var extra = new Dictionary(); // Retries will hang the http request extra["number-of-retries"] = "0"; try { - var r = Runner.Run(Runner.CreateListFilesetsTask(bk, extra), false) as IListFilesetResults; + var r = Runner.Run(connection, eventPollNotify, Runner.CreateListFilesetsTask(bk, extra), false) as IListFilesetResults; if (r == null) throw new ServerErrorException("No result from list operation"); @@ -112,9 +112,9 @@ public class BackupListing : IEndpointV2 } } - private static Dto.V2.ListFileVersionsOutputDto ExecuteListVersions(IBackup bk, Dto.V2.ListFileVersionsRequestDto input) + private static Dto.V2.ListFileVersionsOutputDto ExecuteListVersions(Connection connection, EventPollNotify eventPollNotify, IBackup bk, Dto.V2.ListFileVersionsRequestDto input) { - var r = Runner.Run(Runner.ListFileVersionsTask(bk, input.Paths, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFileVersionsResults; + var r = Runner.Run(connection, eventPollNotify, Runner.ListFileVersionsTask(bk, input.Paths, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFileVersionsResults; if (r == null) throw new ServerErrorException("No result from list operation"); @@ -135,13 +135,13 @@ public class BackupListing : IEndpointV2 r.FileVersions.TotalCount); } - private static Dto.V2.SearchEntriesResponseDto ExecuteSearch(IBackup bk, Dto.V2.SearchEntriesRequestDto input) + private static Dto.V2.SearchEntriesResponseDto ExecuteSearch(Connection connection, EventPollNotify eventPollNotify, IBackup bk, Dto.V2.SearchEntriesRequestDto input) { var time = string.IsNullOrWhiteSpace(input.Time) ? new DateTime(0) : Library.Utility.Timeparser.ParseTimeInterval(input.Time, DateTime.Now); - var r = Runner.Run(Runner.CreateSearchEntriesTask(bk, input.Filters, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as ISearchFilesResults; + var r = Runner.Run(connection, eventPollNotify, Runner.CreateSearchEntriesTask(bk, input.Filters, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as ISearchFilesResults; if (r == null) throw new ServerErrorException("No result from list operation"); diff --git a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs index 6e63db05b..c44725bf7 100644 --- a/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs +++ b/Duplicati/WebserverCore/Endpoints/V2/DestinationVerify.cs @@ -36,7 +36,7 @@ public class DestinationVerify : IEndpointV2 public static void Map(RouteGroupBuilder group) { group.MapPost("/destination/test", ([FromServices] Connection connection, [FromBody] Dto.V2.DestinationTestRequestDto input, CancellationToken cancelToken) - => ExecuteTest(input, cancelToken)) + => ExecuteTest(connection, input, cancelToken)) .RequireAuthorization(); } @@ -51,11 +51,11 @@ public class DestinationVerify : IEndpointV2 } } - private static Dictionary ParseUrlOptions(Library.Utility.Uri uri) + private static Dictionary ParseUrlOptions(Connection connection, Library.Utility.Uri uri) { var qp = uri.QueryParameters; - var opts = Runner.GetCommonOptions(); + var opts = Runner.GetCommonOptions(connection); foreach (var k in qp.Keys.Cast()) opts[k] = qp[k]; @@ -75,10 +75,10 @@ public class DestinationVerify : IEndpointV2 return modules; } - private static async Task GetBackend(string url, CancellationToken cancelToken) + private static async Task GetBackend(Connection connection, string url, CancellationToken cancelToken) { var uri = new Library.Utility.Uri(url); - var opts = ParseUrlOptions(uri); + var opts = ParseUrlOptions(connection, uri); var tmp = new[] { uri }; await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancelToken); @@ -90,13 +90,13 @@ public class DestinationVerify : IEndpointV2 } - private static async Task ExecuteTest(DestinationTestRequestDto input, CancellationToken cancelToken) + private static async Task ExecuteTest(Connection connection, DestinationTestRequestDto input, CancellationToken cancelToken) { TupleDisposeWrapper? wrapper = null; try { - wrapper = await GetBackend(input.DestinationUrl, cancelToken); + wrapper = await GetBackend(connection, input.DestinationUrl, cancelToken); using (var b = wrapper.Backend) { diff --git a/Duplicati/WebserverCore/Services/QueueRunnerService.cs b/Duplicati/WebserverCore/Services/QueueRunnerService.cs index 9335bcc93..4313c0814 100644 --- a/Duplicati/WebserverCore/Services/QueueRunnerService.cs +++ b/Duplicati/WebserverCore/Services/QueueRunnerService.cs @@ -136,7 +136,7 @@ public class QueueRunnerService(Connection connection, EventPollNotify eventPoll if (task.OnStarting != null) await task.OnStarting().ConfigureAwait(false); - await task.Execute(); + Runner.Run(connection, eventPollNotify, task, false); // If the task is completed, don't call OnFinished again completed = true; diff --git a/Duplicati/WebserverCore/Services/SchedulerService.cs b/Duplicati/WebserverCore/Services/SchedulerService.cs index b327c83ba..381fe38d9 100644 --- a/Duplicati/WebserverCore/Services/SchedulerService.cs +++ b/Duplicati/WebserverCore/Services/SchedulerService.cs @@ -21,6 +21,7 @@ using Duplicati.Library.RestAPI; using Duplicati.Server; +using Duplicati.Server.Database; using Duplicati.Server.Serialization.Interface; using Duplicati.WebserverCore.Abstractions; @@ -29,9 +30,9 @@ namespace WebserverCore.Services; public class SchedulerService : ISchedulerService { private readonly Scheduler scheduler; - public SchedulerService(EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IQueueRunnerService queueRunnerService) + public SchedulerService(Connection connection, EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IQueueRunnerService queueRunnerService) { - this.scheduler = new Scheduler(queueRunnerService); + this.scheduler = new Scheduler(connection, queueRunnerService); var lastScheduleId = notificationUpdateService.LastDataUpdateId; eventPollNotify.NewEvent += (sender, e) => {