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}");