diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 9cd02b6c9..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"), @@ -425,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))), @@ -504,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(); } } @@ -515,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 @@ -535,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 @@ -583,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; @@ -625,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 @@ -660,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 @@ -709,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 @@ -750,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)); @@ -774,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; } @@ -795,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. @@ -893,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 @@ -935,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"; } } @@ -962,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 @@ -1070,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 @@ -1197,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)) @@ -1249,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)) @@ -1271,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 @@ -1323,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"); } } @@ -1337,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 @@ -1435,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)); @@ -1470,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; @@ -1616,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 @@ -1660,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 @@ -1688,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 @@ -1955,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 @@ -2047,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')) { @@ -2073,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 @@ -2095,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; } @@ -2104,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. @@ -2114,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. /// @@ -2125,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)); @@ -2144,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 @@ -2172,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 @@ -2282,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/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 1df49958a..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); } ///