From ee371c7413f4bd456299a4d14dff2b0973376fd2 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 9 Jun 2026 16:29:02 +0200 Subject: [PATCH] Handle script modified filter This PR fixes a bug where a script could write a new filter, but the filter was never applied to the backup. This also fixes an issue where the filter string was not available in scripts or emails, except on the initial call. This PR updates the logic so the `run-script-after` and `run-script-post-backup` also have access to the filter string, if needed for reporting or other. This fixes #5143 --- Duplicati/Library/Main/Controller.cs | 185 +++++++++++------- .../Library/Main/Operation/BackupHandler.cs | 2 + .../Library/Modules/Builtin/RunScript.cs | 10 +- Duplicati/UnitTest/RunScriptTests.cs | 122 ++++++++++-- 4 files changed, 219 insertions(+), 100 deletions(-) diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs index dc5620119..0ca46706c 100644 --- a/Duplicati/Library/Main/Controller.cs +++ b/Duplicati/Library/Main/Controller.cs @@ -114,7 +114,7 @@ namespace Duplicati.Library.Main } /// - public async Task BackupAsync(string[] inputsources, IFilter filter = null) + public async Task BackupAsync(string[] inputsources, IFilter inputFilter = null) { UsageReporter.Reporter.Report("USE_BACKEND", new Library.Utility.Uri(m_backendUrl).Scheme); UsageReporter.Reporter.Report("USE_COMPRESSION", m_options.CompressionModule); @@ -130,9 +130,8 @@ namespace Duplicati.Library.Main CheckAutoCompactInterval(); CheckAutoVacuumInterval(); - return await RunActionAsync(new BackupResults(), inputsources, filter, async (result, backendManager) => + return await RunActionAsync(new BackupResults(), inputsources, inputFilter, async (result, backendManager, filter) => { - using (var h = new Operation.BackupHandler(m_options, result)) await h.RunAsync(ExpandInputSources(inputsources, filter), backendManager, filter) .ConfigureAwait(false); @@ -148,9 +147,9 @@ namespace Duplicati.Library.Main } /// - public async Task RestoreAsync(string[] paths, IFilter filter = null) + public async Task RestoreAsync(string[] paths, IFilter inputFilter = null) { - return await RunActionAsync(new RestoreResults(), paths, filter, async (result, backendManager) => + return await RunActionAsync(new RestoreResults(), paths, inputFilter, async (result, backendManager, filter) => { using var restoreDestination = (m_options.Restorepath ?? "").StartsWith("@") @@ -186,9 +185,9 @@ namespace Duplicati.Library.Main } /// - public async Task RestoreControlFilesAsync(IEnumerable files = null, IFilter filter = null) + public async Task RestoreControlFilesAsync(IEnumerable files = null, IFilter inputFilter = null) { - return await RunActionAsync(new RestoreControlFilesResults(), filter, (result, backendManager) => + return await RunActionAsync(new RestoreControlFilesResults(), inputFilter, (result, backendManager, filter) => new Operation.RestoreControlFilesHandler(m_options, result) .RunAsync(files, backendManager, filter) ).ConfigureAwait(false); @@ -197,16 +196,16 @@ namespace Duplicati.Library.Main /// public async Task DeleteAsync() { - return await RunActionAsync(new DeleteResults(), (result, backendManager) => + return await RunActionAsync(new DeleteResults(), (result, backendManager, filter) => new Operation.DeleteHandler(m_options, result) .RunAsync(backendManager) ).ConfigureAwait(false); } /// - public async Task RepairAsync(IFilter filter = null) + public async Task RepairAsync(IFilter inputFilter = null) { - return await RunActionAsync(new RepairResults(), filter, (result, backendManager) => + return await RunActionAsync(new RepairResults(), inputFilter, (result, backendManager, filter) => new Operation.RepairHandler(m_options, result) .RunAsync(backendManager, filter) ).ConfigureAwait(false); @@ -214,25 +213,25 @@ namespace Duplicati.Library.Main /// public async Task ListFilesetsAsync() - => await RunActionAsync(new ListFilesetResults(), (result, backendManager) => + => await RunActionAsync(new ListFilesetResults(), (result, backendManager, _) => Operation.ListFilesetsHandler.RunAsync(m_options, result, backendManager) ).ConfigureAwait(false); /// public async Task ListFolderAsync(string[] folders, long offset, long limit, bool extendedData) - => await RunActionAsync(new ListFolderResults(), (result, _) => + => await RunActionAsync(new ListFolderResults(), (result, _, _) => Operation.ListFolderHandler.RunAsync(m_options, result, folders, offset, limit, extendedData) ).ConfigureAwait(false); /// public async Task ListFileVersionsAsync(string[] files, long offset, long limit) - => await RunActionAsync(new ListFileVersionsResults(), (result, backendManager) => + => await RunActionAsync(new ListFileVersionsResults(), (result, backendManager, _) => Operation.ListFileVersionsHandler.RunAsync(m_options, result, files, offset, limit) ).ConfigureAwait(false); /// - public async Task SearchEntriesAsync(string[] pathprefixes, IFilter filter, long offset, long limit, bool extendedData) - => await RunActionAsync(new SearchFilesResults(), filter, (result, backendManager) => + public async Task SearchEntriesAsync(string[] pathprefixes, IFilter inputFilter, long offset, long limit, bool extendedData) + => await RunActionAsync(new SearchFilesResults(), inputFilter, (result, backendManager, filter) => Operation.SearchEntriesHandler.RunAsync(m_options, result, pathprefixes, filter, offset, limit, extendedData) ).ConfigureAwait(false); @@ -245,18 +244,18 @@ namespace Duplicati.Library.Main => ListAsync(filterstring == null ? null : new string[] { filterstring }, null); /// - public async Task ListAsync(IEnumerable filterstrings, IFilter filter) + public async Task ListAsync(IEnumerable filterstrings, IFilter inputFilter) { - return await RunActionAsync(new ListResults(), filter, (result, backendManager) => + return await RunActionAsync(new ListResults(), inputFilter, (result, backendManager, filter) => new Operation.ListFilesHandler(m_options, result) .RunAsync(backendManager, filterstrings, filter) ).ConfigureAwait(false); } /// - public async Task ListControlFilesAsync(IEnumerable filterstrings, IFilter filter) + public async Task ListControlFilesAsync(IEnumerable filterstrings, IFilter inputFilter) { - return await RunActionAsync(new ListResults(), filter, (result, backendManager) => + return await RunActionAsync(new ListResults(), inputFilter, (result, backendManager, filter) => new Operation.ListControlFilesHandler(m_options, result) .RunAsync(backendManager, filterstrings, filter) ).ConfigureAwait(false); @@ -265,7 +264,7 @@ namespace Duplicati.Library.Main /// public async Task ListRemoteAsync() { - return await RunActionAsync(new ListRemoteResults(), async (result, backendManager) => + return await RunActionAsync(new ListRemoteResults(), async (result, backendManager, _) => { using var tf = File.Exists(m_options.Dbpath) ? null : new TempFile(); await using var db = await LocalDatabase.CreateLocalDatabaseAsync(((string)tf) ?? m_options.Dbpath, "list-remote", true, null, result.TaskControl.ProgressToken).ConfigureAwait(false); @@ -276,7 +275,7 @@ namespace Duplicati.Library.Main /// public async Task DeleteAllRemoteFilesAsync() { - return await RunActionAsync(new ListRemoteResults(), async (result, backendManager) => + return await RunActionAsync(new ListRemoteResults(), async (result, backendManager, _) => { result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Listing); { @@ -335,7 +334,7 @@ namespace Duplicati.Library.Main { CheckAutoVacuumInterval(); - return await RunActionAsync(new CompactResults(), (result, backendManager) => + return await RunActionAsync(new CompactResults(), (result, backendManager, _) => new Operation.CompactHandler(m_options, result) .RunAsync(backendManager) ).ConfigureAwait(false); @@ -344,7 +343,7 @@ namespace Duplicati.Library.Main /// public async Task SetLocksAsync() { - return await RunActionAsync(new SetLockResults(), (result, backendManager) => + return await RunActionAsync(new SetLockResults(), (result, backendManager, _) => new Operation.SetLocksHandler(m_options, result) .RunAsync(backendManager) ).ConfigureAwait(false); @@ -353,18 +352,18 @@ namespace Duplicati.Library.Main /// public async Task ReadLockInfoAsync() { - return await RunActionAsync(new ReadLockInfoResults(), (result, backendManager) => + return await RunActionAsync(new ReadLockInfoResults(), (result, backendManager, _) => new Operation.ReadLockInfoHandler(m_options, result) .RunAsync(backendManager) ).ConfigureAwait(false); } /// - public async Task UpdateDatabaseWithVersionsAsync(IFilter filter = null) + public async Task UpdateDatabaseWithVersionsAsync(IFilter inputFilter = null) { var filelistfilter = Operation.RestoreHandler.GetNumberedFilelistFilterDelegate(m_options.Time, m_options.Version, singleTimeMatch: true); - return await RunActionAsync(new RecreateDatabaseResults(), filter, async (result, backendManager) => + return await RunActionAsync(new RecreateDatabaseResults(), inputFilter, async (result, backendManager, filter) => { using (var h = new Operation.RecreateDatabaseHandler(m_options, result)) await h.RunUpdateAsync(backendManager, filter, filelistfilter, null) @@ -377,17 +376,17 @@ namespace Duplicati.Library.Main { var t = new string[] { targetpath }; - return await RunActionAsync(new CreateLogDatabaseResults(), t, async (result, backendManager) => + return await RunActionAsync(new CreateLogDatabaseResults(), t, async (result, backendManager, _) => await new Operation.CreateBugReportHandler(t[0], m_options, result).RunAsync() ).ConfigureAwait(false); } /// - public async Task ListChangesAsync(string baseVersion, string targetVersion, IEnumerable filterstrings = null, IFilter filter = null, Action>> callback = null) + public async Task ListChangesAsync(string baseVersion, string targetVersion, IEnumerable filterstrings = null, IFilter inputFilter = null, Action>> callback = null) { var t = new string[] { baseVersion, targetVersion }; - return await RunActionAsync(new ListChangesResults(), t, filter, async (result, backendManager) => + return await RunActionAsync(new ListChangesResults(), t, inputFilter, async (result, backendManager, filter) => await new Operation.ListChangesHandler(m_options, result) .RunAsync(t[0], t[1], backendManager, filterstrings, filter, callback) ).ConfigureAwait(false); @@ -396,7 +395,7 @@ namespace Duplicati.Library.Main /// public async Task ListAffectedAsync(List args, Action callback = null) { - return await RunActionAsync(new ListAffectedResults(), (result, backendManager) => + return await RunActionAsync(new ListAffectedResults(), (result, backendManager, _) => new Operation.ListAffected(m_options, result) .RunAsync(args, callback) ).ConfigureAwait(false); @@ -408,14 +407,14 @@ namespace Duplicati.Library.Main if (!m_options.RawOptions.ContainsKey("full-remote-verification")) m_options.RawOptions["full-remote-verification"] = "true"; - return await RunActionAsync(new TestResults(), (result, backendManager) => + return await RunActionAsync(new TestResults(), (result, backendManager, _) => new Operation.TestHandler(m_options, result) .RunAsync(samples, backendManager) ).ConfigureAwait(false); } /// - public async Task TestFilterAsync(string[] paths, IFilter filter = null) + public async Task TestFilterAsync(string[] paths, IFilter inputFilter = null) { m_options.RawOptions["dry-run"] = "true"; m_options.RawOptions["dbpath"] = "INVALID!"; @@ -424,7 +423,7 @@ namespace Duplicati.Library.Main var filtertag = Logging.Log.LogTagFromType(typeof(Operation.Backup.FileEnumerationProcess)); using (Logging.Log.StartScope(m_messageSink.WriteMessage, x => x.FilterTag.Contains(filtertag))) { - return await RunActionAsync(new TestFilterResults(), paths, filter, (result, backendManager) => + return await RunActionAsync(new TestFilterResults(), paths, inputFilter, (result, backendManager, filter) => new Operation.TestFilterHandler(m_options, result) .RunAsync(ExpandInputSources(paths, filter), filter) ).ConfigureAwait(false); @@ -434,33 +433,33 @@ namespace Duplicati.Library.Main /// public async Task SystemInfoAsync() { - return await RunActionAsync(new SystemInfoResults(), (result, backendManager) => + return await RunActionAsync(new SystemInfoResults(), (result, backendManager, _) => Operation.SystemInfoHandler.RunAsync(result) ).ConfigureAwait(false); } /// - public async Task PurgeFilesAsync(IFilter filter) + public async Task PurgeFilesAsync(IFilter inputFilter) { - return await RunActionAsync(new PurgeFilesResults(), (result, backendManager) => + return await RunActionAsync(new PurgeFilesResults(), (result, backendManager, filter) => new Operation.PurgeFilesHandler(m_options, result) .RunAsync(backendManager, filter) ).ConfigureAwait(false); } /// - public async Task ListBrokenFilesAsync(IFilter filter, Func callbackhandler = null) + public async Task ListBrokenFilesAsync(IFilter inputFilter, Func callbackhandler = null) { - return await RunActionAsync(new ListBrokenFilesResults(), (result, backendManager) => + return await RunActionAsync(new ListBrokenFilesResults(), (result, backendManager, filter) => new Operation.ListBrokenFilesHandler(m_options, result) .RunAsync(backendManager, filter, callbackhandler) ).ConfigureAwait(false); } /// - public async Task PurgeBrokenFilesAsync(IFilter filter) + public async Task PurgeBrokenFilesAsync(IFilter inputFilter) { - return await RunActionAsync(new PurgeBrokenFilesResults(), (result, backendManager) => + return await RunActionAsync(new PurgeBrokenFilesResults(), (result, backendManager, filter) => new Operation.PurgeBrokenFilesHandler(m_options, result) .RunAsync(backendManager, filter) ).ConfigureAwait(false); @@ -489,7 +488,7 @@ namespace Duplicati.Library.Main var filtertag = Logging.Log.LogTagFromType(DynamicLoader.GenericLoader.GetModule(sendMailModuleKey).GetType()); using (Logging.Log.StartScope(m_messageSink.WriteMessage, x => x.FilterTag.Contains(filtertag))) { - return await RunActionAsync(new SendMailResults(), (result, backendManager) => + return await RunActionAsync(new SendMailResults(), (result, backendManager, _) => Task.Run(() => { result.Lines = new string[0]; @@ -502,13 +501,13 @@ namespace Duplicati.Library.Main /// public async Task VacuumAsync() { - return await RunActionAsync(new VacuumResults(), (result, backendManager) => + return await RunActionAsync(new VacuumResults(), (result, backendManager, _) => new Operation.VacuumHandler(m_options, result) .RunAsync() ).ConfigureAwait(false); } - private Task RunActionAsync(T result, Func method) + private Task RunActionAsync(T result, Func method) where T : ISetCommonOptions, ITaskControlProvider, Logging.ILogDestination, IBasicResults, IBackendWriterProvider { var tmp = new string[0]; @@ -516,21 +515,21 @@ namespace Duplicati.Library.Main return RunActionAsync(result, tmp, tempfilter, method); } - private Task RunActionAsync(T result, string[] paths, Func method) + private Task RunActionAsync(T result, string[] paths, Func method) where T : ISetCommonOptions, ITaskControlProvider, Logging.ILogDestination, IBasicResults, IBackendWriterProvider { IFilter tempfilter = null; return RunActionAsync(result, paths, tempfilter, method); } - private Task RunActionAsync(T result, IFilter filter, Func method) + private Task RunActionAsync(T result, IFilter filter, Func method) where T : ISetCommonOptions, ITaskControlProvider, Logging.ILogDestination, IBasicResults, IBackendWriterProvider { var tmp = new string[0]; return RunActionAsync(result, tmp, filter, method); } - private async Task RunActionAsync(T result, string[] paths, IFilter filter, Func method) + private async Task RunActionAsync(T result, string[] paths, IFilter filter, Func method) where T : ISetCommonOptions, ITaskControlProvider, Logging.ILogDestination, IBasicResults, IBackendWriterProvider { OnOperationStarted?.Invoke(result); @@ -563,7 +562,7 @@ namespace Duplicati.Library.Main { backend.UpdateThrottleValues(m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond); m_currentBackendManager = backend; - await method(result, backend).ConfigureAwait(false); + await method(result, backend, filter).ConfigureAwait(false); } finally { @@ -633,7 +632,7 @@ namespace Duplicati.Library.Main } } - OperationComplete(result, null); + OperationComplete(result, null, filter); Logging.Log.WriteInformationMessage(LOGTAG, "CompletedOperation", Strings.Controller.CompletedOperationMessage(m_options.MainAction)); @@ -661,7 +660,7 @@ namespace Duplicati.Library.Main } // Do not propagate the cancel exception - OperationComplete(result, null); + OperationComplete(result, null, filter); return result; } @@ -686,7 +685,7 @@ namespace Duplicati.Library.Main } // Report the result, and the failure - OperationComplete(result, ex); + OperationComplete(result, ex, filter); throw; } @@ -697,8 +696,45 @@ namespace Duplicati.Library.Main } } - private void OperationComplete(IBasicResults result, Exception exception) + /// + /// Temporarily pushes the filter into the raw options so modules can read it. + /// Returns a disposable that removes the key when disposed. + /// + /// The options to push the filter into + /// The filter to push + /// A disposable that removes the key when disposed + internal static IDisposable PushFilterToOptions(Options options, IFilter filter) + => PushFilterToOptions(options, filter, out var _); + /// + /// Temporarily pushes the filter into the raw options so modules can read it. + /// Returns a disposable that removes the key when disposed. + /// + /// The options to push the filter into + /// The filter to push + /// A disposable that removes the key when disposed + internal static IDisposable PushFilterToOptions(Options options, IFilter filter, out string filterString) { + filterString = filter != null && !filter.Empty + ? string.Join(Path.PathSeparator.ToString(), FilterExpression.Serialize(filter)) + : null; + + options.RawOptions["filter"] = filterString; + return new FilterOptionScope(options); + } + + /// + /// Helper to remove the filter option on dispose + /// + private sealed class FilterOptionScope : IDisposable + { + private readonly Options m_opts; + public FilterOptionScope(Options opts) => m_opts = opts; + public void Dispose() => m_opts?.RawOptions.Remove("filter"); + } + + private void OperationComplete(IBasicResults result, Exception exception, IFilter filter) + { + using var _ = PushFilterToOptions(m_options, filter); if (m_options?.LoadedModules != null) { foreach (var mx in m_options.LoadedModules) @@ -756,45 +792,42 @@ namespace Duplicati.Library.Main m_options.AddLoadedModule(DynamicLoader.GenericLoader.GetModule(m.Key)); } - // Make the filter read-n-write able in the generic modules - var pristinefilter = string.Join(Path.PathSeparator.ToString(), FilterExpression.Serialize(filter)); - m_options.RawOptions["filter"] = pristinefilter; - // Store the URL connection options separately, as these should only be visible to modules implementing IConnectionModule var conopts = new Dictionary(m_options.RawOptions); var qp = new Library.Utility.Uri(m_backendUrl).QueryParameters; foreach (var k in qp.Keys) conopts[(string)k] = qp[(string)k]; - foreach (var mx in m_options.LoadedModules) + // Make the filter read-n-write able in the generic modules + using (PushFilterToOptions(m_options, filter, out var pristinefilter)) { - if (mx is IConnectionModule) - mx.Configure(conopts); - else - mx.Configure(m_options.RawOptions); - - if (mx is IGenericSourceModule sourcemodule) + foreach (var mx in m_options.LoadedModules) { - if (sourcemodule.ContainFilesForBackup(paths)) - { - var sourceoptions = sourcemodule.ParseSourcePaths(ref paths, ref pristinefilter, m_options.RawOptions); + if (mx is IConnectionModule) + mx.Configure(conopts); + else + mx.Configure(m_options.RawOptions); - foreach (var sourceoption in sourceoptions) - m_options.RawOptions[sourceoption.Key] = sourceoption.Value; + if (mx is IGenericSourceModule sourcemodule) + { + if (sourcemodule.ContainFilesForBackup(paths)) + { + var sourceoptions = sourcemodule.ParseSourcePaths(ref paths, ref pristinefilter, m_options.RawOptions); + + foreach (var sourceoption in sourceoptions) + m_options.RawOptions[sourceoption.Key] = sourceoption.Value; + } } + + if (mx is IGenericCallbackModule module) + module.OnStart(result.MainOperation.ToString(), ref m_backendUrl, ref paths); } - if (mx is IGenericCallbackModule module) - module.OnStart(result.MainOperation.ToString(), ref m_backendUrl, ref paths); + // If the filters were changed by a module, read them back in + if (pristinefilter != m_options.RawOptions["filter"]) + filter = FilterExpression.Deserialize(m_options.RawOptions["filter"].Split(new string[] { Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)); } - // If the filters were changed by a module, read them back in - if (pristinefilter != m_options.RawOptions["filter"]) - { - filter = FilterExpression.Deserialize(m_options.RawOptions["filter"].Split(new string[] { Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)); - } - m_options.RawOptions.Remove("filter"); // "--filter" is not a supported command line option - if (!string.IsNullOrEmpty(m_options.Logfile)) { var path = Path.GetDirectoryName(Path.GetFullPath(m_options.Logfile)); diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 5a3f2287e..bb4218d89 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -899,6 +899,8 @@ namespace Duplicati.Library.Main.Operation // and sources are disposed, but before verification if (await m_result.TaskControl.ProgressRendevouzAsync().ConfigureAwait(false) && m_options.LoadedModules != null) { + // Make filter available to modules + using var _ = Controller.PushFilterToOptions(m_options, filter); foreach (var mx in m_options.LoadedModules) if (mx is IBackupCallbackModule module) try { module.OnFinishBackup(m_result, null); } diff --git a/Duplicati/Library/Modules/Builtin/RunScript.cs b/Duplicati/Library/Modules/Builtin/RunScript.cs index 92b1c03a7..063cee1ef 100644 --- a/Duplicati/Library/Modules/Builtin/RunScript.cs +++ b/Duplicati/Library/Modules/Builtin/RunScript.cs @@ -265,13 +265,11 @@ namespace Duplicati.Library.Modules.Builtin else level = ParsedResultType.Error; - using (TempFile tmpfile = new TempFile()) - { - using (var streamWriter = new StreamWriter(tmpfile)) - streamWriter.Write(resultFormatSerializer.Serialize(result, exception, m_logstorage, null)); + using var tmpfile = new TempFile(); + using (var streamWriter = new StreamWriter(tmpfile)) + streamWriter.Write(resultFormatSerializer.Serialize(result, exception, m_logstorage, null)); - Execute(m_finishScript, "AFTER", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_enableArguments, m_options, tmpfile, level); - } + Execute(m_finishScript, "AFTER", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_enableArguments, m_options, tmpfile, level); } /// diff --git a/Duplicati/UnitTest/RunScriptTests.cs b/Duplicati/UnitTest/RunScriptTests.cs index 3e8b5150b..726ac217f 100644 --- a/Duplicati/UnitTest/RunScriptTests.cs +++ b/Duplicati/UnitTest/RunScriptTests.cs @@ -96,9 +96,6 @@ namespace Duplicati.UnitTest options["blocksize"] = blocksize.ToString() + "b"; options["run-script-timeout"] = "5s"; - // We need a small delay as we run very small backups back-to-back - var PAUSE_TIME = TimeSpan.FromSeconds(3); - BorderTests.WriteTestFilesToFolder(DATAFOLDER, blocksize, 0); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) @@ -110,7 +107,6 @@ namespace Duplicati.UnitTest throw new Exception("Unexpected result from base backup"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(0); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -121,7 +117,6 @@ namespace Duplicati.UnitTest throw new Exception("Backup did not examine any files for code 0?"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(1); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -132,7 +127,6 @@ namespace Duplicati.UnitTest throw new Exception("Backup did examine files for code 1?"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(2); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -143,7 +137,6 @@ namespace Duplicati.UnitTest throw new Exception("Backup did not examine any files for code 2?"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(3); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -154,7 +147,6 @@ namespace Duplicati.UnitTest throw new Exception("Backup did examine files for code 3?"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(4); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -167,7 +159,6 @@ namespace Duplicati.UnitTest foreach (int exitCode in new[] { 5, 6, 10, 99 }) { - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(exitCode); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -179,7 +170,6 @@ namespace Duplicati.UnitTest } } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(2, "TEST WARNING MESSAGE"); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -192,7 +182,6 @@ namespace Duplicati.UnitTest throw new Exception("Found no warning message in output for code 2"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(3, "TEST WARNING MESSAGE"); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -205,7 +194,6 @@ namespace Duplicati.UnitTest throw new Exception("Found no warning message in output for code 3"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(4, "TEST ERROR MESSAGE"); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -218,7 +206,6 @@ namespace Duplicati.UnitTest throw new Exception("Found no error message in output for code 4"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(5, "TEST ERROR MESSAGE"); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -231,7 +218,6 @@ namespace Duplicati.UnitTest throw new Exception("Found no error message in output for code 5"); } - System.Threading.Thread.Sleep(PAUSE_TIME); options["run-script-before"] = CreateScript(0, sleeptime: 10); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { @@ -381,6 +367,109 @@ namespace Duplicati.UnitTest Assert.AreEqual(3, customTargetEntries.Count()); } + [Test] + [Category("Border")] + public async Task CustomFilterAsync() + { + var expectedFilter = "+[^(.*b.*)$]"; + var parsedResultFileAfter = Path.Combine(this.RESTOREFOLDER, "result_after.txt"); + var parsedResultFilePostBackup = Path.Combine(this.RESTOREFOLDER, "result_post_backup.txt"); + + var customCommandsBefore = new List + { + $"echo --filter=\"{expectedFilter}\"" + }; + + var customCommandsAfter = new List(); + if (OperatingSystem.IsWindows()) + { + customCommandsAfter.Add($"echo %DUPLICATI__filter%>\"{parsedResultFileAfter}\""); + } + else + { + customCommandsAfter.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFileAfter}\""); + } + + var customCommandsPostBackup = new List(); + if (OperatingSystem.IsWindows()) + { + customCommandsPostBackup.Add($"echo %DUPLICATI__filter%>\"{parsedResultFilePostBackup}\""); + } + else + { + customCommandsPostBackup.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFilePostBackup}\""); + } + + var options = new Dictionary(this.TestOptions); + options["run-script-before"] = CreateScript(0, null, null, 0, customCommandsBefore); + options["run-script-after"] = CreateScript(0, null, null, 0, customCommandsAfter); + options["run-script-post-backup"] = CreateScript(0, null, null, 0, customCommandsPostBackup); + + using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null)) + { + var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER }); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + var linesAfter = File.ReadAllLines(parsedResultFileAfter); + Assert.AreEqual(1, linesAfter.Length); + Assert.AreEqual(expectedFilter, linesAfter[0]); + + var linesPostBackup = File.ReadAllLines(parsedResultFilePostBackup); + Assert.AreEqual(1, linesPostBackup.Length); + Assert.AreEqual(expectedFilter, linesPostBackup[0]); + } + + [Test] + [Category("Border")] + public async Task CommandLineFilterSeenByScriptsAsync() + { + var expectedFilter = "+*B*"; + var parsedResultFileAfter = Path.Combine(this.RESTOREFOLDER, "result_after_cmdline.txt"); + var parsedResultFilePostBackup = Path.Combine(this.RESTOREFOLDER, "result_post_backup_cmdline.txt"); + + var customCommandsAfter = new List(); + if (OperatingSystem.IsWindows()) + { + customCommandsAfter.Add($"echo %DUPLICATI__filter%>\"{parsedResultFileAfter}\""); + } + else + { + customCommandsAfter.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFileAfter}\""); + } + + var customCommandsPostBackup = new List(); + if (OperatingSystem.IsWindows()) + { + customCommandsPostBackup.Add($"echo %DUPLICATI__filter%>\"{parsedResultFilePostBackup}\""); + } + else + { + customCommandsPostBackup.Add($"echo \"$DUPLICATI__filter\">\"{parsedResultFilePostBackup}\""); + } + + var options = new Dictionary(this.TestOptions); + options["run-script-after"] = CreateScript(0, null, null, 0, customCommandsAfter); + options["run-script-post-backup"] = CreateScript(0, null, null, 0, customCommandsPostBackup); + + using (var c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null)) + { + var filter = new Library.Utility.FilterExpression("*b*", true); + var backupResults = await c.BackupAsync(new[] { this.DATAFOLDER }, filter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + var linesAfter = File.ReadAllLines(parsedResultFileAfter); + Assert.AreEqual(1, linesAfter.Length); + Assert.AreEqual(expectedFilter, linesAfter[0]); + + var linesPostBackup = File.ReadAllLines(parsedResultFilePostBackup); + Assert.AreEqual(1, linesPostBackup.Length); + Assert.AreEqual(expectedFilter, linesPostBackup[0]); + } + private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0, List customCommands = null) { var id = Guid.NewGuid().ToString("N").Substring(0, 6); @@ -403,13 +492,10 @@ namespace Duplicati.UnitTest } else { - var commands = new List(); - commands.Add("#!/bin/sh"); + var commands = new List() { "#!/bin/sh" }; if (customCommands != null) - { commands.AddRange(customCommands); - } if (!string.IsNullOrWhiteSpace(stdout)) commands.Add($@"echo {stdout}");