diff --git a/Duplicati/Agent/Program.cs b/Duplicati/Agent/Program.cs
index d5d0351c5..157621d3e 100644
--- a/Duplicati/Agent/Program.cs
+++ b/Duplicati/Agent/Program.cs
@@ -275,7 +275,7 @@ public static class Program
using var cts = new CancellationTokenSource();
- var target = new ControllerMultiLogTarget(new ConsoleLogDestination(), LogMessageType.Information, null);
+ var target = new ControllerMultiLogTarget(new ConsoleLogDestination(), LogMessageType.Information, null, null);
using (Log.StartScope(target))
{
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(agentConfig.WindowsEventLog))
diff --git a/Duplicati/Library/Backend/SMB/SMBBackend.cs b/Duplicati/Library/Backend/SMB/SMBBackend.cs
index 4c8cfadac..7c8e538f3 100644
--- a/Duplicati/Library/Backend/SMB/SMBBackend.cs
+++ b/Duplicati/Library/Backend/SMB/SMBBackend.cs
@@ -150,7 +150,7 @@ public class SMBBackend : IStreamingBackend, IFolderEnabledBackend
var writeBufferSizeConfig = options.GetValueOrDefault(WRITE_BUFFER_SIZE_OPTION);
if (!string.IsNullOrWhiteSpace(writeBufferSizeConfig))
- writeBufferSize = int.TryParse(readBufferSizeConfig, out int value) ? value : null;
+ writeBufferSize = int.TryParse(writeBufferSizeConfig, out int value) ? value : null;
// Normalize to 10KB minimum buffers size
readBufferSize = readBufferSize < 1024 * 10 ? null : readBufferSize;
diff --git a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs
index 0ebe27c2f..555a43cd7 100644
--- a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs
+++ b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs
@@ -422,7 +422,7 @@ partial class BackendManager
}
// Refresh DNS name if we fail to connect in order to prevent issues with incorrect DNS entries
- var dnsFailure = Library.Utility.Utility.FlattenException(ex).Any(x => x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure);
+ var dnsFailure = Library.Utility.ExceptionExtensions.FlattenException(ex).Any(x => x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure);
if (dnsFailure)
{
try
@@ -443,7 +443,7 @@ partial class BackendManager
var recovered = false;
// Check if this was a folder missing exception and we are allowed to autocreate folders
- if (!(anyDownloaded || anyUploaded) && context.Options.AutocreateFolders && Library.Utility.Utility.FlattenException(ex).Any(x => x is FolderMissingException))
+ if (!(anyDownloaded || anyUploaded) && context.Options.AutocreateFolders && Library.Utility.ExceptionExtensions.FlattenException(ex).Any(x => x is FolderMissingException))
{
if (await TryCreateFolder().ConfigureAwait(false))
recovered = true;
diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs
index 3e242612d..873f51858 100644
--- a/Duplicati/Library/Main/Controller.cs
+++ b/Duplicati/Library/Main/Controller.cs
@@ -478,7 +478,7 @@ namespace Duplicati.Library.Main
{
OnOperationStarted?.Invoke(result);
var resultSetter = result as ISetCommonOptions;
- using (var logTarget = new ControllerMultiLogTarget(result, Logging.LogMessageType.Information, null))
+ using (var logTarget = new ControllerMultiLogTarget(result, Logging.LogMessageType.Information, null, m_options.SuppressWarningsFilter))
using (Logging.Log.StartScope(logTarget, null))
{
logTarget.AddTarget(m_messageSink, m_options.ConsoleLoglevel, m_options.ConsoleLogFilter);
diff --git a/Duplicati/Library/Main/ControllerMultiLogTarget.cs b/Duplicati/Library/Main/ControllerMultiLogTarget.cs
index fe8ddc464..67484b3f0 100644
--- a/Duplicati/Library/Main/ControllerMultiLogTarget.cs
+++ b/Duplicati/Library/Main/ControllerMultiLogTarget.cs
@@ -1,24 +1,26 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// 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
+// Copyright (C) 2025, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// 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.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -29,12 +31,25 @@ namespace Duplicati.Library.Main
///
/// The log target handler for the controller, which sends to multiple log targets
///
- public class ControllerMultiLogTarget : Logging.ILogDestination, IDisposable
+ public class ControllerMultiLogTarget : ILogDestination, IDisposable
{
+ ///
+ /// The target entry for the log target
+ ///
+ /// The log target.
+ /// The minimum log level to consider
+ /// The log filter.
+ private sealed record TargetEntry(ILogDestination Target, LogMessageType Level, Library.Utility.IFilter Filter);
+
///
/// The list of log targets to handle
///
- private readonly List> m_targets = new List>();
+ private readonly List m_targets = new List();
+
+ ///
+ /// The filter used to suppress warnings
+ ///
+ private readonly HashSet? m_supressWarningFilter;
///
/// Initializes a new instance of the class.
@@ -42,8 +57,9 @@ namespace Duplicati.Library.Main
/// The log target.
/// The minimum log level to consider
/// The log filter.
- public ControllerMultiLogTarget(ILogDestination target, Logging.LogMessageType loglevel, Library.Utility.IFilter filter)
+ public ControllerMultiLogTarget(ILogDestination target, Logging.LogMessageType loglevel, Library.Utility.IFilter? filter, HashSet? supressWarningFilter)
{
+ m_supressWarningFilter = supressWarningFilter;
AddTarget(target, loglevel, filter);
}
@@ -53,12 +69,12 @@ namespace Duplicati.Library.Main
/// The log target.
/// The minimum log level to consider
/// The log filter.
- public void AddTarget(ILogDestination target, LogMessageType loglevel, Library.Utility.IFilter filter)
+ public void AddTarget(ILogDestination target, LogMessageType loglevel, Library.Utility.IFilter? filter)
{
if (target == null)
return;
- m_targets.Add(new Tuple(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
+ m_targets.Add(new TargetEntry(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
}
///
@@ -73,7 +89,7 @@ namespace Duplicati.Library.Main
public void Dispose()
{
foreach (var m in m_targets)
- (m.Item1 as IDisposable)?.Dispose();
+ (m.Target as IDisposable)?.Dispose();
m_targets.Clear();
}
@@ -81,7 +97,7 @@ namespace Duplicati.Library.Main
/// Gets the minimum log level of all the targets
///
public LogMessageType MinimumLevel
- => m_targets.Select(x => x.Item2).DefaultIfEmpty(LogMessageType.Error).Min();
+ => m_targets.Select(x => x.Level).DefaultIfEmpty(LogMessageType.Error).Min();
///
/// Writes the message to all the destinations.
@@ -89,9 +105,13 @@ namespace Duplicati.Library.Main
/// Entry.
public void WriteMessage(LogEntry entry)
{
+ // If the message is a warning, check if we should suppress it
+ if (m_supressWarningFilter != null && entry.Level == LogMessageType.Warning && m_supressWarningFilter.Contains(entry.Id))
+ entry = new LogEntry(entry.Message, entry.Arguments, LogMessageType.Information, entry.Tag, entry.Id, entry.Exception);
+
foreach (var e in m_targets)
{
- var found = e.Item3.Matches(entry.FilterTag, out var result, out var match);
+ var found = e.Filter.Matches(entry.FilterTag, out var result, out var _);
// If there is a filter match, use that
if (found)
@@ -102,12 +122,12 @@ namespace Duplicati.Library.Main
else
{
// Otherwise, filter by log-level
- if (entry.Level < e.Item2)
+ if (entry.Level < e.Level)
continue;
}
// If we get here, write the message
- e.Item1.WriteMessage(entry);
+ e.Target.WriteMessage(entry);
}
}
}
diff --git a/Duplicati/Library/Main/Operation/Backup/FileBlockProcessor.cs b/Duplicati/Library/Main/Operation/Backup/FileBlockProcessor.cs
index 02a983aac..b404232f0 100644
--- a/Duplicati/Library/Main/Operation/Backup/FileBlockProcessor.cs
+++ b/Duplicati/Library/Main/Operation/Backup/FileBlockProcessor.cs
@@ -142,8 +142,8 @@ namespace Duplicati.Library.Main.Operation.Backup
{
if (ex.IsRetiredException())
return;
- else
- Logging.Log.WriteWarningMessage(FILELOGTAG, "PathProcessingFailed", ex, "Failed to process path: {0}", e.Entry.Path);
+
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FileProcessingFailed", e.Entry.Path);
}
}
});
diff --git a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs
index 848610186..1f655d423 100644
--- a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs
+++ b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs
@@ -96,7 +96,6 @@ namespace Duplicati.Library.Main.Operation.Backup
if (ignorenames != null && ignorenames.Length == 0)
ignorenames = null;
-
// Shared filter function with bound variables
ValueTask FilterEntry(ISourceProviderEntry entry)
=> SourceFileEntryFilter(entry, blacklistPaths, hardlinkPolicy, symlinkPolicy, hardlinkmap, fileAttributeFilter, enumeratefilter, ignorenames, mixinqueue, token);
@@ -303,7 +302,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorEnumerate", ex, "Failed to enumerate path: {0}", e.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorEnumerate", e.Path, "Failed to enumerate path: {0}");
}
}
}
@@ -369,7 +368,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorBlockDevice", ex, "Failed to process path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorBlockDevice", entry.Path);
return false;
}
@@ -384,7 +383,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorCharacterDevice", ex, "Failed to process path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorCharacterDevice", entry.Path);
return false;
}
@@ -444,7 +443,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorHardLink", ex, "Failed to process path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorHardLink", entry.Path);
return false;
}
}
@@ -465,7 +464,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorIgnoreFile", ex, "Failed to process path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorIgnoreFile", entry.Path);
}
}
@@ -480,7 +479,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorAttributes", ex, "Failed to process path, using default attributes: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorAttributes", entry.Path, "Failed to process path, using default attributes: {0}");
}
// If we exclude files based on attributes, filter that
@@ -511,7 +510,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteExplicitMessage(FILTER_LOGTAG, "SymlinkTargetReadError", ex, "Failed to read symlink target for path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "SymlinkTargetReadError", entry.Path, "Failed to read symlink target for path: {0}");
}
if (symlinkTarget != null)
diff --git a/Duplicati/Library/Main/Operation/Backup/FilePreFilterProcess.cs b/Duplicati/Library/Main/Operation/Backup/FilePreFilterProcess.cs
index 1574bff60..744606614 100644
--- a/Duplicati/Library/Main/Operation/Backup/FilePreFilterProcess.cs
+++ b/Duplicati/Library/Main/Operation/Backup/FilePreFilterProcess.cs
@@ -77,7 +77,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteExplicitMessage(FILELOGTAG, "FailedToReadSize", ex, "Failed to read size of file: {0}", e.Entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FailedToReadSize", e.Entry.Path, "Failed to read size on \"{0}\"");
}
await stats.AddExaminedFile(filestatsize);
diff --git a/Duplicati/Library/Main/Operation/Backup/LogExceptionHelper.cs b/Duplicati/Library/Main/Operation/Backup/LogExceptionHelper.cs
new file mode 100644
index 000000000..c7125042c
--- /dev/null
+++ b/Duplicati/Library/Main/Operation/Backup/LogExceptionHelper.cs
@@ -0,0 +1,57 @@
+// Copyright (C) 2025, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// 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.
+
+#nullable enable
+
+using System;
+using Duplicati.Library.Logging;
+using Duplicati.Library.Utility;
+
+namespace Duplicati.Library.Main.Operation.Backup;
+
+///
+/// Helper class to log path exceptions in a common way
+///
+public static class LogExceptionHelper
+{
+ ///
+ /// Logs a path warning message, using exception detection for the appropriate message
+ ///
+ /// The exception to log
+ /// The log tag to use for the message
+ /// The id to use for the message
+ /// The path to use for the message
+ /// The message to log
+ public static void LogCommonWarning(Exception? ex, string logtag, string id, string path, string message = "Failed to process path: {0}")
+ {
+ if (ex.IsPermissionDeniedException())
+ Log.WriteWarningMessage(logtag, "PermissionDenied", ex, "Excluding path due to permission denied: {0}", path);
+ else if (ex.IsFileLockedException())
+ Log.WriteWarningMessage(logtag, "FileLocked", ex, "Excluding path due to file locked: {0}", path);
+ else if (ex.IsPathNotFoundException())
+ Log.WriteWarningMessage(logtag, "PathNotFound", ex, "Excluding path due to path not found: {0}", path);
+ else if (ex.IsPathTooLongException())
+ Log.WriteWarningMessage(logtag, "PathTooLong", ex, "Excluding path due to path too long: {0}", path);
+ else
+ Log.WriteWarningMessage(logtag, id, ex, message, path);
+
+ }
+}
\ No newline at end of file
diff --git a/Duplicati/Library/Main/Operation/Backup/MetadataGenerator.cs b/Duplicati/Library/Main/Operation/Backup/MetadataGenerator.cs
index 6be52d47c..daaad0441 100644
--- a/Duplicati/Library/Main/Operation/Backup/MetadataGenerator.cs
+++ b/Duplicati/Library/Main/Operation/Backup/MetadataGenerator.cs
@@ -55,7 +55,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(METALOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
}
}
@@ -67,7 +67,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(METALOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
}
}
}
@@ -80,7 +80,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(METALOGTAG, "MetadataProcessFailed", ex, "Failed to process metadata for \"{0}\", storing empty metadata", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "MetadataProcessFailed", entry.Path, "Failed to process metadata on \"{0}\", storing empty metadata");
return new Dictionary();
}
}
diff --git a/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs b/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs
index 2ae10faa1..defdee0bb 100644
--- a/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs
+++ b/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs
@@ -98,7 +98,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILELOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
}
try
@@ -107,7 +107,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteVerboseMessage(FILELOGTAG, "FailedAttributeRead", "Failed to read attributes from {0}: {1}", entry.Path, ex.Message);
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "AttributeReadFailed", entry.Path, "Failed to read attributes on \"{0}\"");
}
// If we only have metadata, stop here
@@ -166,8 +166,7 @@ namespace Duplicati.Library.Main.Operation.Backup
if (ex.IsRetiredException())
continue;
- Logging.Log.WriteWarningMessage(FILELOGTAG, "ProcessingMetadataFailed", ex,
- "Failed to process entry, path: {0}", entry.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "ProcessingMetadataFailed", entry.Path);
}
}
}
diff --git a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs
index 449ee9cfb..6d63dfd91 100644
--- a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs
+++ b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs
@@ -1,22 +1,22 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// 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
+// Copyright (C) 2025, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// 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 System;
@@ -25,10 +25,8 @@ using Duplicati.Library.Main.Operation.Common;
using System.Threading.Tasks;
using System.Collections.Generic;
using Duplicati.Library.Utility;
-using System.Linq;
using Duplicati.Library.Interface;
using System.IO;
-using System.Security.Cryptography;
namespace Duplicati.Library.Main.Operation.Backup
{
@@ -87,7 +85,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
catch (Exception ex)
{
- Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path);
+ LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FileLengthFailure", e.Path, "Failed to read file length for \"{0}\"");
}
if (e.IsMetadata && fslen > maxmetadatasize)
diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs
index 18312efaa..3bb038d54 100644
--- a/Duplicati/Library/Main/Options.cs
+++ b/Duplicati/Library/Main/Options.cs
@@ -19,6 +19,8 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
+#nullable enable
+
using System;
using System.Linq;
using System.Collections.Generic;
@@ -28,6 +30,7 @@ using System.Globalization;
using System.Threading;
using Duplicati.Library.Utility.Options;
using Duplicati.Library.SQLiteHelper;
+using System.Diagnostics.CodeAnalysis;
namespace Duplicati.Library.Main
{
@@ -81,6 +84,31 @@ namespace Duplicati.Library.Main
///
private const string DEFAULT_READ_WRITE_TIMEOUT = "30s";
+ ///
+ /// The default asynchronous upload limit
+ ///
+ private const int DEFAULT_ASYNCHRONOUS_UPLOAD_LIMIT = 4;
+
+ ///
+ /// The default number of concurrent uploads
+ ///
+ private const int DEFAULT_ASYNCHRONOUS_CONCURRENT_UPLOAD_LIMIT = 4;
+
+ ///
+ /// The default retry delay
+ ///
+ private const string DEFAULT_RETRY_DELAY = "10s";
+
+ ///
+ /// The default number of retries
+ ///
+ private const int DEFAULT_NUMBER_OF_RETRIES = 5;
+
+ ///
+ /// The default number of backup test samples
+ ///
+ private const long DEFAULT_BACKUP_TEST_SAMPLES = 1;
+
///
/// The default number of compressor instances
///
@@ -256,34 +284,28 @@ namespace Duplicati.Library.Main
///
protected readonly object m_lock = new object();
- protected readonly Dictionary m_options;
+ protected readonly Dictionary m_options;
protected readonly List> m_loadedModules = new List>();
///
/// Lookup table for compression hints
///
- private Dictionary m_compressionHints;
+ private Dictionary? m_compressionHints;
- public Options(Dictionary options)
+ public Options(Dictionary options)
{
m_options = options;
}
- public Dictionary RawOptions { get { return m_options; } }
+ public Dictionary RawOptions => m_options;
///
/// Returns a list of strings that are not supported on the commandline as options, but used internally
///
- public static string[] InternalOptions
- {
- get
- {
- return new string[] {
+ public static string[] InternalOptions => [
"main-action"
- };
- }
- }
+ ];
///
/// Returns a list of options that are intentionally duplicate
@@ -297,13 +319,7 @@ namespace Duplicati.Library.Main
///
/// A default backup name
///
- public static string DefaultBackupName
- {
- get
- {
- return System.IO.Path.GetFileNameWithoutExtension(Library.Utility.Utility.getEntryAssembly().Location);
- }
- }
+ public static string DefaultBackupName => System.IO.Path.GetFileNameWithoutExtension(Library.Utility.Utility.getEntryAssembly().Location);
///
/// Gets all supported commands
@@ -339,13 +355,13 @@ namespace Duplicati.Library.Main
new CommandLineArgument("passphrase", CommandLineArgument.ArgumentType.Password, Strings.Options.PassphraseShort, Strings.Options.PassphraseLong),
new CommandLineArgument("no-encryption", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NoencryptionShort, Strings.Options.NoencryptionLong, "false"),
- new CommandLineArgument("number-of-retries", CommandLineArgument.ArgumentType.Integer, Strings.Options.NumberofretriesShort, Strings.Options.NumberofretriesLong, "5"),
- new CommandLineArgument("retry-delay", CommandLineArgument.ArgumentType.Timespan, Strings.Options.RetrydelayShort, Strings.Options.RetrydelayLong, "10s"),
+ new CommandLineArgument("number-of-retries", CommandLineArgument.ArgumentType.Integer, Strings.Options.NumberofretriesShort, Strings.Options.NumberofretriesLong, DEFAULT_NUMBER_OF_RETRIES.ToString()),
+ new CommandLineArgument("retry-delay", CommandLineArgument.ArgumentType.Timespan, Strings.Options.RetrydelayShort, Strings.Options.RetrydelayLong, DEFAULT_RETRY_DELAY),
new CommandLineArgument("retry-with-exponential-backoff", CommandLineArgument.ArgumentType.Boolean, Strings.Options.RetrywithexponentialbackoffShort, Strings.Options.RetrywithexponentialbackoffLong, "false"),
new CommandLineArgument("synchronous-upload", CommandLineArgument.ArgumentType.Boolean, Strings.Options.SynchronousuploadShort, Strings.Options.SynchronousuploadLong, "false"),
- new CommandLineArgument("asynchronous-upload-limit", CommandLineArgument.ArgumentType.Integer, Strings.Options.AsynchronousuploadlimitShort, Strings.Options.AsynchronousuploadlimitLong, "4"),
- new CommandLineArgument("asynchronous-concurrent-upload-limit", CommandLineArgument.ArgumentType.Integer, Strings.Options.AsynchronousconcurrentuploadlimitShort, Strings.Options.AsynchronousconcurrentuploadlimitLong, "4"),
+ new CommandLineArgument("asynchronous-upload-limit", CommandLineArgument.ArgumentType.Integer, Strings.Options.AsynchronousuploadlimitShort, Strings.Options.AsynchronousuploadlimitLong, DEFAULT_ASYNCHRONOUS_UPLOAD_LIMIT.ToString()),
+ new CommandLineArgument("asynchronous-concurrent-upload-limit", CommandLineArgument.ArgumentType.Integer, Strings.Options.AsynchronousconcurrentuploadlimitShort, Strings.Options.AsynchronousconcurrentuploadlimitLong, DEFAULT_ASYNCHRONOUS_CONCURRENT_UPLOAD_LIMIT.ToString()),
new CommandLineArgument("asynchronous-upload-folder", CommandLineArgument.ArgumentType.Path, Strings.Options.AsynchronousuploadfolderShort, Strings.Options.AsynchronousuploadfolderLong, System.IO.Path.GetTempPath()),
new CommandLineArgument("disable-streaming-transfers", CommandLineArgument.ArgumentType.Boolean, Strings.Options.DisableStreamingShort, Strings.Options.DisableStreamingLong, "false"),
@@ -375,11 +391,14 @@ namespace Duplicati.Library.Main
new CommandLineArgument("log-file", CommandLineArgument.ArgumentType.Path, Strings.Options.LogfileShort, Strings.Options.LogfileLong),
new CommandLineArgument("log-file-log-level", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.LogfileloglevelShort, Strings.Options.LogfileloglevelLong, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))),
new CommandLineArgument("log-file-log-filter", CommandLineArgument.ArgumentType.String, Strings.Options.LogfilelogfiltersShort, Strings.Options.LogfilelogfiltersLong(System.IO.Path.PathSeparator.ToString()), null),
+ new CommandLineArgument("log-file-log-ignore", CommandLineArgument.ArgumentType.String, Strings.Options.LogfilelogignoreShort, Strings.Options.LogfilelogignoreLong(System.IO.Path.PathSeparator.ToString()), null),
new CommandLineArgument("console-log-level", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.ConsoleloglevelShort, Strings.Options.ConsoleloglevelLong, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))),
new CommandLineArgument("console-log-filter", CommandLineArgument.ArgumentType.String, Strings.Options.ConsolelogfiltersShort, Strings.Options.ConsolelogfiltersLong(System.IO.Path.PathSeparator.ToString()), null),
+ new CommandLineArgument("console-log-ignore", CommandLineArgument.ArgumentType.String, Strings.Options.ConsolelogignoreShort, Strings.Options.ConsolelogignoreLong(System.IO.Path.PathSeparator.ToString()), null),
new CommandLineArgument("log-level", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.LoglevelShort, Strings.Options.LoglevelLong, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)), Strings.Options.LogLevelDeprecated("log-file-log-level", "console-log-level")),
+ new CommandLineArgument("suppress-warnings", CommandLineArgument.ArgumentType.String, Strings.Options.SuppresswarningsShort, Strings.Options.SuppresswarningsLong),
new CommandLineArgument("profile-all-database-queries", CommandLineArgument.ArgumentType.Boolean, Strings.Options.ProfilealldatabasequeriesShort, Strings.Options.ProfilealldatabasequeriesLong, "false"),
@@ -422,7 +441,7 @@ namespace Duplicati.Library.Main
new CommandLineArgument("threshold", CommandLineArgument.ArgumentType.Integer, Strings.Options.ThresholdShort, Strings.Options.ThresholdLong, DEFAULT_THRESHOLD.ToString()),
new CommandLineArgument("index-file-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.IndexfilepolicyShort, Strings.Options.IndexfilepolicyLong, IndexFileStrategy.Full.ToString(), null, Enum.GetNames(typeof(IndexFileStrategy))),
new CommandLineArgument("no-backend-verification", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NobackendverificationShort, Strings.Options.NobackendverificationLong, "false"),
- new CommandLineArgument("backup-test-samples", CommandLineArgument.ArgumentType.Integer, Strings.Options.BackendtestsamplesShort, Strings.Options.BackendtestsamplesLong("no-backend-verification"), "1"),
+ new CommandLineArgument("backup-test-samples", CommandLineArgument.ArgumentType.Integer, Strings.Options.BackendtestsamplesShort, Strings.Options.BackendtestsamplesLong("no-backend-verification"), DEFAULT_BACKUP_TEST_SAMPLES.ToString()),
new CommandLineArgument("backup-test-percentage", CommandLineArgument.ArgumentType.Decimal, Strings.Options.BackendtestpercentageShort, Strings.Options.BackendtestpercentageLong, "0.1"),
new CommandLineArgument("full-remote-verification", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.FullremoteverificationShort, Strings.Options.FullremoteverificationLong("no-backend-verification"), Enum.GetName(typeof(RemoteTestStrategy), RemoteTestStrategy.False), null, Enum.GetNames(typeof(RemoteTestStrategy))),
@@ -501,7 +520,13 @@ namespace Duplicati.Library.Main
///
public OperationMode MainAction
{
- get { return (OperationMode)Enum.Parse(typeof(OperationMode), m_options["main-action"]); }
+ get
+ {
+ var value = m_options.GetValueOrDefault("main-action");
+ return string.IsNullOrEmpty(value)
+ ? ((OperationMode)(-1))
+ : (OperationMode)Enum.Parse(typeof(OperationMode), value);
+ }
set { m_options["main-action"] = value.ToString(); }
}
@@ -512,11 +537,7 @@ namespace Duplicati.Library.Main
{
get
{
- string volsize;
- m_options.TryGetValue("dblock-size", out volsize);
- if (string.IsNullOrEmpty(volsize))
- volsize = DEFAULT_VOLUME_SIZE;
-
+ var volsize = GetString("dblock-size", DEFAULT_VOLUME_SIZE);
#if DEBUG
return Math.Max(1024 * 10, Library.Utility.Sizeparser.ParseSize(volsize, "mb"));
#else
@@ -532,46 +553,38 @@ namespace Duplicati.Library.Main
{
get
{
- if (!m_options.ContainsKey("skip-files-larger-than") || string.IsNullOrEmpty(m_options["skip-files-larger-than"]))
- return long.MaxValue;
- else
- return Library.Utility.Sizeparser.ParseSize(m_options["skip-files-larger-than"], "mb");
+ var value = m_options.GetValueOrDefault("skip-files-larger-than");
+ return string.IsNullOrWhiteSpace(value)
+ ? long.MaxValue
+ : Library.Utility.Sizeparser.ParseSize(value, "mb");
}
}
///
/// A value indicating if orphan files are deleted automatically
///
- public bool AutoCleanup { get { return GetBool("auto-cleanup"); } }
+ public bool AutoCleanup => GetBool("auto-cleanup");
///
/// A value indicating if we are running in unittest mode
///
- public bool UnittestMode { get { return GetBool("unittest-mode"); } }
+ public bool UnittestMode => GetBool("unittest-mode");
///
/// Gets a list of files to add to the signature volumes
///
- public string ControlFiles
- {
- get
- {
- string v;
- m_options.TryGetValue("control-files", out v);
- return v;
- }
- }
+ public string? ControlFiles => m_options.GetValueOrDefault("control-files");
///
/// A value indicating if file hash checks are skipped
///
- public bool SkipFileHashChecks { get { return GetBool("skip-file-hash-checks"); } }
+ public bool SkipFileHashChecks => GetBool("skip-file-hash-checks");
///
/// A value indicating if the manifest files are not read
///
- public bool DontReadManifests { get { return GetBool("dont-read-manifests"); } }
+ public bool DontReadManifests => GetBool("dont-read-manifests");
///
/// Gets the backup that should be restored
@@ -580,22 +593,21 @@ namespace Duplicati.Library.Main
{
get
{
- if (!m_options.ContainsKey("time") || string.IsNullOrEmpty(m_options["time"]))
- return new DateTime(0, DateTimeKind.Utc);
- else
- return Library.Utility.Timeparser.ParseTimeInterval(m_options["time"], DateTime.Now);
+ var value = m_options.GetValueOrDefault("time");
+ return string.IsNullOrEmpty(value)
+ ? new DateTime(0, DateTimeKind.Utc)
+ : Library.Utility.Timeparser.ParseTimeInterval(value, DateTime.Now);
}
}
///
/// Gets the versions the restore or list operation is limited to
///
- public long[] Version
+ public long[]? Version
{
get
{
- string v;
- m_options.TryGetValue("version", out v);
+ m_options.TryGetValue("version", out var v);
if (string.IsNullOrEmpty(v))
return null;
@@ -622,32 +634,32 @@ namespace Duplicati.Library.Main
///
/// A value indicating if all versions are listed
///
- public bool AllVersions { get { return GetBool("all-versions"); } }
+ public bool AllVersions => GetBool("all-versions");
///
/// A value indicating if only the largest common prefix is returned
///
- public bool ListPrefixOnly { get { return GetBool("list-prefix-only"); } }
+ public bool ListPrefixOnly => GetBool("list-prefix-only");
///
/// A value indicating if only folder contents are returned
///
- public bool ListFolderContents { get { return GetBool("list-folder-contents"); } }
+ public bool ListFolderContents => GetBool("list-folder-contents");
///
/// A value indicating that only filesets are returned
///
- public bool ListSetsOnly { get { return GetBool("list-sets-only"); } }
+ public bool ListSetsOnly => GetBool("list-sets-only");
///
/// A value indicating if file time checks are skipped
///
- public bool DisableFiletimeCheck { get { return GetBool("disable-filetime-check"); } }
+ public bool DisableFiletimeCheck => GetBool("disable-filetime-check");
///
/// A value indicating if file time checks are skipped
///
- public bool CheckFiletimeOnly { get { return GetBool("check-filetime-only"); } }
+ public bool CheckFiletimeOnly => GetBool("check-filetime-only");
///
/// A value indicating if USN numbers are used to get list of changed files
@@ -657,33 +669,22 @@ namespace Duplicati.Library.Main
///
/// A value indicating if time tolerance is disabled
///
- public bool DisableTimeTolerance { get { return GetBool("disable-time-tolerance"); } }
+ public bool DisableTimeTolerance => GetBool("disable-time-tolerance");
///
/// Gets a value indicating whether a temporary folder has been specified
///
- public bool HasTempDir { get { return m_options.ContainsKey("tempdir") && !string.IsNullOrEmpty(m_options["tempdir"]); } }
+ public bool HasTempDir => m_options.ContainsKey("tempdir") && !string.IsNullOrEmpty(m_options["tempdir"]);
///
/// Gets the folder where temporary files are stored
///
- public string TempDir
- {
- get
- {
- if (!m_options.ContainsKey("tempdir") || string.IsNullOrEmpty(m_options["tempdir"]))
- {
- return Duplicati.Library.Utility.TempFolder.SystemTempPath;
- }
-
- return m_options["tempdir"];
- }
- }
+ public string TempDir => GetString("tempdir", TempFolder.SystemTempPath);
///
/// Gets a value indicating whether the user has forced the locale
///
- public bool HasForcedLocale { get { return m_options.ContainsKey("force-locale"); } }
+ public bool HasForcedLocale => m_options.ContainsKey("force-locale");
///
/// Gets the forced locale for the current user
@@ -706,39 +707,17 @@ namespace Duplicati.Library.Main
///
/// A value indicating if missing folders should be created automatically
///
- public bool AutocreateFolders { get { return !GetBool("disable-autocreate-folder"); } }
+ public bool AutocreateFolders => !GetBool("disable-autocreate-folder");
///
/// Gets the backup prefix
///
- public string Prefix
- {
- get
- {
- string v;
- m_options.TryGetValue("prefix", out v);
- if (!string.IsNullOrEmpty(v))
- return v;
-
- return "duplicati";
- }
- }
+ public string Prefix => GetString("prefix", "duplicati");
///
/// Gets the number of old backups to keep
///
- public int KeepVersions
- {
- get
- {
- string v;
- m_options.TryGetValue("keep-versions", out v);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_KEEP_VERSIONS;
-
- return Math.Max(0, int.Parse(v));
- }
- }
+ public int KeepVersions => Math.Max(0, GetInt("keep-versions", DEFAULT_KEEP_VERSIONS));
///
/// Gets the timelimit for removal
@@ -747,13 +726,12 @@ namespace Duplicati.Library.Main
{
get
{
- string v;
- m_options.TryGetValue("keep-time", out v);
+ m_options.TryGetValue("keep-time", out var v);
if (string.IsNullOrEmpty(v))
return new DateTime(0);
- TimeSpan tolerance =
+ var tolerance =
this.DisableTimeTolerance ?
TimeSpan.FromSeconds(0) :
TimeSpan.FromSeconds(Math.Min(Library.Utility.Timeparser.ParseTimeSpan(v).TotalSeconds / 100, 60.0 * 60.0));
@@ -771,19 +749,14 @@ namespace Duplicati.Library.Main
{
var retentionPolicyConfig = new List();
- string v;
- m_options.TryGetValue("retention-policy", out v);
+ m_options.TryGetValue("retention-policy", out var v);
if (string.IsNullOrEmpty(v))
- {
return retentionPolicyConfig;
- }
var periodIntervalStrings = v.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var periodIntervalString in periodIntervalStrings)
- {
retentionPolicyConfig.Add(RetentionPolicyValue.CreateFromString(periodIntervalString));
- }
return retentionPolicyConfig;
}
@@ -792,95 +765,47 @@ namespace Duplicati.Library.Main
///
/// Gets the encryption passphrase
///
- public string Passphrase
- {
- get
- {
- if (!m_options.ContainsKey("passphrase") || string.IsNullOrEmpty(m_options["passphrase"]))
- return null;
- else
- return m_options["passphrase"];
- }
- }
+ public string? Passphrase => GetString("passphrase", null);
///
/// A value indicating if backups are not encrypted
///
- public bool NoEncryption { get { return GetBool("no-encryption"); } }
+ public bool NoEncryption => GetBool("no-encryption");
///
/// Gets the module used for encryption
///
- public string EncryptionModule
- {
- get
- {
- //Disabled?
- if (NoEncryption)
- return null;
-
- //Specified?
- if (m_options.ContainsKey("encryption-module"))
- return m_options["encryption-module"];
-
- return "aes";
- }
- }
+ public string? EncryptionModule => NoEncryption ? null : GetString("encryption-module", "aes");
///
/// Gets the module used for compression
///
- public string CompressionModule
- {
- get
- {
- if (m_options.ContainsKey("compression-module"))
- return m_options["compression-module"];
- else
- return "zip";
- }
- }
-
+ public string CompressionModule => GetString("compression-module", "zip");
///
/// Gets the number of time to retry transmission if it fails
///
- public int NumberOfRetries
- {
- get
- {
- if (!m_options.ContainsKey("number-of-retries") || string.IsNullOrEmpty(m_options["number-of-retries"]))
- return 5;
- else
- {
- int x = int.Parse(m_options["number-of-retries"]);
- if (x < 0)
- throw new UserInformationException("Invalid count for number-of-retries", "NumberOfRetriesInvalid");
-
- return x;
- }
- }
- }
+ public int NumberOfRetries => Math.Max(0, GetInt("number-of-retries", DEFAULT_NUMBER_OF_RETRIES));
///
/// A value indicating if backups are transmitted on a separate thread
///
- public bool SynchronousUpload { get { return Library.Utility.Utility.ParseBoolOption(m_options, "synchronous-upload"); } }
+ public bool SynchronousUpload => GetBool("synchronous-upload");
///
/// A value indicating if system is allowed to enter sleep power states during backup/restore
///
- public bool AllowSleep { get { return GetBool("allow-sleep"); } }
+ public bool AllowSleep => GetBool("allow-sleep");
///
/// A value indicating if system should use the low-priority IO during backup/restore
///
- public bool UseBackgroundIOPriority { get { return GetBool("use-background-io-priority"); } }
+ public bool UseBackgroundIOPriority => GetBool("use-background-io-priority");
///
/// A value indicating if use of the streaming interface is disallowed
///
- public bool DisableStreamingTransfers { get { return GetBool("disable-streaming-transfers"); } }
+ public bool DisableStreamingTransfers => GetBool("disable-streaming-transfers");
///
/// The maximum time to allow inactivity before a connection is closed.
@@ -890,39 +815,20 @@ namespace Duplicati.Library.Main
{
get
{
- var v = m_options.GetValueOrDefault("read-write-timeout");
- if (string.IsNullOrWhiteSpace(v))
- v = DEFAULT_READ_WRITE_TIMEOUT;
-
- var res = Library.Utility.Timeparser.ParseTimeSpan(v);
- if (res.Ticks <= 0)
- return Timeout.Infinite;
-
- return (int)res.TotalMilliseconds;
+ var ts = Library.Utility.Utility.ParseTimespanOption(m_options, "read-write-timeout", DEFAULT_READ_WRITE_TIMEOUT);
+ return ts.Ticks <= 0 ? Timeout.Infinite : (int)ts.TotalMilliseconds;
}
}
///
/// Gets the delay period to retry uploads
///
- public TimeSpan RetryDelay
- {
- get
- {
- if (!m_options.ContainsKey("retry-delay") || string.IsNullOrEmpty(m_options["retry-delay"]))
- return new TimeSpan(TimeSpan.TicksPerSecond * 10);
- else
- return Library.Utility.Timeparser.ParseTimeSpan(m_options["retry-delay"]);
- }
- }
+ public TimeSpan RetryDelay => Library.Utility.Utility.ParseTimespanOption(m_options, "retry-delay", DEFAULT_RETRY_DELAY);
///
/// Gets whether exponential backoff is enabled
///
- public Boolean RetryWithExponentialBackoff
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "retry-with-exponential-backoff"); }
- }
+ public bool RetryWithExponentialBackoff => GetBool("retry-with-exponential-backoff");
///
/// Gets the max upload speed in bytes pr. second
@@ -932,22 +838,12 @@ namespace Duplicati.Library.Main
get
{
lock (m_lock)
- {
- string v;
- m_options.TryGetValue("throttle-upload", out v);
- if (string.IsNullOrEmpty(v))
- return 0;
- else
- return Library.Utility.Sizeparser.ParseSize(v, "kb");
- }
+ return GetSize("throttle-upload", "kb", "0b");
}
set
{
lock (m_lock)
- if (value <= 0)
- m_options["throttle-upload"] = "";
- else
- m_options["throttle-upload"] = value.ToString() + "b";
+ m_options["throttle-upload"] = value <= 0 ? "" : $"{value}b";
}
}
@@ -959,104 +855,55 @@ namespace Duplicati.Library.Main
get
{
lock (m_lock)
- {
- string v;
- m_options.TryGetValue("throttle-download", out v);
- if (string.IsNullOrEmpty(v))
- return 0;
- else
- return Library.Utility.Sizeparser.ParseSize(v, "kb");
- }
+ return GetSize("throttle-download", "kb", "0b");
}
set
{
lock (m_lock)
- if (value <= 0)
- m_options["throttle-download"] = "";
- else
- m_options["throttle-download"] = value.ToString() + "b";
+ m_options["throttle-download"] = value <= 0 ? "" : $"{value}b";
}
}
///
/// A value indicating if the backup is a full backup
///
- public bool AllowFullRemoval { get { return GetBool("allow-full-removal"); } }
+ public bool AllowFullRemoval => GetBool("allow-full-removal");
///
/// A value indicating if debug output is enabled
///
- public bool DebugOutput { get { return GetBool("debug-output"); } }
+ public bool DebugOutput => GetBool("debug-output");
///
/// A value indicating if unchanged backups are uploaded
///
- public bool UploadUnchangedBackups { get { return GetBool("upload-unchanged-backups"); } }
+ public bool UploadUnchangedBackups => GetBool("upload-unchanged-backups");
///
/// Gets a list of modules that should be loaded
///
public string[] EnableModules
- {
- get
- {
- if (m_options.ContainsKey("enable-module"))
- return m_options["enable-module"].Trim().ToLower(CultureInfo.InvariantCulture).Split(',');
- else
- return new string[0];
- }
- }
+ => m_options.GetValueOrDefault("enable-module")?.Trim().ToLower(CultureInfo.InvariantCulture).Split(',') ?? [];
///
/// Gets a list of modules that should not be loaded
///
public string[] DisableModules
- {
- get
- {
- if (m_options.ContainsKey("disable-module"))
- return m_options["disable-module"].Trim().ToLower(CultureInfo.InvariantCulture).Split(',');
- else
- return new string[0];
- }
- }
+ => m_options.GetValueOrDefault("disable-module")?.Trim().ToLower(CultureInfo.InvariantCulture).Split(',') ?? [];
///
/// Gets the snapshot strategy to use
///
- public OptimizationStrategy SnapShotStrategy
- {
- get
- {
- string strategy;
- if (!m_options.TryGetValue("snapshot-policy", out strategy))
- strategy = "";
-
- OptimizationStrategy r;
- if (!Enum.TryParse(strategy, true, out r))
- r = OptimizationStrategy.Off;
-
- return r;
- }
- }
+ public OptimizationStrategy SnapShotStrategy => GetEnum("snapshot-policy", OptimizationStrategy.Off);
///
/// Gets the snapshot strategy to use
///
public Snapshots.SnapshotProvider SnapShotProvider
- {
- get
- {
- if (!m_options.TryGetValue("snapshot-provider", out var provider))
- provider = "";
-
- Snapshots.SnapshotProvider r;
- if (!Enum.TryParse(provider, true, out r))
- r = OperatingSystem.IsWindows() ? Snapshots.SnapshotProvider.AlphaVSS : Snapshots.SnapshotProvider.LVM;
-
- return r;
- }
- }
+ => GetEnum("snapshot-provider",
+ OperatingSystem.IsWindows()
+ ? Snapshots.SnapshotProvider.AlphaVSS
+ : Snapshots.SnapshotProvider.LVM);
///
/// Gets a flag indicating if advisory locking should be ignored
@@ -1067,125 +914,42 @@ namespace Duplicati.Library.Main
/// Gets the symlink strategy to use
///
public SymlinkStrategy SymlinkPolicy
- {
- get
- {
- string policy;
- if (!m_options.TryGetValue("symlink-policy", out policy))
- policy = "";
-
- SymlinkStrategy r;
- if (!Enum.TryParse(policy, true, out r))
- r = SymlinkStrategy.Store;
-
- return r;
- }
- }
+ => GetEnum("symlink-policy", SymlinkStrategy.Store);
///
/// Gets the hardlink strategy to use
///
public HardlinkStrategy HardlinkPolicy
- {
- get
- {
- string policy;
- if (!m_options.TryGetValue("hardlink-policy", out policy))
- policy = "";
+ => GetEnum("hardlink-policy", HardlinkStrategy.All);
- HardlinkStrategy r;
- if (!Enum.TryParse(policy, true, out r))
- r = HardlinkStrategy.All;
-
- return r;
- }
- }
///
/// Gets the update sequence number (USN) strategy to use
///
public OptimizationStrategy UsnStrategy
- {
- get
- {
- string strategy;
- if (!m_options.TryGetValue("usn-policy", out strategy))
- strategy = "";
-
- OptimizationStrategy r;
- if (!Enum.TryParse(strategy, true, out r))
- r = OptimizationStrategy.Off;
-
- return r;
- }
- }
+ => GetEnum("usn-policy", OptimizationStrategy.Off);
///
/// Gets the number of concurrent volume uploads allowed. Zero for unlimited.
///
public int AsynchronousConcurrentUploadLimit
- {
- get
- {
- if (!m_options.TryGetValue("asynchronous-concurrent-upload-limit", out var value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return 4;
- else
- return int.Parse(value);
- }
- }
+ => GetInt("asynchronous-concurrent-upload-limit", DEFAULT_ASYNCHRONOUS_CONCURRENT_UPLOAD_LIMIT);
///
/// Gets the number of volumes to create ahead of time when using async transfers,
/// a value of zero indicates no limit
///
public long AsynchronousUploadLimit
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("asynchronous-upload-limit", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return 4;
- else
- return long.Parse(value);
- }
- }
+ => GetInt("asynchronous-upload-limit", DEFAULT_ASYNCHRONOUS_UPLOAD_LIMIT);
///
/// Gets the temporary folder to use for asynchronous transfers
///
- public string AsynchronousUploadFolder
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("asynchronous-upload-folder", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return this.TempDir;
- else
- return value;
- }
- }
+ public string AsynchronousUploadFolder => GetString("asynchronous-upload-folder", TempDir);
///
/// Gets the logfile filename
///
- public string Logfile
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("log-file", out value))
- value = null;
- return value;
- }
- }
+ public string? Logfile => m_options.GetValueOrDefault("log-file");
///
/// Gets the log-file detail level
@@ -1194,8 +958,7 @@ namespace Duplicati.Library.Main
{
get
{
- string value;
- if (!m_options.TryGetValue("log-file-log-level", out value))
+ if (!m_options.TryGetValue("log-file-log-level", out var value))
value = null;
if (string.IsNullOrWhiteSpace(value))
@@ -1213,31 +976,31 @@ namespace Duplicati.Library.Main
}
}
+ ///
+ /// Gets the filter used to suppress warning messages.
+ ///
+ public HashSet? SuppressWarningsFilter
+ => m_options.GetValueOrDefault("suppress-warnings")?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)?.ToHashSet(StringComparer.OrdinalIgnoreCase);
+
///
/// Gets the filter used for log-file messages.
///
/// The log file filter.
- public IFilter LogFileLogFilter
- {
- get
- {
- m_options.TryGetValue("log-file-log-filter", out var value);
- return Library.Utility.FilterExpression.ParseLogFilter(value);
- }
- }
+ public IFilter? LogFileLogFilter
+ => FilterExpression.Combine(
+ new FilterExpression(m_options.GetValueOrDefault("log-file-log-ignore")?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)?.Select(x => $"*-{x}"), false),
+ FilterExpression.ParseLogFilter(m_options.GetValueOrDefault("log-file-log-filter"))
+ );
///
/// Gets the filter used for console messages.
///
/// The log file filter.
- public IFilter ConsoleLogFilter
- {
- get
- {
- m_options.TryGetValue("console-log-filter", out var value);
- return Library.Utility.FilterExpression.ParseLogFilter(value);
- }
- }
+ public IFilter? ConsoleLogFilter
+ => FilterExpression.Combine(
+ new FilterExpression(m_options.GetValueOrDefault("console-log-ignore")?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)?.Select(x => $"*-{x}"), false),
+ FilterExpression.ParseLogFilter(m_options.GetValueOrDefault("console-log-filter"))
+ );
///
/// Gets the console log detail level
@@ -1246,8 +1009,7 @@ namespace Duplicati.Library.Main
{
get
{
- string value;
- if (!m_options.TryGetValue("console-log-level", out value))
+ if (!m_options.TryGetValue("console-log-level", out var value))
value = null;
if (string.IsNullOrWhiteSpace(value))
@@ -1268,50 +1030,33 @@ namespace Duplicati.Library.Main
///
/// A value indicating if all database queries should be logged
///
- public bool ProfileAllDatabaseQueries { get { return GetBool("profile-all-database-queries"); } }
+ public bool ProfileAllDatabaseQueries => GetBool("profile-all-database-queries");
///
/// Gets the attribute filter used to exclude files and folders.
///
public System.IO.FileAttributes FileAttributeFilter
- {
- get
- {
- System.IO.FileAttributes res = default(System.IO.FileAttributes);
- string v;
- if (!m_options.TryGetValue("exclude-files-attributes", out v))
- return res;
-
- foreach (string s in v.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries))
- {
- System.IO.FileAttributes f;
- if (Enum.TryParse(s.Trim(), true, out f))
- res |= f;
- }
-
- return res;
- }
- }
+ => Library.Utility.Utility.ParseFlagsOption(m_options, "exclude-files-attributes", default(System.IO.FileAttributes));
///
/// A value indicating if server uploads are verified by listing the folder contents
///
- public bool ListVerifyUploads { get { return GetBool("list-verify-uploads"); } }
+ public bool ListVerifyUploads => GetBool("list-verify-uploads");
///
/// A value indicating if connections cannot be re-used
///
- public bool NoConnectionReuse { get { return GetBool("no-connection-reuse"); } }
+ public bool NoConnectionReuse => GetBool("no-connection-reuse");
///
/// A value indicating if the returned value should not be truncated
///
- public bool FullResult { get { return GetBool("full-result"); } }
+ public bool FullResult => GetBool("full-result");
///
/// A value indicating restored files overwrite existing ones
///
- public bool Overwrite { get { return GetBool("overwrite"); } }
+ public bool Overwrite => GetBool("overwrite");
///
/// Gets the total size in bytes that the backup should use, returns -1 if there is no upper limit
@@ -1320,10 +1065,8 @@ namespace Duplicati.Library.Main
{
get
{
- if (!m_options.ContainsKey("quota-size") || string.IsNullOrEmpty(m_options["quota-size"]))
- return -1;
- else
- return Library.Utility.Sizeparser.ParseSize(m_options["quota-size"], "mb");
+ var value = m_options.GetValueOrDefault("quota-size");
+ return string.IsNullOrEmpty(value) ? -1 : Library.Utility.Sizeparser.ParseSize(value, "mb");
}
}
@@ -1334,96 +1077,46 @@ namespace Duplicati.Library.Main
/// This is treated as a percentage, where a warning is given when the amount of free space is less than this percentage of the backup size.
///
public int QuotaWarningThreshold
- {
- get
- {
- string tmp;
- m_options.TryGetValue("quota-warning-threshold", out tmp);
- if (string.IsNullOrEmpty(tmp))
- {
- return DEFAULT_QUOTA_WARNING_THRESHOLD;
- }
- else
- {
- return int.Parse(tmp);
- }
- }
- }
+ => GetInt("quota-warning-threshold", DEFAULT_QUOTA_WARNING_THRESHOLD);
///
/// Gets a flag indicating that backup quota reported by the backend should be ignored
///
/// This is necessary because in some cases the backend might report a wrong quota (especially with some Linux mounts).
- public bool QuotaDisable
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "quota-disable"); }
- }
+ public bool QuotaDisable => GetBool("quota-disable");
///
/// Gets the display name of the backup
///
public string BackupName
{
- get
- {
- string tmp;
- m_options.TryGetValue("backup-name", out tmp);
- if (string.IsNullOrEmpty(tmp))
- return DefaultBackupName;
- else
- return tmp;
- }
- set
- {
- m_options["backup-name"] = value;
- }
+ get => GetString("backup-name", DefaultBackupName);
+ set => m_options["backup-name"] = value;
}
///
/// Gets the ID of the backup
///
- public string BackupId
- {
- get
- {
- m_options.TryGetValue("backup-id", out var tmp);
- return tmp;
- }
- }
+ public string? BackupId => m_options.GetValueOrDefault("backup-id");
///
/// Gets the ID of the machine
///
- public string MachineId
- {
- get
- {
- if (m_options.TryGetValue("machine-id", out var tmp))
- return tmp;
- return Library.AutoUpdater.DataFolderManager.InstallID;
- }
- }
+ public string? MachineId => m_options.GetValueOrDefault("machine-id", Library.AutoUpdater.DataFolderManager.InstallID);
+
///
/// Gets the path to the database
///
- public string Dbpath
+ public string? Dbpath
{
- get
- {
- string tmp;
- m_options.TryGetValue("dbpath", out tmp);
- return tmp;
- }
- set
- {
- m_options["dbpath"] = value;
- }
+ get => m_options.GetValueOrDefault("dbpath", null);
+ set => m_options["dbpath"] = value;
}
///
/// Gets a value indicating whether a blocksize has been specified
///
- public bool HasBlocksize { get { return m_options.ContainsKey("blocksize") && !string.IsNullOrEmpty(m_options["blocksize"]); } }
+ public bool HasBlocksize => !string.IsNullOrEmpty(m_options.GetValueOrDefault("blocksize"));
///
/// Gets the size of file-blocks
@@ -1432,11 +1125,7 @@ namespace Duplicati.Library.Main
{
get
{
- string tmp;
- if (!m_options.TryGetValue("blocksize", out tmp))
- tmp = DEFAULT_BLOCKSIZE;
-
- long blocksize = Library.Utility.Sizeparser.ParseSize(tmp, "kb");
+ var blocksize = GetSize("blocksize", "kb", DEFAULT_BLOCKSIZE);
if (blocksize > int.MaxValue || blocksize < 1024)
throw new ArgumentOutOfRangeException(nameof(blocksize), string.Format("The blocksize cannot be less than {0}, nor larger than {1}", 1024, int.MaxValue));
@@ -1467,142 +1156,68 @@ namespace Duplicati.Library.Main
///
/// Gets a flag indicating if metadata for files and folders should be ignored
///
- public bool SkipMetadata
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "skip-metadata"); }
- }
+ public bool SkipMetadata => GetBool("skip-metadata");
///
/// Gets a flag indicating if empty folders should be ignored
///
- public bool ExcludeEmptyFolders
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "exclude-empty-folders"); }
- }
+ public bool ExcludeEmptyFolders => GetBool("exclude-empty-folders");
///
/// Gets a flag indicating if during restores metadata should be applied to the symlink target.
/// Setting this to true can result in errors if the target no longer exists.
///
- public bool RestoreSymlinkMetadata
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "restore-symlink-metadata"); }
- }
+ public bool RestoreSymlinkMetadata => GetBool("restore-symlink-metadata");
///
/// Gets a flag indicating if permissions should be restored
///
- public bool RestorePermissions
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "restore-permissions"); }
- }
+ public bool RestorePermissions => GetBool("restore-permissions");
///
/// Gets a flag indicating if file hashes are checked after a restore
///
- public bool PerformRestoredFileVerification
- {
- get { return !Library.Utility.Utility.ParseBoolOption(m_options, "skip-restore-verification"); }
- }
+ public bool PerformRestoredFileVerification => !GetBool("skip-restore-verification");
///
/// Gets a flag indicating if synthetic filelist generation is disabled
///
- public bool DisableSyntheticFilelist
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "disable-synthetic-filelist"); }
- }
+ public bool DisableSyntheticFilelist => GetBool("disable-synthetic-filelist");
///
/// Gets the compact threshold
///
- public long Threshold
- {
- get
- {
- string v;
- m_options.TryGetValue("threshold", out v);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_THRESHOLD;
-
- return Convert.ToInt64(v);
- }
- }
+ public long Threshold => GetLong("threshold", DEFAULT_THRESHOLD);
///
/// Gets the size of small volumes
///
- public long SmallFileSize
- {
- get
- {
- string v;
- m_options.TryGetValue("small-file-size", out v);
- if (string.IsNullOrEmpty(v))
- return this.VolumeSize / 5;
-
- return Library.Utility.Sizeparser.ParseSize(v, "mb");
- }
- }
+ public long SmallFileSize => GetSize("small-file-size", "mb", $"{this.VolumeSize / 5}b");
///
/// Gets the maximum number of small volumes
///
- public long SmallFileMaxCount
- {
- get
- {
- string v;
- m_options.TryGetValue("small-file-max-count", out v);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_SMALL_FILE_MAX_COUNT;
-
- return Convert.ToInt64(v);
- }
- }
+ public long SmallFileMaxCount => GetLong("small-file-max-count", DEFAULT_SMALL_FILE_MAX_COUNT);
///
/// List of files to check for changes
///
- public string[] ChangedFilelist
- {
- get
- {
- string v;
- m_options.TryGetValue("changed-files", out v);
- if (string.IsNullOrEmpty(v))
- return null;
-
- return v.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
+ public string[]? ChangedFilelist => m_options.GetValueOrDefault("changed-files")?.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
///
/// List of files to mark as deleted
///
- public string[] DeletedFilelist
- {
- get
- {
- string v;
- m_options.TryGetValue("deleted-files", out v);
- if (string.IsNullOrEmpty(v))
- return null;
-
- return v.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
+ public string[]? DeletedFilelist => m_options.GetValueOrDefault("deleted-files")?.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
///
/// List of filenames that are used to exclude a folder
///
- public string[] IgnoreFilenames
+ public string[]? IgnoreFilenames
{
get
{
- string v;
- if (!m_options.TryGetValue("ignore-filenames", out v))
+ if (!m_options.TryGetValue("ignore-filenames", out var v))
v = "CACHEDIR.TAG";
if (string.IsNullOrEmpty(v))
return null;
@@ -1613,42 +1228,17 @@ namespace Duplicati.Library.Main
///
/// Alternate restore path
///
- public string Restorepath
- {
- get
- {
- string v;
- m_options.TryGetValue("restore-path", out v);
- return v;
- }
- }
+ public string? Restorepath => m_options.GetValueOrDefault("restore-path");
///
/// Gets the index file usage method
///
- public IndexFileStrategy IndexfilePolicy
- {
- get
- {
- string strategy;
- if (!m_options.TryGetValue("index-file-policy", out strategy))
- strategy = "";
-
- IndexFileStrategy res;
- if (!Enum.TryParse(strategy, true, out res))
- res = IndexFileStrategy.Full;
-
- return res;
- }
- }
+ public IndexFileStrategy IndexfilePolicy => GetEnum("index-file-policy", IndexFileStrategy.Full);
///
/// Gets a flag indicating if the check for files on the remote storage should be omitted
///
- public bool NoBackendverification
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "no-backend-verification"); }
- }
+ public bool NoBackendverification => GetBool("no-backend-verification");
///
/// Gets the percentage of samples to test during a backup operation
@@ -1657,11 +1247,9 @@ namespace Duplicati.Library.Main
{
get
{
- m_options.TryGetValue("backup-test-percentage", out string s);
+ m_options.TryGetValue("backup-test-percentage", out var s);
if (string.IsNullOrEmpty(s))
- {
return 0.1m;
- }
decimal percentage;
try
@@ -1685,265 +1273,137 @@ namespace Duplicati.Library.Main
///
/// Gets the number of samples to test during a backup operation
///
- public long BackupTestSampleCount
- {
- get
- {
- string s;
- m_options.TryGetValue("backup-test-samples", out s);
- if (string.IsNullOrEmpty(s))
- return 1;
-
- return long.Parse(s);
- }
- }
+ public long BackupTestSampleCount => Math.Max(0, GetLong("backup-test-samples", DEFAULT_BACKUP_TEST_SAMPLES));
///
/// Gets a flag indicating if compacting should not be done automatically
///
- public bool NoAutoCompact
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "no-auto-compact"); }
- }
+ public bool NoAutoCompact => GetBool("no-auto-compact");
///
/// Gets the minimum time that must elapse after last compaction before running next automatic compaction
///
- public TimeSpan AutoCompactInterval
- {
- get
- {
- if (!m_options.ContainsKey("auto-compact-interval") || string.IsNullOrEmpty(m_options["auto-compact-interval"]))
- return TimeSpan.Zero;
- else
- return Library.Utility.Timeparser.ParseTimeSpan(m_options["auto-compact-interval"]);
- }
- }
+ public TimeSpan AutoCompactInterval => Library.Utility.Utility.ParseTimespanOption(m_options, "auto-compact-interval", "0s");
///
/// Gets a flag indicating if missing source elements should be ignored
///
- public bool AllowMissingSource
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "allow-missing-source"); }
- }
+ public bool AllowMissingSource => GetBool("allow-missing-source");
///
/// Gets a value indicating if a verification file should be uploaded after changing the remote store
///
- public bool UploadVerificationFile
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "upload-verification-file"); }
- }
+ public bool UploadVerificationFile => GetBool("upload-verification-file");
///
/// Gets a value indicating if a passphrase change is allowed
///
- public bool AllowPassphraseChange
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "allow-passphrase-change"); }
- }
+ public bool AllowPassphraseChange => GetBool("allow-passphrase-change");
///
/// Gets a flag indicating if the current operation should merely output the changes
///
- public bool Dryrun
- {
- get
- {
- if (m_options.ContainsKey("dry-run"))
- return Library.Utility.Utility.ParseBoolOption(m_options, "dry-run");
- else
- return Library.Utility.Utility.ParseBoolOption(m_options, "dryrun");
- }
- }
+ public bool Dryrun => GetBool("dry-run") || GetBool("dryrun");
///
/// Gets a value indicating if the remote verification is deep
///
- public RemoteTestStrategy FullRemoteVerification
- {
- get
- {
- string policy;
- if (!m_options.TryGetValue("full-remote-verification", out policy))
- policy = "False";
-
- RemoteTestStrategy r;
- if (!Enum.TryParse(policy, true, out r))
- r = RemoteTestStrategy.True;
-
- return r;
- }
- }
+ public RemoteTestStrategy FullRemoteVerification => GetEnum("full-remote-verification", RemoteTestStrategy.True);
///
/// The block hash algorithm to use
///
- public string BlockHashAlgorithm
- {
- get
- {
- string v;
- m_options.TryGetValue("block-hash-algorithm", out v);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_BLOCK_HASH_ALGORITHM;
-
- return v;
- }
- }
+ public string BlockHashAlgorithm => GetString("block-hash-algorithm", DEFAULT_BLOCK_HASH_ALGORITHM);
///
/// The file hash algorithm to use
///
- public string FileHashAlgorithm
- {
- get
- {
- string v;
- m_options.TryGetValue("file-hash-algorithm", out v);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_FILE_HASH_ALGORITHM;
-
- return v;
- }
- }
+ public string FileHashAlgorithm => GetString("file-hash-algorithm", DEFAULT_FILE_HASH_ALGORITHM);
///
/// Gets a value indicating whether local blocks usage should be used for restore.
///
/// true if no local blocks; otherwise, false.
- public bool UseLocalBlocks
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "restore-with-local-blocks"); }
- }
+ public bool UseLocalBlocks => GetBool("restore-with-local-blocks");
///
/// Gets a flag indicating if the local database should not be used
///
/// true if no local db is used; otherwise, false.
- public bool NoLocalDb
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "no-local-db"); }
- }
+ public bool NoLocalDb => GetBool("no-local-db");
///
/// Gets a flag indicating if the local database should not be used
///
/// true if no local db is used; otherwise, false.
- public bool DontCompressRestorePaths
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "dont-compress-restore-paths"); }
- }
+ public bool DontCompressRestorePaths => GetBool("dont-compress-restore-paths");
///
/// Gets a flag indicating if block hashes are checked before being applied
///
/// true if block hashes are checked; otherwise, false.
- public bool FullBlockVerification
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "full-block-verification"); }
- }
+ public bool FullBlockVerification => GetBool("full-block-verification");
///
/// Gets a flag indicating if the repair process will only restore paths
///
/// true if only paths are restored; otherwise, false.
- public bool RepairOnlyPaths
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "repair-only-paths"); }
- }
+ public bool RepairOnlyPaths => GetBool("repair-only-paths");
///
/// Gets a flag indicating if the repair process will ignore outdated database
///
/// true if repair process will ignore outdated database; otherwise, false.
- public bool RepairIgnoreOutdatedDatabase
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "repair-ignore-outdated-database"); }
- }
+ public bool RepairIgnoreOutdatedDatabase => GetBool("repair-ignore-outdated-database");
///
/// Gets a flag indicating if the repair process will always use blocks
///
/// true if repair process always use blocks; otherwise, false.
- public bool RepairForceBlockUse
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "repair-force-block-use"); }
- }
+ public bool RepairForceBlockUse => GetBool("repair-force-block-use");
///
/// Gets a flag indicating whether the VACUUM operation should ever be run automatically.
///
- public bool AutoVacuum
- {
- get { return GetBool("auto-vacuum"); }
- }
+ public bool AutoVacuum => GetBool("auto-vacuum");
///
/// Gets the minimum time that must elapse after last vacuum before running next automatic vacuum
///
- public TimeSpan AutoVacuumInterval
- {
- get
- {
- if (!m_options.ContainsKey("auto-vacuum-interval") || string.IsNullOrEmpty(m_options["auto-vacuum-interval"]))
- return TimeSpan.Zero;
- else
- return Library.Utility.Timeparser.ParseTimeSpan(m_options["auto-vacuum-interval"]);
- }
- }
+ public TimeSpan AutoVacuumInterval => Library.Utility.Utility.ParseTimespanOption(m_options, "auto-vacuum-interval", "0s");
///
/// Gets a flag indicating if the local filescanner should be disabled
///
/// true if the filescanner should be disabled; otherwise, false.
- public bool DisableFileScanner
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "disable-file-scanner"); }
- }
+ public bool DisableFileScanner => GetBool("disable-file-scanner");
///
/// Gets a flag indicating if the filelist consistency checks should be disabled
///
/// true if the filelist consistency checks should be disabled; otherwise, false.
- public bool DisableFilelistConsistencyChecks
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "disable-filelist-consistency-checks"); }
- }
+ public bool DisableFilelistConsistencyChecks => GetBool("disable-filelist-consistency-checks");
///
/// Gets a flag indicating whether the backup should be disabled when on battery power.
///
/// true if the backup should be disabled when on battery power; otherwise, false.
- public bool DisableOnBattery
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "disable-on-battery"); }
- }
+ public bool DisableOnBattery => GetBool("disable-on-battery");
///
/// Gets a value indicating if missing dblock files are attempted created
///
- public bool RebuildMissingDblockFiles
- {
- get { return GetBool("rebuild-missing-dblock-files"); }
- }
+ public bool RebuildMissingDblockFiles => GetBool("rebuild-missing-dblock-files");
///
/// Gets a value indicating if partial dblock recovery is disabled
///
- public bool DisablePartialDblockRecovery
- {
- get { return GetBool("disable-partial-dblock-recovery"); }
- }
+ public bool DisablePartialDblockRecovery => GetBool("disable-partial-dblock-recovery");
///
/// Gets a value indicating if missing metadata is replaced with empty content on purge-broken-files
///
- public bool DisableReplaceMissingMetadata
- {
- get { return GetBool("disable-replace-missing-metadata"); }
- }
+ public bool DisableReplaceMissingMetadata => GetBool("disable-replace-missing-metadata");
///
/// Gets the threshold for when log data should be cleaned
@@ -1952,86 +1412,30 @@ namespace Duplicati.Library.Main
{
get
{
- string pts;
- if (!m_options.TryGetValue("log-retention", out pts))
- pts = DEFAULT_LOG_RETENTION;
-
- return Library.Utility.Timeparser.ParseTimeInterval(pts, DateTime.Now, true);
+ var value = GetString("log-retention", DEFAULT_LOG_RETENTION);
+ return Library.Utility.Timeparser.ParseTimeInterval(value, DateTime.Now, true);
}
}
-
///
/// Gets the number of concurrent threads
///
- public int ConcurrencyMaxThreads
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("concurrency-max-threads", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return 0;
- else
- return int.Parse(value);
- }
- }
+ public int ConcurrencyMaxThreads => GetInt("concurrency-max-threads", 0);
///
/// Gets the number of concurrent block hashers
///
- public int ConcurrencyBlockHashers
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("concurrency-block-hashers", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return DEFAULT_BLOCK_HASHERS;
- else
- return Math.Max(1, int.Parse(value));
- }
- }
+ public int ConcurrencyBlockHashers => Math.Max(1, GetInt("concurrency-block-hashers", DEFAULT_BLOCK_HASHERS));
///
/// Gets the number of concurrent block hashers
///
- public int ConcurrencyCompressors
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("concurrency-compressors", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return DEFAULT_COMPRESSORS;
- else
- return Math.Max(1, int.Parse(value));
- }
- }
+ public int ConcurrencyCompressors => Math.Max(1, GetInt("concurrency-compressors", DEFAULT_COMPRESSORS));
///
/// Gets the number of concurrent file processors
///
- public int ConcurrencyFileprocessors
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("concurrency-fileprocessors", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return DEFAULT_FILE_PROCESSORS;
- else
- return Math.Max(1, int.Parse(value));
- }
- }
+ public int ConcurrencyFileprocessors => Math.Max(1, GetInt("concurrency-fileprocessors", DEFAULT_FILE_PROCESSORS));
///
/// Gets a lookup table with compression hints, the key is the file extension with the leading period
@@ -2044,10 +1448,7 @@ namespace Duplicati.Library.Main
{
var hints = new Dictionary(StringComparer.OrdinalIgnoreCase); // Ignore file system case sensitivity, since file extensions case rarely indicates type
- string file;
- if (!m_options.TryGetValue("compression-extension-file", out file))
- file = DEFAULT_COMPRESSED_EXTENSION_FILE;
-
+ var file = GetString("compression-extension-file", DEFAULT_COMPRESSED_EXTENSION_FILE);
if (!string.IsNullOrEmpty(file) && System.IO.File.Exists(file))
foreach (var _line in Library.Utility.Utility.ReadFileWithDefaultEncoding(file).Split('\n'))
{
@@ -2070,20 +1471,7 @@ namespace Duplicati.Library.Main
///
/// Gets the CPU intensity level indicating target CPU utilization. 1 is the lowest, 10 is the highest. Default is 10.
///
- public int CPUIntensity
- {
- get
- {
- string value;
- if (!m_options.TryGetValue("cpu-intensity", out value))
- value = null;
-
- if (string.IsNullOrEmpty(value))
- return 10;
- else
- return Math.Max(1, Math.Min(10, int.Parse(value)));
- }
- }
+ public int CPUIntensity => Math.Max(1, Math.Min(10, GetInt("cpu-intensity", 10)));
///
/// Gets a compression hint from a filename
@@ -2092,8 +1480,7 @@ namespace Duplicati.Library.Main
/// The compression hint
public CompressionHint GetCompressionHintFromFilename(string filename)
{
- CompressionHint h;
- if (!CompressionHints.TryGetValue(System.IO.Path.GetExtension(filename), out h))
+ if (!CompressionHints.TryGetValue(System.IO.Path.GetExtension(filename), out var h))
return CompressionHint.Default;
return h;
}
@@ -2101,7 +1488,7 @@ namespace Duplicati.Library.Main
///
/// Gets a list of modules, the key indicates if they are loaded
///
- public List> LoadedModules { get { return m_loadedModules; } }
+ public List> LoadedModules => m_loadedModules;
///
/// Helper method to extract boolean values.
@@ -2111,10 +1498,59 @@ namespace Duplicati.Library.Main
/// The name of the option to read
/// The interpreted value of the option
private bool GetBool(string name)
+ => Library.Utility.Utility.ParseBoolOption(m_options, name);
+
+ ///
+ /// Helper method to extract string values.
+ ///
+ /// The option name
+ /// The default value
+ /// The value of the option, or the default value if the option is not present or empty
+ [return: NotNullIfNotNull("default")]
+ private string? GetString(string name, string? @default)
{
- return Library.Utility.Utility.ParseBoolOption(m_options, name);
+ var value = m_options.GetValueOrDefault(name);
+ return string.IsNullOrEmpty(value) ? @default : value;
}
+ ///
+ /// Helper method to extract integer values.
+ ///
+ /// The option name
+ /// The default value
+ /// The value of the option, or the default value if the option is not present or empty
+ private int GetInt(string name, int @default)
+ => Library.Utility.Utility.ParseIntOption(m_options, name, @default);
+
+ ///
+ /// Helper method to extract long values.
+ ///
+ /// The option name
+ /// The default value
+ /// The value of the option, or the default value if the option is not present or empty
+ private long GetLong(string name, long @default)
+ => Library.Utility.Utility.ParseLongOption(m_options, name, @default);
+
+ ///
+ /// Helper method to extract enum values.
+ ///
+ /// The enum type
+ /// The option name
+ /// The default value
+ /// The value of the option, or the default value if the option is not present or empty
+ private T GetEnum(string name, T @default) where T : struct, Enum
+ => Library.Utility.Utility.ParseEnumOption(m_options, name, @default);
+
+ ///
+ /// Helper method to extract size values.
+ ///
+ /// The option name
+ /// The default unit
+ /// The default value
+ /// The value of the option, or the default value if the option is not present or empty
+ private long GetSize(string name, string unit, string @default)
+ => Library.Utility.Utility.ParseSizeOption(m_options, name, unit, @default);
+
///
/// Gets the maximum number of data blocks to keep in the cache. If set to 0, the cache is effictively disabled, but some is still kept for bookkeeping.
///
@@ -2122,11 +1558,7 @@ namespace Duplicati.Library.Main
{
get
{
- if (!m_options.TryGetValue("restore-cache-max", out string v))
- v = DEFAULT_RESTORE_CACHE_MAX;
-
- long max_cache = Sizeparser.ParseSize(v, "mb");
-
+ var max_cache = GetSize("restore-cache-max", "mb", DEFAULT_RESTORE_CACHE_MAX);
if (max_cache > 0 && max_cache < Blocksize)
throw new ArgumentOutOfRangeException(nameof(max_cache), string.Format("The maximum cache size cannot be less than the blocksize if not explicitly 0: {0} < {1}", max_cache, Blocksize));
@@ -2141,11 +1573,9 @@ namespace Duplicati.Library.Main
{
get
{
- m_options.TryGetValue("restore-cache-evict", out string s);
+ m_options.TryGetValue("restore-cache-evict", out var s);
if (string.IsNullOrEmpty(s))
- {
return DEFAULT_RESTORE_CACHE_EVICT / 100f;
- }
int percentage;
try
@@ -2169,108 +1599,42 @@ namespace Duplicati.Library.Main
///
/// Gets the number of file processors to use in the restore process
///
- public int RestoreFileProcessors
- {
- get
- {
- if (!m_options.TryGetValue("restore-file-processors", out string v))
- v = null;
-
- if (string.IsNullOrEmpty(v))
- return DEFAULT_RESTORE_FILE_PROCESSORS;
- else
- return int.Parse(v);
- }
- }
+ public int RestoreFileProcessors => GetInt("restore-file-processors", DEFAULT_RESTORE_FILE_PROCESSORS);
///
/// Gets whether to use the legacy restore method
///
- public bool RestoreLegacy
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "restore-legacy"); }
- }
+ public bool RestoreLegacy => GetBool("restore-legacy");
///
/// Gets whether to preallocate files during restore
///
- public bool RestorePreAllocate
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "restore-pre-allocate"); }
- }
+ public bool RestorePreAllocate => GetBool("restore-pre-allocate");
///
/// Gets the number of volume decryptors to use in the restore process
///
- public int RestoreVolumeDecryptors
- {
- get
- {
- if (!m_options.TryGetValue("restore-volume-decryptors", out string v))
- v = null;
-
- if (string.IsNullOrEmpty(v))
- return DEFAULT_RESTORE_VOLUME_DECRYPTORS;
- else
- return int.Parse(v);
- }
- }
+ public int RestoreVolumeDecryptors => GetInt("restore-volume-decryptors", DEFAULT_RESTORE_VOLUME_DECRYPTORS);
///
/// Gets the number of volume decompressors to use in the restore process
///
- public int RestoreVolumeDecompressors
- {
- get
- {
- if (!m_options.TryGetValue("restore-volume-decompressors", out string v))
- v = null;
-
- if (string.IsNullOrEmpty(v))
- return DEFAULT_RESTORE_VOLUME_DECOMPRESSORS;
- else
- return int.Parse(v);
- }
- }
+ public int RestoreVolumeDecompressors => GetInt("restore-volume-decompressors", DEFAULT_RESTORE_VOLUME_DECOMPRESSORS);
///
/// Gets the number of volume downloaders to use in the restore process
///
- public int RestoreVolumeDownloaders
- {
- get
- {
- if (!m_options.TryGetValue("restore-volume-downloaders", out string v))
- v = null;
+ public int RestoreVolumeDownloaders => GetInt("restore-volume-downloaders", DEFAULT_RESTORE_VOLUME_DOWNLOADERS);
- if (string.IsNullOrEmpty(v))
- return DEFAULT_RESTORE_VOLUME_DOWNLOADERS;
- else
- return int.Parse(v);
- }
- }
-
- public int RestoreChannelBufferSize
- {
- get
- {
- if (!m_options.TryGetValue("restore-channel-buffer-size", out string v))
- v = null;
-
- if (string.IsNullOrEmpty(v))
- return DEFAULT_RESTORE_CHANNEL_BUFFER_SIZE;
- else
- return int.Parse(v);
- }
- }
+ ///
+ /// Gets the size of the buffer used for the restore channel
+ ///
+ public int RestoreChannelBufferSize => GetInt("restore-channel-buffer-size", DEFAULT_RESTORE_CHANNEL_BUFFER_SIZE);
///
/// Toggles whether internal profiling is enabled and should be logged.
///
- public bool InternalProfiling
- {
- get { return Library.Utility.Utility.ParseBoolOption(m_options, "internal-profiling"); }
- }
+ public bool InternalProfiling => GetBool("internal-profiling");
///
/// Gets the size of file-blocks
@@ -2279,21 +1643,17 @@ namespace Duplicati.Library.Main
{
get
{
- if (!m_options.TryGetValue("sqlite-page-cache", out var tmp))
- tmp = DEFAULT_SQLITE_PAGE_CACHE_SIZE;
-
- var pagesize = Sizeparser.ParseSize(tmp, "kb");
- if (pagesize <= SQLiteLoader.MINIMUM_SQLITE_PAGE_CACHE_SIZE)
- return 0;
-
- return pagesize;
+ var pagesize = GetSize("sqlite-page-cache", "kb", DEFAULT_SQLITE_PAGE_CACHE_SIZE);
+ return pagesize <= SQLiteLoader.MINIMUM_SQLITE_PAGE_CACHE_SIZE
+ ? 0
+ : pagesize;
}
}
///
/// Ignores the update if the version already exists in the database.
///
public bool IgnoreUpdateIfVersionExists
- => Library.Utility.Utility.ParseBoolOption(m_options, "ignore-update-if-version-exists");
+ => GetBool("ignore-update-if-version-exists");
///
/// Class for handling a single RetentionPolicy timeframe-interval-pair
diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs
index 6db36589e..7895b20d9 100644
--- a/Duplicati/Library/Main/Strings.cs
+++ b/Duplicati/Library/Main/Strings.cs
@@ -123,6 +123,8 @@ namespace Duplicati.Library.Main.Strings
public static string LoglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LoglevelShort { get { return LC.L(@"Log information level"); } }
public static string LogLevelDeprecated(string option1, string option2) { return LC.L("Use the options --{0} and --{1} instead.", option1, option2); }
+ public static string SuppresswarningsLong { get { return LC.L(@"Suppress warnings and log them as information instead. Use this if you need to silence specific warnings. This option accepts a comma separated list of warning IDs."); } }
+ public static string SuppresswarningsShort { get { return LC.L(@"Suppress specific warnings"); } }
public static string DisableautocreatefolderLong { get { return LC.L(@"If Duplicati detects that the target folder is missing, it will create it automatically. Activate this option to prevent automatic folder creation."); } }
public static string DisableautocreatefolderShort { get { return LC.L(@"Disable automatic folder creation"); } }
public static string VssexcludewritersLong { get { return LC.L(@"Use this option to exclude faulty writers from a snapshot. This is equivalent to the -wx flag of the vshadow.exe tool, except that it only accepts writer class GUIDs, and not component names or instance GUIDs. Multiple GUIDs must be separated with a semicolon, and most forms of GUIDs are allowed, including with and without curly braces."); } }
@@ -289,10 +291,14 @@ namespace Duplicati.Library.Main.Strings
public static string LogfileloglevelShort { get { return LC.L(@"Log file information level"); } }
public static string LogfilelogfiltersLong(string delimiter) { return LC.L(@"This option accepts filters that removes or includes messages regardless of their log level. Multiple filters are supported by separating with {0}. Filters are matched against the log tag and assumed to be including, unless they start with '-'. Regular expressions are supported within hard braces. Example: ""+Path*{0}+*Mail*{0}-[.*DNS]"" ", delimiter); }
public static string LogfilelogfiltersShort { get { return LC.L(@"Apply filters to the file log data"); } }
+ public static string LogfilelogignoreLong(string option) { return LC.L(@"This is a simplified version of the --{0} option. It will ignore all log messages that have an ID in the list. The list is a comma separated list of log message ids.", option); }
+ public static string LogfilelogignoreShort { get { return LC.L(@"Ignore log messages with the specified IDs"); } }
public static string ConsoleloglevelLong { get { return LC.L(@"Specify the amount of log information to output to the console."); } }
public static string ConsoleloglevelShort { get { return LC.L(@"Console information level"); } }
public static string ConsolelogfiltersLong(string delimiter) { return LogfilelogfiltersLong(delimiter); }
public static string ConsolelogfiltersShort { get { return LC.L(@"Apply filters to the console log data"); } }
+ public static string ConsolelogignoreLong(string option) { return LogfilelogignoreLong(option); }
+ public static string ConsolelogignoreShort { get { return LC.L(@"Ignore log messages with the specified IDs"); } }
public static string UsebackgroundiopriorityLong { get { return LC.L("This option instructs the operating system to set the current process to use the lowest IO priority level, which can make operations run slower but will interfere less with other operations running at the same time."); } }
public static string UsebackgroundiopriorityShort { get { return LC.L("Set the process to use low IO priority"); } }
diff --git a/Duplicati/Library/RestAPI/LogWriteHandler.cs b/Duplicati/Library/RestAPI/LogWriteHandler.cs
index 6b0613d67..26e3614d9 100644
--- a/Duplicati/Library/RestAPI/LogWriteHandler.cs
+++ b/Duplicati/Library/RestAPI/LogWriteHandler.cs
@@ -116,7 +116,7 @@ namespace Duplicati.Server
private RingBuffer m_buffer;
- private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null);
+ private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null, null);
private LogMessageType m_logLevel;
public LogWriteHandler()
diff --git a/Duplicati/Library/Utility/EnumerableExtensions.cs b/Duplicati/Library/Utility/EnumerableExtensions.cs
index 9aca73d90..9a8e6ed06 100644
--- a/Duplicati/Library/Utility/EnumerableExtensions.cs
+++ b/Duplicati/Library/Utility/EnumerableExtensions.cs
@@ -47,7 +47,7 @@ public static class EnumerableExtensions
/// A sequence that contains only the non-null elements from the input sequence.
public static IEnumerable WhereNotNull(this IEnumerable source) where T : struct
=> source.Where(x => x != null)
- .Select(x => x.Value);
+ .Select(x => x!.Value);
///
/// Filters a sequence of values to exclude null or whitespace elements.
diff --git a/Duplicati/Library/Utility/ExceptionExtensions.cs b/Duplicati/Library/Utility/ExceptionExtensions.cs
new file mode 100644
index 000000000..40c807a81
--- /dev/null
+++ b/Duplicati/Library/Utility/ExceptionExtensions.cs
@@ -0,0 +1,279 @@
+// Copyright (C) 2025, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// 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.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Duplicati.Library.Utility;
+
+///
+/// Extension methods for exceptions
+///
+public static class ExceptionExtensions
+{
+ ///
+ /// Flattens an exception and its inner exceptions
+ ///
+ /// The exception to flatten
+ /// An enumerable of exceptions
+ public static IEnumerable FlattenException(Exception? ex)
+ {
+ if (ex == null)
+ yield break;
+
+ yield return ex;
+
+ if (ex is AggregateException aex)
+ foreach (var iex in aex.Flatten().InnerExceptions)
+ foreach (var iex2 in FlattenException(iex))
+ yield return iex2;
+
+ foreach (var iex in FlattenException(ex.InnerException))
+ yield return iex;
+ }
+
+ ///
+ /// Checks if an exception is a stop, cancel or timeout exception
+ ///
+ /// The operation to check
+ /// true if the exception is a stop or cancel exception, false otherwise
+ public static bool IsAbortOrCancelException(this Exception ex)
+ => ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
+
+ ///
+ /// Checks if an exception is a stop exception
+ ///
+ /// The operation to check
+ /// true if the exception is a stop exception, false otherwise
+ public static bool IsAbortException(this Exception ex)
+ => ex is OperationCanceledException || ex is ThreadAbortException;
+
+ ///
+ /// List of Windows error codes that indicate a permission denied error
+ ///
+ private static readonly int[] WindowsPermissionDeniedCodes = {
+ 5, // ERROR_ACCESS_DENIED
+ 19, // ERROR_WRITE_PROTECT
+ 65, // ERROR_NETWORK_ACCESS_DENIED
+ 82, // ERROR_CANNOT_MAKE
+ 1314, // ERROR_PRIVILEGE_NOT_HELD
+ };
+
+ ///
+ /// List of Windows error codes that indicate a file lock error
+ ///
+ private static readonly int[] WindowsFileLockCodes = {
+ 32, // ERROR_SHARING_VIOLATION
+ 33 // ERROR_LOCK_VIOLATION
+ };
+
+ ///
+ /// List of Windows error codes that indicate a path not found error
+ ///
+ private static readonly int[] WindowsPathNotFoundCodes = {
+ 2, // ERROR_FILE_NOT_FOUND
+ 3, // ERROR_PATH_NOT_FOUND
+ 21, // ERROR_NOT_READY (e.g., missing drive)
+ 267 // ERROR_DIRECTORY
+ };
+
+ ///
+ /// List of Windows error codes that indicate a path too long error
+ ///
+ private static readonly int[] WindowsPathTooLongCodes = {
+ 206, // ERROR_FILENAME_EXCED_RANGE
+ 3 // ERROR_PATH_NOT_FOUND (used when long path causes resolution failure)
+ };
+
+ ///
+ /// List of Posix error codes that indicate a permission denied error
+ ///
+ private static readonly int[] PosixPermissionErrnos = {
+ 1, // EPERM
+ 13, // EACCES
+ 30, // EROFS - Read-only file system
+ };
+
+ ///
+ /// List of Posix error codes that indicate a file lock error
+ ///
+ private static readonly int[] PosixFileLockErrnos = {
+ // Linux file lock errors are not always distinguishable
+ 11 // EAGAIN (on NFS), or temporary unavailable (used for lock contention in some cases)
+ };
+
+ ///
+ /// List of Posix error codes that indicate a path not found error
+ ///
+ private static readonly int[] PosixPathNotFoundErrnos = {
+ 2, // ENOENT - No such file or directory
+ 20 // ENOTDIR - A path component is not a directory
+ };
+
+ ///
+ /// List of Posix error codes that indicate a path too long error
+ ///
+ private static readonly int[] PosixPathTooLongErrnos = {
+ 36, // ENAMETOOLONG - File name too long
+ 63 // ENAMETOOLONG (on some macOS versions)
+ };
+
+
+ ///
+ /// Checks if an exception is a permission denied error
+ ///
+ /// The operation to check
+ /// true if the exception is a permission denied error, false otherwise
+ public static bool IsPermissionDeniedException(this Exception? ex)
+ {
+ if (ex is UnauthorizedAccessException)
+ return true;
+
+ if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
+ return WindowsPermissionDeniedCodes.Contains(win32Ex.NativeErrorCode);
+
+ if (ex is IOException ioEx)
+ {
+ if (ioEx.HResult == unchecked((int)0x80070005)) // E_ACCESSDENIED
+ return true;
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ if (ioEx.InnerException is Win32Exception innerWin32 &&
+ PosixPermissionErrnos.Contains(innerWin32.NativeErrorCode))
+ return true;
+
+ var errno = ioEx.HResult & 0xFFFF;
+ return PosixPermissionErrnos.Contains(errno);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if an exception is a file lock error
+ ///
+ /// The operation to check
+ /// true if the exception is a file lock error, false otherwise
+ public static bool IsFileLockedException(this Exception? ex)
+ {
+ if (ex is IOException ioEx)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ // Check inner Win32Exception
+ if (ioEx.InnerException is Win32Exception innerWin32 &&
+ WindowsFileLockCodes.Contains(innerWin32.NativeErrorCode))
+ return true;
+
+ // Fallback to HRESULT
+ var winCode = ioEx.HResult & 0xFFFF;
+ return WindowsFileLockCodes.Contains(winCode);
+ }
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ if (ioEx.InnerException is Win32Exception innerWin32 &&
+ PosixFileLockErrnos.Contains(innerWin32.NativeErrorCode))
+ return true;
+
+ var errno = ioEx.HResult & 0xFFFF;
+ return PosixFileLockErrnos.Contains(errno);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if an exception is a path not found error
+ ///
+ /// The operation to check
+ /// true if the exception is a path not found error, false otherwise
+ public static bool IsPathNotFoundException(this Exception? ex)
+ {
+ if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
+ return true;
+
+ if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
+ return WindowsPathNotFoundCodes.Contains(win32Ex.NativeErrorCode);
+
+ if (ex is IOException ioEx)
+ {
+ var code = ioEx.HResult & 0xFFFF;
+
+ if (OperatingSystem.IsWindows())
+ return WindowsPathNotFoundCodes.Contains(code);
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ if (ioEx.InnerException is Win32Exception innerWin32 &&
+ PosixPathNotFoundErrnos.Contains(innerWin32.NativeErrorCode))
+ return true;
+
+ return PosixPathNotFoundErrnos.Contains(code);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if an exception is a path too long error
+ ///
+ /// The operation to check
+ /// true if the exception is a path too long error, false otherwise
+ public static bool IsPathTooLongException(this Exception? ex)
+ {
+ if (ex is PathTooLongException)
+ return true;
+
+ if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
+ return WindowsPathTooLongCodes.Contains(win32Ex.NativeErrorCode);
+
+ if (ex is IOException ioEx)
+ {
+ var code = ioEx.HResult & 0xFFFF;
+
+ if (OperatingSystem.IsWindows())
+ return WindowsPathTooLongCodes.Contains(code);
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ if (ioEx.InnerException is Win32Exception innerWin32 &&
+ PosixPathTooLongErrnos.Contains(innerWin32.NativeErrorCode))
+ return true;
+
+ return PosixPathTooLongErrnos.Contains(code);
+ }
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Duplicati/Library/Utility/FilterExpression.cs b/Duplicati/Library/Utility/FilterExpression.cs
index ea23eba06..266a3b562 100644
--- a/Duplicati/Library/Utility/FilterExpression.cs
+++ b/Duplicati/Library/Utility/FilterExpression.cs
@@ -1,29 +1,32 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// 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
+// Copyright (C) 2025, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// 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.
+#nullable enable
+
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Globalization;
+using System.Diagnostics.CodeAnalysis;
namespace Duplicati.Library.Utility
{
@@ -71,11 +74,11 @@ namespace Duplicati.Library.Utility
///
/// The filter string
///
- public readonly string Filter;
+ public readonly string? Filter;
///
/// The regular expression version of the filter
///
- public readonly Regex Regexp;
+ public readonly Regex? Regexp;
///
/// The single wildcard character (DOS style)
@@ -104,7 +107,7 @@ namespace Duplicati.Library.Utility
/// Initializes a new instance of the struct.
///
/// The filter string to use.
- public FilterEntry(string filter)
+ public FilterEntry(string? filter)
{
if (string.IsNullOrEmpty(filter))
{
@@ -155,8 +158,7 @@ namespace Duplicati.Library.Utility
private static Regex GetFilterGroupRegex(string filterGroupName)
{
FilterGroup filterGroup = FilterGroups.ParseFilterList(filterGroupName, FilterGroup.None);
- Regex result;
- if (FilterEntry.filterGroupRegexCache.TryGetValue(filterGroup, out result))
+ if (FilterEntry.filterGroupRegexCache.TryGetValue(filterGroup, out var result))
{
return result;
}
@@ -290,17 +292,17 @@ namespace Duplicati.Library.Utility
case FilterType.Simple:
return string.Equals(this.Filter, path, Library.Utility.Utility.ClientFilenameStringComparison);
case FilterType.Wildcard:
- return IsWildcardMatch(!Utility.IsFSCaseSensitive ? path.ToUpper(CultureInfo.InvariantCulture) : path, this.Filter);
+ return IsWildcardMatch(!Utility.IsFSCaseSensitive ? path.ToUpper(CultureInfo.InvariantCulture) : path, this.Filter!);
case FilterType.Regexp:
case FilterType.Group:
- var m = this.Regexp.Match(path);
+ var m = this.Regexp!.Match(path);
return m.Success && m.Length == path.Length;
default:
return false;
}
}
- public override string ToString()
+ public override string? ToString()
{
switch (this.Type)
{
@@ -319,7 +321,7 @@ namespace Duplicati.Library.Utility
///
/// The internal list of expressions
///
- private readonly List m_filters;
+ private readonly List? m_filters;
///
/// Gets the type of the filter
@@ -354,7 +356,7 @@ namespace Duplicati.Library.Utility
///
/// The match result
/// The filter that matched
- public bool Matches(string path, out bool result, out IFilter match)
+ public bool Matches(string path, out bool result, out IFilter? match)
{
result = false;
if (this.Type == FilterType.Empty)
@@ -363,7 +365,7 @@ namespace Duplicati.Library.Utility
return false;
}
- if (m_filters.Any(x => x.Matches(path)))
+ if (m_filters != null && m_filters.Any(x => x.Matches(path)))
{
match = this;
result = this.Result;
@@ -385,7 +387,7 @@ namespace Duplicati.Library.Utility
/// Creates a new instance, representing an empty filter.
///
public FilterExpression()
- : this((IEnumerable)null, true)
+ : this((IEnumerable?)null, true)
{
}
@@ -403,7 +405,7 @@ namespace Duplicati.Library.Utility
///
/// The filter string that represents the filter
/// Return value of in case of match
- public FilterExpression(IEnumerable filter, bool result = true)
+ public FilterExpression(IEnumerable? filter, bool result = true)
{
this.Result = result;
@@ -414,10 +416,9 @@ namespace Duplicati.Library.Utility
}
m_filters = Compact(
- (from n in filter
- let nx = new FilterEntry(n)
- where nx.Type != FilterType.Empty
- select nx)
+ filter
+ .Select(x => new FilterEntry(x))
+ .Where(x => x.Type != FilterType.Empty)
);
if (m_filters.Count == 0)
@@ -426,7 +427,7 @@ namespace Duplicati.Library.Utility
this.Type = m_filters.Max((a) => a.Type);
}
- private static IEnumerable Expand(string filter)
+ private static IEnumerable? Expand(string filter)
{
if (string.IsNullOrWhiteSpace(filter))
return null;
@@ -459,7 +460,7 @@ namespace Duplicati.Library.Utility
// since that would make their names much more difficult to interpret on the command line.
if (f.Type == FilterType.Simple || f.Type == FilterType.Wildcard || f.Type == FilterType.Group)
r.Add(f);
- else if (f.Type != FilterType.Empty)
+ else if (f.Type != FilterType.Empty && f.Regexp != null)
{
combined.Append(f.Regexp.ToString());
first = true;
@@ -518,12 +519,12 @@ namespace Duplicati.Library.Utility
/// The filter to examine
/// True if the filter contains includes, false otherwise.
/// True if the filter contains excludes, false otherwise.
- public static void AnalyzeFilters(IFilter filter, out bool includes, out bool excludes)
+ public static void AnalyzeFilters(IFilter? filter, out bool includes, out bool excludes)
{
includes = false;
excludes = false;
- Tuple cacheLookup = null;
+ Tuple? cacheLookup = null;
// Check for cached results
if (filter != null)
@@ -537,7 +538,7 @@ namespace Duplicati.Library.Utility
// Figure out what components are in the filter
if (cacheLookup == null)
{
- var q = new Queue();
+ var q = new Queue();
q.Enqueue(filter);
while (q.Count > 0)
@@ -564,7 +565,8 @@ namespace Duplicati.Library.Utility
{
if (_matchFallbackLookup.Count > 10)
_matchFallbackLookup.Remove(_matchFallbackLookup.Keys.Skip(new Random().Next(0, _matchFallbackLookup.Count)).First());
- _matchFallbackLookup[filter] = new Tuple(includes, excludes);
+ if (filter != null)
+ _matchFallbackLookup[filter] = new Tuple(includes, excludes);
}
}
}
@@ -575,7 +577,7 @@ namespace Duplicati.Library.Utility
/// The filter to evaluate
/// The path to evaluate
/// The filter that matched
- public static bool Matches(IFilter filter, string path, out IFilter match)
+ public static bool Matches(IFilter filter, string path, out IFilter? match)
{
if (filter == null || filter.Empty)
{
@@ -583,14 +585,11 @@ namespace Duplicati.Library.Utility
return true;
}
- bool result;
- if (filter.Matches(path, out result, out match))
+ if (filter.Matches(path, out bool result, out match))
return result;
- bool includes;
- bool excludes;
- AnalyzeFilters(filter, out includes, out excludes);
+ AnalyzeFilters(filter, out bool includes, out bool excludes);
match = null;
// We have only include filters, we exclude files by default
@@ -611,16 +610,16 @@ namespace Duplicati.Library.Utility
///
/// First.
/// Second.
- public static FilterExpression Combine(FilterExpression first, FilterExpression second)
+ public static FilterExpression? Combine(FilterExpression? first, FilterExpression? second)
{
- if (first == null || first.Empty)
+ if (first?.m_filters == null || first.Empty)
return second;
- if (second == null || second.Empty)
+ if (second?.m_filters == null || second.Empty)
return first;
if (first.Result != second.Result)
throw new ArgumentException("Both filters must have the same result property");
- return new FilterExpression(first.m_filters.Union(second.m_filters).Select(x => x.ToString()), first.Result);
+ return new FilterExpression(first.m_filters.Union(second.m_filters).Select(x => x.ToString()).WhereNotNull(), first.Result);
}
///
@@ -628,7 +627,7 @@ namespace Duplicati.Library.Utility
///
/// First.
/// Second.
- public static IFilter Combine(IFilter first, IFilter second)
+ public static IFilter? Combine(IFilter? first, IFilter? second)
{
if (second == null || second.Empty)
return first;
@@ -663,15 +662,13 @@ namespace Duplicati.Library.Utility
/// Serializes the filter instance into a list of strings
/// that can be passed to the deserialize method
///
- public string[] Serialize()
+ [return: NotNullIfNotNull("m_filters")]
+ public string[]? Serialize()
{
- if (this.Empty)
+ if (this.Empty || m_filters == null)
return null;
- return
- (from n in m_filters
- select $"{(this.Result ? "+" : "-")}{n.ToString()}"
- ).ToArray();
+ return m_filters.Select(n => $"{(this.Result ? "+" : "-")}{n.ToString()}").ToArray();
}
///
@@ -692,7 +689,7 @@ namespace Duplicati.Library.Utility
var f = work.Pop();
if (f is FilterExpression expression)
- res = res.Union(expression.Serialize());
+ res = res.Union(expression.Serialize() ?? []);
else if (f is JoinedFilterExpression filterExpression)
{
work.Push(filterExpression.Second);
@@ -710,12 +707,12 @@ namespace Duplicati.Library.Utility
/// prefixed with either minus or plus
///
/// The filters to deserialize from.
- public static IFilter Deserialize(string[] filters)
+ public static IFilter? Deserialize(string[] filters)
{
if (filters == null || filters.Length == 0)
return null;
- IFilter res = null;
+ IFilter? res = null;
foreach (var n in filters)
{
bool include;
@@ -737,7 +734,7 @@ namespace Duplicati.Library.Utility
///
/// The log filter.
/// The filter string to parse.
- public static IFilter ParseLogFilter(string value)
+ public static IFilter ParseLogFilter(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return new FilterExpression();
@@ -745,7 +742,8 @@ namespace Duplicati.Library.Utility
return value
.Split(new char[] { System.IO.Path.PathSeparator, ':', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(StringToIFilter)
- .Aggregate(FilterExpression.Combine);
+ .Cast()
+ .Aggregate((a, b) => FilterExpression.Combine(a, b)!);
}
///
diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs
index d29f548ab..d2b9c849a 100644
--- a/Duplicati/Library/Utility/Utility.cs
+++ b/Duplicati/Library/Utility/Utility.cs
@@ -789,8 +789,33 @@ namespace Duplicati.Library.Utility
/// default value
///
public static int ParseIntOption(IReadOnlyDictionary options, string value, int @default)
+ => options.TryGetValue(value, out var opt) && int.TryParse(opt ?? string.Empty, out var result) ? result : @default;
+
+ ///
+ /// Parses an option with long value, returning the default value if the option is not found or cannot be parsed
+ ///
+ /// The set of options to look for the setting in
+ /// The value to look for in the settings
+ /// default value
+ ///
+ public static long ParseLongOption(IReadOnlyDictionary options, string value, long @default)
+ => options.TryGetValue(value, out var opt) && long.TryParse(opt ?? string.Empty, out var result) ? result : @default;
+
+ ///
+ /// Parses a size option from the option set, returning the default value if the option is not found or cannot be parsed
+ ///
+ /// The set of options to look for the setting in
+ /// The value to look for in the settings
+ /// Multiplier to use if the value does not have a multiplier
+ /// The default value to return if there are no matches.
+ /// The parsed or default size value.
+ public static long ParseSizeOption(IReadOnlyDictionary options, string value, string defaultMultiplier, string @default)
{
- return options.TryGetValue(value, out var opt) && int.TryParse(opt ?? string.Empty, out var result) ? result : @default;
+ var opt = options.GetValueOrDefault(value);
+ if (string.IsNullOrWhiteSpace(opt))
+ opt = @default;
+
+ return Sizeparser.ParseSize(opt, defaultMultiplier);
}
///
@@ -1569,26 +1594,7 @@ namespace Duplicati.Library.Utility
public static string IpVersionCompatibleLoopback =>
HasIPv4Loopback ? IPAddress.Loopback.ToString() : $"[{IPAddress.IPv6Loopback.ToString()}]";
- ///
- /// Flattens an exception and its inner exceptions
- ///
- /// The exception to flatten
- /// An enumerable of exceptions
- public static IEnumerable FlattenException(Exception? ex)
- {
- if (ex == null)
- yield break;
- yield return ex;
-
- if (ex is AggregateException aex)
- foreach (var iex in aex.Flatten().InnerExceptions)
- foreach (var iex2 in FlattenException(iex))
- yield return iex2;
-
- foreach (var iex in FlattenException(ex.InnerException))
- yield return iex;
- }
///
/// Guesses the URL scheme and returns it
@@ -1639,26 +1645,6 @@ namespace Duplicati.Library.Utility
return sanitizedUrl;
}
- ///
- /// Checks if an exception is a stop, cancel or timeout exception
- ///
- /// The operation to check
- /// true if the exception is a stop or cancel exception, false otherwise
- public static bool IsAbortOrCancelException(this Exception ex)
- {
- return ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
- }
-
- ///
- /// Checks if an exception is a stop exception
- ///
- /// The operation to check
- /// true if the exception is a stop exception, false otherwise
- public static bool IsAbortException(this Exception ex)
- {
- return ex is OperationCanceledException || ex is ThreadAbortException;
- }
-
///
/// Formats the string using the invariant culture
///
diff --git a/Duplicati/UnitTest/ArchiveAttributeTests.cs b/Duplicati/UnitTest/ArchiveAttributeTests.cs
index 4487aa109..03c726c69 100644
--- a/Duplicati/UnitTest/ArchiveAttributeTests.cs
+++ b/Duplicati/UnitTest/ArchiveAttributeTests.cs
@@ -69,7 +69,7 @@ namespace Duplicati.UnitTest
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
=> backend.GetAsync(remotename, filename, cancellationToken);
public Task GetDNSNamesAsync(CancellationToken cancelToken)
- => GetDNSNamesAsync(cancelToken);
+ => backend.GetDNSNamesAsync(cancelToken);
public IAsyncEnumerable ListAsync(CancellationToken cancellationToken)
=> ListModifier(backend.ListAsync(cancellationToken));
diff --git a/Duplicati/UnitTest/Issue5845.cs b/Duplicati/UnitTest/Issue5845.cs
index 9eef94cd9..82f7311a1 100644
--- a/Duplicati/UnitTest/Issue5845.cs
+++ b/Duplicati/UnitTest/Issue5845.cs
@@ -67,7 +67,7 @@ namespace Duplicati.UnitTest
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
=> backend.GetAsync(remotename, filename, cancellationToken);
public Task GetDNSNamesAsync(CancellationToken cancelToken)
- => GetDNSNamesAsync(cancelToken);
+ => backend.GetDNSNamesAsync(cancelToken);
public IAsyncEnumerable ListAsync(CancellationToken cancellationToken)
=> Sorter(backend.ListAsync(cancellationToken).ToBlockingEnumerable()).ToAsyncEnumerable();
diff --git a/Duplicati/UnitTest/Issue5862.cs b/Duplicati/UnitTest/Issue5862.cs
index 22ab5bf48..fd8eadc76 100644
--- a/Duplicati/UnitTest/Issue5862.cs
+++ b/Duplicati/UnitTest/Issue5862.cs
@@ -64,7 +64,7 @@ namespace Duplicati.UnitTest
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
=> backend.GetAsync(remotename, filename, cancellationToken);
public Task GetDNSNamesAsync(CancellationToken cancelToken)
- => GetDNSNamesAsync(cancelToken);
+ => backend.GetDNSNamesAsync(cancelToken);
public IAsyncEnumerable ListAsync(CancellationToken cancellationToken)
=> backend.ListAsync(cancellationToken);
diff --git a/ReleaseBuilder/Build/Command.CreatePackage.cs b/ReleaseBuilder/Build/Command.CreatePackage.cs
index 4d8de5310..a14da8606 100644
--- a/ReleaseBuilder/Build/Command.CreatePackage.cs
+++ b/ReleaseBuilder/Build/Command.CreatePackage.cs
@@ -1016,7 +1016,9 @@ public static partial class Command
Directory.Delete(tmpbuild, true);
Directory.CreateDirectory(tmpbuild);
- var tarsrc = Path.Combine(tmpbuild, $"duplicati-{rtcfg.ReleaseInfo.Version}");
+ var servicename = target.Interface == InterfaceType.Agent ? "-agent" : "";
+
+ var tarsrc = Path.Combine(tmpbuild, $"duplicati{servicename}-{rtcfg.ReleaseInfo.Version}");
EnvHelper.CopyDirectory(Path.Combine(buildRoot, target.BuildTargetString), tarsrc, recursive: true);
await PackageSupport.InstallPackageIdentifier(tarsrc, target);
@@ -1090,7 +1092,7 @@ public static partial class Command
.Replace("%BUILDVERSION%", rtcfg.ReleaseInfo.Version.ToString())
.Replace("%BUILDTAG%", rtcfg.ReleaseInfo.Channel.ToString().ToLowerInvariant())
.Replace("%VERSION%", rtcfg.ReleaseInfo.Version.ToString())
- .Replace("%SERVICENAME%", target.Interface == InterfaceType.Agent ? "-agent" : "")
+ .Replace("%SERVICENAME%", servicename)
.Replace("%PROVIDES%", string.Join("\n", executables.Select(x => $"Provides:\t{x}")))
.Replace("%DEPENDS%", string.Join("\n",
(target.Interface == InterfaceType.GUI
diff --git a/ReleaseBuilder/Resources/debian/control.template.txt b/ReleaseBuilder/Resources/debian/control.template.txt
index 5f98b38e3..433042aa6 100644
--- a/ReleaseBuilder/Resources/debian/control.template.txt
+++ b/ReleaseBuilder/Resources/debian/control.template.txt
@@ -10,8 +10,8 @@ Version: %VERSION%
Description: Backup client for encrypted online backups
Duplicati is a free open-source backup client that securely stores encrypted, incremental,
compressed backups on cloud storage services and remote file servers. It
- supports targets like Amazon S3, Windows Live SkyDrive, Rackspace Cloud Files
- or WebDAV, SSH, FTP (and many more).
+ supports targets like Amazon S3, Backblaze B2, Microsoft OneDrive,
+ WebDAV, SSH, FTP (and many more).
.
Duplicati has built-in AES-256 encryption and backups be can signed using GNU
Privacy Guard. A built-in scheduler makes sure that backups are always
diff --git a/ReleaseBuilder/Resources/fedora/duplicati.spec.template.txt b/ReleaseBuilder/Resources/fedora/duplicati.spec.template.txt
index b1a247a12..1701582e2 100644
--- a/ReleaseBuilder/Resources/fedora/duplicati.spec.template.txt
+++ b/ReleaseBuilder/Resources/fedora/duplicati.spec.template.txt
@@ -2,6 +2,7 @@
# Set up some defaults
%global namer duplicati
+%global installname %{namer}%SERVICENAME%
%global debug_package %{nil}
%global alphatag .git
%global _builddate %BUILDDATE%
@@ -10,7 +11,7 @@
# All lines starting with #GUI_ONLY# are removed in CLI builds
-Name: %{namer}
+Name: %{installname}
Version: %{_buildversion}
Release: %{_buildtag}
Icon: duplicati.xpm
@@ -27,11 +28,11 @@ URL: https://duplicati.com
Source0: duplicati-%{_buildversion}.tar.bz2
Source1: %{namer}-install-recursive.sh
Source2: %{namer}-install-binaries.sh
-Source3: %{namer}%SERVICENAME%.service
-Source4: %{namer}%SERVICENAME%.default
+Source3: %{installname}.service
+Source4: %{installname}.default
#GUI_ONLY#Source5: %{namer}.png
#GUI_ONLY#Source6: %{namer}.desktop
-#AGENT_ONLY#Source5: %{namer}%SERVICENAME%.preset
+#AGENT_ONLY#Source5: %{installname}.preset
#GUI_ONLY#BuildRequires: desktop-file-utils
BuildRequires: systemd
@@ -44,8 +45,8 @@ Requires: bash
%description
Duplicati is a free backup client that securely stores encrypted,
incremental, compressed backups on cloud storage services and remote file
-servers. It supports targets like Amazon S3, Windows Live SkyDrive,
-Rackspace Cloud Files or WebDAV, SSH, FTP (and many more).
+servers. It supports targets like Amazon S3, Backblaze B2,
+Microsoft OneDrive, WebDAV, SSH, FTP (and many more).
Duplicati has built-in AES-256 encryption and backups be can signed using
GNU Privacy Guard. A built-in scheduler makes sure that backups are always
@@ -54,7 +55,7 @@ tweaks like filters, deletion rules, transfer and bandwidth options to run
backups for specific purposes.
%prep
-%setup -q -n %{namer}-%{_buildversion}
+%setup -q -n %{installname}-%{_buildversion}
%build
@@ -64,10 +65,10 @@ backups for specific purposes.
%install
#GUI_ONLY#install -d %{buildroot}%{_datadir}/pixmaps/
-install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/
-install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/
-install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/
-install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/
+install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/
+install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/licenses/
+install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/webroot/
+install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/lvm-scripts/
install -d %{buildroot}%{_exec_prefix}/bin/
install -d %{buildroot}%{_unitdir}
install -d %{buildroot}%{_sysconfdir}/sysconfig/
@@ -77,35 +78,35 @@ install -d %{buildroot}%{_sysconfdir}/sysconfig/
find . -type f -name ._\* | xargs rm -rf
# Install all files, but the list is too long to be in the script itself :/
-/bin/bash %{_topdir}/SOURCES/%{namer}-install-recursive.sh "." "%{buildroot}%{_exec_prefix}/lib/%{namer}/"
+/bin/bash %{_topdir}/SOURCES/%{namer}-install-recursive.sh "." "%{buildroot}%{_exec_prefix}/lib/%{installname}/"
# Move the icon in to place
#GUI_ONLY#install -p %{_topdir}/SOURCES/%{namer}.png %{buildroot}%{_datadir}/pixmaps/
# Fix executable permissions and install symlinks
-/bin/bash %{_topdir}/SOURCES/%{namer}-install-binaries.sh "%{buildroot}" "%{_exec_prefix}" "%{namer}"
+/bin/bash %{_topdir}/SOURCES/%{namer}-install-binaries.sh "%{buildroot}" "%{_exec_prefix}" "%{installname}"
#GUI_ONLY#desktop-file-install %{_topdir}/SOURCES/%{namer}.desktop
# Install the service:
install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.service %{buildroot}%{_unitdir}
-install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroot}%{_sysconfdir}/sysconfig/%{namer}
-
+install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroot}%{_sysconfdir}/sysconfig/%{installname}
+#AGENT_ONLY#install -p -D -m 644 %{_topdir}/SOURCES/%{installname}.preset %{buildroot}%{_presetdir}/50-%{installname}.preset
+
%post
#GUI_ONLY#/bin/touch --no-create %{_datadir}/icons/hicolor || :
#GUI_ONLY#%{_bindir}/gtk-update-icon-cache \
#GUI_ONLY# --quiet %{_datadir}/icons/hicolor 2> /dev/null|| :
-%systemd_post %{namer}%SERVICENAME%.service
-#AGENT_ONLY#%{__install} -Dm644 %{namer}%SERVICENAME%.preset %{buildroot}%{_presetdir}/50-%{namer}%SERVICENAME%.preset
+%systemd_post %{installname}.service
%preun
-%systemd_preun %{namer}%SERVICENAME%.service
+%systemd_preun %{installname}.service
%postun
#GUI_ONLY#/bin/touch --no-create %{_datadir}/icons/hicolor || :
#GUI_ONLY#%{_bindir}/gtk-update-icon-cache \
#GUI_ONLY# --quiet %{_datadir}/icons/hicolor 2> /dev/null|| :
-%systemd_postun_with_restart %{namer}%SERVICENAME%.service
+%systemd_postun_with_restart %{installname}.service
%posttrans
#GUI_ONLY#/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
@@ -116,10 +117,14 @@ install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroo
#GUI_ONLY#%{_datadir}/*/*
%{_exec_prefix}/lib/*
%{_exec_prefix}/bin/*
-%{_sysconfdir}/sysconfig/*
-
+%config(noreplace) %{_sysconfdir}/sysconfig/%{installname}
+#AGENT_ONLY#%{_presetdir}/50-%{installname}.preset
%changelog
+* Mon May 19 2025 Kenneth Skovhede - 2.0.0-0.20250519.git
+- Fixed config files not being overwritten
+- Moved agent files into separate folder to allow cli and agent to co-exist
+
* Wed May 22 2024 Kenneth Skovhede - 2.0.0-0.20240522.git
- Split for CLI and GUI package
diff --git a/Tools/RemoteSynchronization/Program.cs b/Tools/RemoteSynchronization/Program.cs
index 91c8ae793..aa2406c2e 100644
--- a/Tools/RemoteSynchronization/Program.cs
+++ b/Tools/RemoteSynchronization/Program.cs
@@ -151,7 +151,7 @@ destination will be verified before being overwritten (if they seemingly match).
log_level_enum = log_level_parsed ? log_level_enum : Duplicati.Library.Logging.LogMessageType.Information;
using var console_sink = new Duplicati.CommandLine.ConsoleOutput(Console.Out, global_options);
- using var multi_sink = new Duplicati.Library.Main.ControllerMultiLogTarget(console_sink, log_level_enum, null);
+ using var multi_sink = new Duplicati.Library.Main.ControllerMultiLogTarget(console_sink, log_level_enum, null, null);
// Parse the log file
// The log file sink doesn't have to be disposed, as the multi_sink will take care of it