duplicati/Duplicati/Library/Utility/CommandLineArgumentMapper.cs
Kenneth Skovhede f91c814a45 Introduce DateTime option type.
This commit adds the `DateTime` option to support validation of the `--time` parameter, which can both be an absolute date and time, or a relative value.

This fixes #6107
2025-04-01 14:17:37 +02:00

224 lines
10 KiB
C#

// 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.Collections.Specialized;
using System.Linq;
using System.Reflection;
using Duplicati.Library.Interface;
namespace Duplicati.Library.Utility;
/// <summary>
/// Helper to get the command line argument type attribute
/// </summary>
public interface ICommandLineArgumentMapper
{
/// <summary>
/// Get the command line argument type attribute
/// </summary>
/// <param name="mi">The member info to get the attribute from</param>
/// <returns>The command line argument type attribute</returns>
CommandLineArgumentDescriptionAttribute? GetCommandLineArgumentDescription(MemberInfo mi);
}
/// <summary>
/// Attribute to define command line argument types
/// </summary>
public class CommandLineArgumentDescriptionAttribute
{
/// <summary>
/// The type of the argument, using the property type as default
/// </summary>
public CommandLineArgument.ArgumentType? Type { get; init; } = null;
/// <summary>
/// The name of the argument, using the property name as default
/// </summary>
public string? Name { get; init; } = null;
/// <summary>
/// A short description of the argument, using the property name as default
/// </summary>
public string? ShortDescription { get; init; } = null;
/// <summary>
/// A long description of the argument, using the property name as default
/// </summary>
public string? LongDescription { get; init; } = null;
/// <summary>
/// The default value for the argument, using the property value as default
/// </summary>
public string? DefaultValue { get; init; } = null;
/// <summary>
/// The list of valid values for the argument
/// </summary>
public string[]? ValueList { get; init; } = null;
/// <summary>
/// The converter to use for the argument
/// </summary>
public Func<string, object>? Converter { get; init; } = null;
}
/// <summary>
/// Implementation of commandline argument mapping
/// </summary>
public static class CommandLineArgumentMapper
{
/// <summary>
/// Helper method to detect slow-access properties
/// </summary>
/// <param name="p">The property to read</param>
/// <param name="parent">The parent object to read from</param>
/// <param name="excludeDefaultValue">A list of properties to exclude</param>
/// <returns>The value of the property</returns>
private static object? GetValueGuarded(PropertyInfo p, object parent, HashSet<string>? excludeDefaultValue)
{
object? value = null;
if (excludeDefaultValue != null && excludeDefaultValue.Contains(p.Name))
return value;
#if DEBUG
var start = DateTime.Now;
value = p.GetValue(parent);
var duration = DateTime.Now - start;
if (duration.TotalMilliseconds > 100)
Console.WriteLine($"Warning: {parent.GetType().FullName}.{p.Name} took {duration.TotalMilliseconds}ms to get value");
#else
value = p.GetValue(parent);
#endif
return value;
}
/// <summary>
/// Extracts all primitive types from a class and maps them to command line arguments
/// </summary>
/// <param name="type">The type to extract arguments from</param>
/// <param name="prefix">The prefix to use for the arguments</param>
/// <param name="exclude">A list of properties to exclude</param>
/// <returns>A list of command line arguments</returns>
public static IEnumerable<ICommandLineArgument> MapArguments(object obj, string prefix = "", HashSet<string>? exclude = null, HashSet<string>? excludeDefaultValue = null)
{
exclude ??= new HashSet<string>();
foreach (var p in obj.GetType().GetProperties().Where(p => p.CanWrite && p.CanRead))
{
if (exclude.Contains(p.Name))
continue;
var customAttr = (obj as ICommandLineArgumentMapper)?.GetCommandLineArgumentDescription(p);
var name = (prefix + (customAttr?.Name ?? p.Name)).ToLowerInvariant();
var shortDescription = customAttr?.ShortDescription ?? p.Name;
var longDescription = customAttr?.LongDescription ?? p.Name;
var defaultValue = customAttr?.DefaultValue ?? GetValueGuarded(p, obj, excludeDefaultValue)?.ToString();
var argumentType = customAttr?.Type;
// Fully custom argument
if (customAttr != null && customAttr.Converter != null && argumentType != null)
{
yield return new CommandLineArgument(name, argumentType.Value, shortDescription, longDescription, defaultValue, null, customAttr.ValueList);
continue;
}
var propType = p.PropertyType;
if (Nullable.GetUnderlyingType(propType) != null)
propType = Nullable.GetUnderlyingType(propType);
if (propType == null)
continue;
if (propType == typeof(string))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue);
else if (propType == typeof(int))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Integer, shortDescription, longDescription, defaultValue);
else if (propType == typeof(bool))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Boolean, shortDescription, longDescription, defaultValue);
else if (propType.IsEnum)
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue, null, customAttr?.ValueList ?? Enum.GetNames(propType));
else if (propType == typeof(Uri))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue);
else if (propType == typeof(TimeSpan))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Timespan, shortDescription, longDescription, defaultValue);
else if (propType == typeof(DateTime))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.DateTime, shortDescription, longDescription, defaultValue);
}
}
/// <summary>
/// Applies the arguments to the object
/// </summary>
/// <typeparam name="T">The type of object to apply the arguments to</typeparam>
/// <param name="obj">The object to apply the arguments to</param>
/// <param name="args">The arguments to apply</param>
/// <param name="prefix">The prefix to use for the arguments</param>
/// <returns>The object with the arguments applied</returns>
public static T ApplyArguments<T>(T obj, IReadOnlyDictionary<string, string?> args, string prefix = "")
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
foreach (var p in obj.GetType().GetProperties().Where(p => p.CanWrite && p.CanRead))
{
var customAttr = (obj as ICommandLineArgumentMapper)?.GetCommandLineArgumentDescription(p);
var name = (prefix + (customAttr?.Name ?? p.Name)).ToLowerInvariant();
var propType = p.PropertyType;
if (Nullable.GetUnderlyingType(propType) != null)
propType = Nullable.GetUnderlyingType(propType);
if (propType == null)
continue;
// Ignore case-sensitive properties of input dictionary
var value = args.Keys.FirstOrDefault(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)) is string key ? args[key] : null;
if (value != null)
{
if (customAttr?.Converter != null)
p.SetValue(obj, customAttr.Converter(value));
else if (propType == typeof(string))
p.SetValue(obj, value);
else if (propType == typeof(int))
p.SetValue(obj, int.Parse(value));
else if (propType == typeof(bool))
p.SetValue(obj, bool.Parse(value));
else if (propType.IsEnum)
p.SetValue(obj, Enum.Parse(propType, value, true));
else if (propType == typeof(Uri))
p.SetValue(obj, new Uri(value));
else if (propType == typeof(TimeSpan))
p.SetValue(obj, Timeparser.ParseTimeSpan(value));
}
}
return obj;
}
/// <summary>
/// Applies the arguments to the object
/// </summary>
/// <typeparam name="T">The type of object to apply the arguments to</typeparam>
/// <param name="obj">The object to apply the arguments to</param>
/// <param name="nameValueCollection">The arguments to apply</param>
/// <param name="prefix">The prefix to use for the arguments</param>
/// <returns>The object with the arguments applied</returns>
public static T ApplyArguments<T>(T obj, NameValueCollection nameValueCollection, string prefix = "")
=> ApplyArguments(obj, nameValueCollection.AllKeys.Where(k => !string.IsNullOrWhiteSpace(k)).ToDictionary(k => k!, k => nameValueCollection[k]), prefix);
}