Enable Nullable

This commit is contained in:
MBucari 2023-07-30 13:31:57 -06:00
parent e8c63e9a6e
commit 34033e7947
32 changed files with 403 additions and 275 deletions

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace FileManager namespace FileManager
{ {
/// <summary> /// <summary>
@ -16,9 +17,9 @@ namespace FileManager
public string SearchPattern { get; private set; } public string SearchPattern { get; private set; }
public SearchOption SearchOption { get; private set; } public SearchOption SearchOption { get; private set; }
private FileSystemWatcher fileSystemWatcher { get; set; } private FileSystemWatcher? fileSystemWatcher { get; set; }
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; } private BlockingCollection<FileSystemEventArgs>? directoryChangesEvents { get; set; }
private Task backgroundScanner { get; set; } private Task? backgroundScanner { get; set; }
private object fsCacheLocker { get; } = new(); private object fsCacheLocker { get; } = new();
private List<LongPath> fsCache { get; } = new(); private List<LongPath> fsCache { get; } = new();
@ -32,7 +33,7 @@ namespace FileManager
Init(); Init();
} }
public LongPath FindFile(System.Text.RegularExpressions.Regex regex) public LongPath? FindFile(System.Text.RegularExpressions.Regex regex)
{ {
lock (fsCacheLocker) lock (fsCacheLocker)
return fsCache.FirstOrDefault(s => regex.IsMatch(s)); return fsCache.FirstOrDefault(s => regex.IsMatch(s));
@ -105,13 +106,13 @@ namespace FileManager
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{ {
directoryChangesEvents.Add(e); directoryChangesEvents?.Add(e);
} }
#region Background Thread #region Background Thread
private void BackgroundScanner() private void BackgroundScanner()
{ {
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) while (directoryChangesEvents?.TryTake(out var change, -1) is true)
{ {
lock (fsCacheLocker) lock (fsCacheLocker)
UpdateLocalCache(change); UpdateLocalCache(change);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -7,20 +8,20 @@ using Dinah.Core;
using Polly; using Polly;
using Polly.Retry; using Polly.Retry;
#nullable enable
namespace FileManager namespace FileManager
{ {
public static class FileUtility public static class FileUtility
{ {
/// <summary> /// <summary>
/// "txt" => ".txt" /// "txt" => ".txt"
/// <br />".txt" => ".txt" /// <br />".txt" => ".txt"
/// <br />null or whitespace => "" /// <br />null or whitespace => ""
/// </summary> /// </summary>
public static string GetStandardizedExtension(string extension) [return: NotNull]
public static string GetStandardizedExtension(string? extension)
=> string.IsNullOrWhiteSpace(extension) => string.IsNullOrWhiteSpace(extension)
? (extension ?? "")?.Trim() ? string.Empty
: '.' + extension.Trim().Trim('.'); : '.' + extension.Trim().Trim('.');
/// <summary> /// <summary>
@ -48,18 +49,18 @@ namespace FileManager
/// <br/>- ensure uniqueness /// <br/>- ensure uniqueness
/// <br/>- enforce max file length /// <br/>- enforce max file length
/// </summary> /// </summary>
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false) public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string? fileExtension, bool returnFirstExisting = false)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
fileExtension = GetStandardizedExtension(fileExtension); fileExtension = GetStandardizedExtension(fileExtension);
// remove invalid chars // remove invalid chars
path = GetSafePath(path, replacements); path = GetSafePath(path, replacements);
// ensure uniqueness and check lengths // ensure uniqueness and check lengths
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
var fileName = Path.GetFileName(path); var fileName = Path.GetFileName(path);
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase); var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
@ -84,6 +85,7 @@ namespace FileManager
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
var pathNoPrefix = path.PathWithoutPrefix; var pathNoPrefix = path.PathWithoutPrefix;
@ -159,7 +161,7 @@ namespace FileManager
LongPath source, LongPath source,
LongPath destination, LongPath destination,
ReplacementCharacters replacements, ReplacementCharacters replacements,
string extension = null, string? extension = null,
bool overwrite = false) bool overwrite = false)
{ {
extension ??= Path.GetExtension(source); extension ??= Path.GetExtension(source);
@ -213,6 +215,9 @@ namespace FileManager
SaferDelete(destination); SaferDelete(destination);
var dir = Path.GetDirectoryName(destination); var dir = Path.GetDirectoryName(destination);
if (dir is null)
throw new DirectoryNotFoundException();
Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir }); Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);

View File

@ -8,9 +8,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace FileManager namespace FileManager
{ {
public sealed class LogArchiver : IAsyncDisposable public sealed class LogArchiver : IAsyncDisposable, IDisposable
{ {
public Encoding Encoding { get; set; } public Encoding Encoding { get; set; }
public string FileName { get; } public string FileName { get; }
@ -39,28 +40,28 @@ namespace FileManager
e.Delete(); e.Delete();
} }
public async Task AddFileAsync(string name, JObject contents, string comment = null) public async Task AddFileAsync(string name, JObject contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(contents, nameof(contents)); ArgumentValidator.EnsureNotNull(contents, nameof(contents));
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment); await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
} }
public async Task AddFileAsync(string name, string contents, string comment = null) public async Task AddFileAsync(string name, string contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(contents, nameof(contents)); ArgumentValidator.EnsureNotNull(contents, nameof(contents));
await AddFileAsync(name, Encoding.GetBytes(contents), comment); await AddFileAsync(name, Encoding.GetBytes(contents), comment);
} }
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null) public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(name, nameof(name)); ArgumentValidator.EnsureNotNull(name, nameof(name));
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name); name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
return Task.Run(() => AddfileInternal(name, contents.Span, comment)); return Task.Run(() => AddFileInternal(name, contents.Span, comment));
} }
private readonly object lockObj = new(); private readonly object lockObj = new();
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment) private void AddFileInternal(string name, ReadOnlySpan<byte> contents, string? comment)
{ {
lock (lockObj) lock (lockObj)
{ {
@ -73,5 +74,7 @@ namespace FileManager
} }
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose); public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
public void Dispose() => archive.Dispose();
} }
} }

View File

@ -1,9 +1,11 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
#nullable enable
namespace FileManager namespace FileManager
{ {
public class LongPath public class LongPath
@ -15,9 +17,9 @@ namespace FileManager
public static readonly int MaxPathLength; public static readonly int MaxPathLength;
private const int WIN_MAX_PATH = 260; private const int WIN_MAX_PATH = 260;
private const string WIN_LONG_PATH_PREFIX = @"\\?\"; private const string WIN_LONG_PATH_PREFIX = @"\\?\";
internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows); internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux); internal static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX); internal static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public string Path { get; } public string Path { get; }
@ -60,7 +62,8 @@ namespace FileManager
=> IsWindows ? filename.Length => IsWindows ? filename.Length
: Encoding.UTF8.GetByteCount(filename); : Encoding.UTF8.GetByteCount(filename);
public static implicit operator LongPath(string path) [return: NotNullIfNotNull(nameof(path))]
public static implicit operator LongPath?(string? path)
{ {
if (path is null) return null; if (path is null) return null;
@ -93,7 +96,8 @@ namespace FileManager
} }
} }
public static implicit operator string(LongPath path) => path?.Path; [return: NotNullIfNotNull(nameof(path))]
public static implicit operator string?(LongPath? path) => path?.Path;
[JsonIgnore] [JsonIgnore]
public string ShortPathName public string ShortPathName
@ -127,8 +131,6 @@ namespace FileManager
//for newly-created entries in ther file system. Existing entries made while //for newly-created entries in ther file system. Existing entries made while
//8dot3 names were disabled will not be reachable by short paths. //8dot3 names were disabled will not be reachable by short paths.
if (Path is null) return null;
StringBuilder shortPathBuffer = new(MaxPathLength); StringBuilder shortPathBuffer = new(MaxPathLength);
GetShortPathName(Path, shortPathBuffer, MaxPathLength); GetShortPathName(Path, shortPathBuffer, MaxPathLength);
return shortPathBuffer.ToString(); return shortPathBuffer.ToString();
@ -141,7 +143,6 @@ namespace FileManager
get get
{ {
if (!IsWindows) return Path; if (!IsWindows) return Path;
if (Path is null) return null;
StringBuilder longPathBuffer = new(MaxPathLength); StringBuilder longPathBuffer = new(MaxPathLength);
GetLongPathName(Path, longPathBuffer, MaxPathLength); GetLongPathName(Path, longPathBuffer, MaxPathLength);
@ -156,17 +157,18 @@ namespace FileManager
{ {
if (!IsWindows) return Path; if (!IsWindows) return Path;
return return
Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length) Path.StartsWith(WIN_LONG_PATH_PREFIX)
:Path; ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
: Path;
} }
} }
public override string ToString() => Path; public override string ToString() => Path;
public override int GetHashCode() => Path.GetHashCode(); public override int GetHashCode() => Path.GetHashCode();
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path; public override bool Equals(object? obj) => obj is LongPath other && Path == other.Path;
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2); public static bool operator ==(LongPath? path1, LongPath? path2) => path1?.Equals(path2) is true;
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2); public static bool operator !=(LongPath? path1, LongPath? path2) => path1 is null || path2 is null || !path1.Equals(path2);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
internal interface IClosingPropertyTag : IPropertyTag internal interface IClosingPropertyTag : IPropertyTag
@ -17,7 +19,7 @@ internal interface IClosingPropertyTag : IPropertyTag
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param> /// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag); bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
} }
public class ConditionalTagCollection<TClass> : TagCollection public class ConditionalTagCollection<TClass> : TagCollection
@ -37,6 +39,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
private class ConditionalTag : TagBase, IClosingPropertyTag private class ConditionalTag : TagBase, IClosingPropertyTag
{ {
public override Regex NameMatcher { get; }
public Regex NameCloseMatcher { get; } public Regex NameCloseMatcher { get; }
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression) public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
@ -46,7 +49,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options); NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
} }
public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag) public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
{ {
var match = NameCloseMatcher.Match(templateString); var match = NameCloseMatcher.Match(templateString);
if (match.Success) if (match.Success)

View File

@ -1,19 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
public class NamingTemplate public class NamingTemplate
{ {
public string TemplateText { get; private set; } public string TemplateText { get; private set; } = string.Empty;
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse; public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName); public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
public IEnumerable<string> Warnings => errors.Concat(warnings); public IEnumerable<string> Warnings => errors.Concat(warnings);
public IEnumerable<string> Errors => errors; public IEnumerable<string> Errors => errors;
private Delegate templateToString; private Delegate? templateToString;
private readonly List<string> warnings = new(); private readonly List<string> warnings = new();
private readonly List<string> errors = new(); private readonly List<string> errors = new();
private readonly IEnumerable<TagCollection> TagCollections; private readonly IEnumerable<TagCollection> TagCollections;
@ -30,6 +32,9 @@ public class NamingTemplate
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
public TemplatePart Evaluate(params object[] propertyClasses) public TemplatePart Evaluate(params object[] propertyClasses)
{ {
if (templateToString is null)
throw new InvalidOperationException();
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
// First parameter is "this", so ignore it. // First parameter is "this", so ignore it.
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1); var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
@ -39,7 +44,7 @@ public class NamingTemplate
if (args.Length != delegateArgTypes.Count()) if (args.Length != delegateArgTypes.Count())
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart; return (templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
} }
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary> /// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
@ -69,7 +74,7 @@ public class NamingTemplate
} }
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary> /// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
private static Expression GetExpressionTree(BinaryNode node) private static Expression GetExpressionTree(BinaryNode? node)
{ {
if (node is null) return TemplatePart.Blank; if (node is null) return TemplatePart.Blank;
else if (node.IsValue) return node.Expression; else if (node.IsValue) return node.Expression;
@ -81,10 +86,10 @@ public class NamingTemplate
} }
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary> /// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
private BinaryNode IntermediateParse(string templateString) private BinaryNode IntermediateParse(string? templateString)
{ {
if (templateString is null) if (templateString is null)
throw new NullReferenceException(ERROR_NULL_IS_INVALID); throw new ArgumentException(ERROR_NULL_IS_INVALID);
else if (string.IsNullOrEmpty(templateString)) else if (string.IsNullOrEmpty(templateString))
warnings.Add(WARNING_EMPTY); warnings.Add(WARNING_EMPTY);
else if (string.IsNullOrWhiteSpace(templateString)) else if (string.IsNullOrWhiteSpace(templateString))
@ -93,12 +98,12 @@ public class NamingTemplate
TemplateText = templateString; TemplateText = templateString;
BinaryNode topNode = BinaryNode.CreateRoot(); BinaryNode topNode = BinaryNode.CreateRoot();
BinaryNode currentNode = topNode; BinaryNode? currentNode = topNode;
List<char> literalChars = new(); List<char> literalChars = new();
while (templateString.Length > 0) while (templateString.Length > 0)
{ {
if (StartsWith(templateString, out string exactPropertyName, out var propertyTag, out var valueExpression)) if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
{ {
checkAndAddLiterals(); checkAndAddLiterals();
@ -116,7 +121,7 @@ public class NamingTemplate
{ {
checkAndAddLiterals(); checkAndAddLiterals();
BinaryNode lastParenth = currentNode; BinaryNode? lastParenth = currentNode;
while (lastParenth?.IsConditional is false) while (lastParenth?.IsConditional is false)
lastParenth = lastParenth.Parent; lastParenth = lastParenth.Parent;
@ -168,7 +173,7 @@ public class NamingTemplate
} }
} }
private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression) private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
{ {
foreach (var pc in TagCollections) foreach (var pc in TagCollections)
{ {
@ -182,7 +187,7 @@ public class NamingTemplate
return false; return false;
} }
private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag) private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
{ {
foreach (var pc in TagCollections) foreach (var pc in TagCollections)
{ {
@ -198,36 +203,36 @@ public class NamingTemplate
private class BinaryNode private class BinaryNode
{ {
public string Name { get; } public string Name { get; }
public BinaryNode Parent { get; private set; } public BinaryNode? Parent { get; private set; }
public BinaryNode RightChild { get; private set; } public BinaryNode? RightChild { get; private set; }
public BinaryNode LeftChild { get; private set; } public BinaryNode? LeftChild { get; private set; }
public Expression Expression { get; private init; } public Expression Expression { get; }
public bool IsConditional { get; private init; } = false; public bool IsConditional { get; private init; } = false;
public bool IsValue { get; private init; } = false; public bool IsValue { get; private init; } = false;
public static BinaryNode CreateRoot() => new("Root"); public static BinaryNode CreateRoot() => new("Root", Expression.Empty());
public static BinaryNode CreateValue(string literal) => new("Literal") public static BinaryNode CreateValue(string literal)
{ => new("Literal", TemplatePart.CreateLiteral(literal))
IsValue = true, {
Expression = TemplatePart.CreateLiteral(literal) IsValue = true
}; };
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property)
{ => new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property))
IsValue = true, {
Expression = TemplatePart.CreateProperty(templateTag, property) IsValue = true
}; };
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property)
{ => new(templateTag.TagName, property)
IsConditional = true, {
Expression = property IsConditional = true
}; };
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
{ {
var newNode = new BinaryNode("Concatenation") var newNode = new BinaryNode("Concatenation", Expression.Empty())
{ {
LeftChild = left, LeftChild = left,
RightChild = right RightChild = right
@ -237,7 +242,12 @@ public class NamingTemplate
return newNode; return newNode;
} }
private BinaryNode(string name) => Name = name; private BinaryNode(string name, Expression expression)
{
Name = name;
Expression = expression;
}
public override string ToString() => Name; public override string ToString() => Name;
public BinaryNode AddNewNode(BinaryNode newNode) public BinaryNode AddNewNode(BinaryNode newNode)

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString); public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
@ -37,7 +38,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default /// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param> /// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty> formatter = null) public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
where TProperty : struct where TProperty : struct
=> RegisterWithFormatter(templateTag, propertyGetter, formatter); => RegisterWithFormatter(templateTag, propertyGetter, formatter);
@ -59,7 +60,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default /// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param> /// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty> formatter = null) public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
=> RegisterWithFormatter(templateTag, propertyGetter, formatter); => RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary> /// <summary>
@ -72,14 +73,15 @@ public class PropertyTagCollection<TClass> : TagCollection
=> RegisterWithToString(templateTag, propertyGetter, toString); => RegisterWithToString(templateTag, propertyGetter, toString);
private void RegisterWithFormatter<TProperty, TPropertyValue> private void RegisterWithFormatter<TProperty, TPropertyValue>
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter) (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue>? formatter)
{ {
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
formatter ??= GetDefaultFormatter<TPropertyValue>();
if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null) if (formatter is null)
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc)); AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
else else
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter)); AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
@ -97,7 +99,7 @@ public class PropertyTagCollection<TClass> : TagCollection
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? ""; private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
private PropertyFormatter<T> GetDefaultFormatter<T>() private PropertyFormatter<T>? GetDefaultFormatter<T>()
{ {
try try
{ {
@ -109,6 +111,7 @@ public class PropertyTagCollection<TClass> : TagCollection
private class PropertyTag<TPropertyValue> : TagBase private class PropertyTag<TPropertyValue> : TagBase
{ {
public override Regex NameMatcher { get; }
private Func<Expression, string, Expression> CreateToStringExpression { get; } private Func<Expression, string, Expression> CreateToStringExpression { get; }
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter) public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
internal interface IPropertyTag internal interface IPropertyTag
@ -22,13 +24,13 @@ internal interface IPropertyTag
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param> /// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
bool StartsWith(string templateString, out string exactName, out Expression propertyValue); bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
} }
internal abstract class TagBase : IPropertyTag internal abstract class TagBase : IPropertyTag
{ {
public ITemplateTag TemplateTag { get; } public ITemplateTag TemplateTag { get; }
public Regex NameMatcher { get; protected init; } public abstract Regex NameMatcher { get; }
public Type ReturnType => ValueExpression.Type; public Type ReturnType => ValueExpression.Type;
protected Expression ValueExpression { get; } protected Expression ValueExpression { get; }
@ -43,7 +45,7 @@ internal abstract class TagBase : IPropertyTag
/// <param name="formatter">The optional format string in the match inside the square brackets</param> /// <param name="formatter">The optional format string in the match inside the square brackets</param>
protected abstract Expression GetTagExpression(string exactName, string formatter); protected abstract Expression GetTagExpression(string exactName, string formatter);
public bool StartsWith(string templateString, out string exactName, out Expression propertyValue) public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
{ {
var match = NameMatcher.Match(templateString); var match = NameMatcher.Match(templateString);
if (match.Success) if (match.Success)

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary> /// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
@ -32,7 +34,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param> /// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns> /// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue) internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue)
{ {
foreach (var p in PropertyTags) foreach (var p in PropertyTags)
{ {
@ -57,7 +59,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param> /// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag) internal bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
{ {
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>()) foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
{ {

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary> /// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
@ -15,13 +16,13 @@ public class TemplatePart : IEnumerable<TemplatePart>
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is /// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
/// a registered property, otherwise <see cref="null"/> for string literals. </summary> /// a registered property, otherwise <see cref="null"/> for string literals. </summary>
public ITemplateTag TemplateTag { get; } public ITemplateTag? TemplateTag { get; }
/// <summary>The evaluated string.</summary> /// <summary>The evaluated string.</summary>
public string Value { get; } public string Value { get; }
private TemplatePart previous; private TemplatePart? previous;
private TemplatePart next; private TemplatePart? next;
private TemplatePart(string name, string value) private TemplatePart(string name, string value)
{ {
TagName = name; TagName = name;
@ -53,14 +54,33 @@ public class TemplatePart : IEnumerable<TemplatePart>
private static Expression CreateExpression(string name, Expression value) private static Expression CreateExpression(string name, Expression value)
=> Expression.New(constructorInfo, Expression.Constant(name), value); => Expression.New(constructorInfo, Expression.Constant(name), value);
private static readonly ConstructorInfo constructorInfo private static readonly ConstructorInfo constructorInfo;
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) }); private static readonly ConstructorInfo tagTemplateConstructorInfo;
private static readonly MethodInfo addMethodInfo;
static TemplatePart()
{
var type = typeof(TemplatePart);
private static readonly ConstructorInfo tagTemplateConstructorInfo if (type.GetConstructor(
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) }); BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(string), typeof(string) }) is not ConstructorInfo c1)
throw new MissingMethodException(nameof(TemplatePart));
private static readonly MethodInfo addMethodInfo if (type.GetConstructor(
= typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) }); BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(ITemplateTag), typeof(string) }) is not ConstructorInfo c2)
throw new MissingMethodException(nameof(TemplatePart));
if (type.GetMethod(
nameof(Concatenate),
BindingFlags.NonPublic | BindingFlags.Static,
new Type[] { typeof(TemplatePart), typeof(TemplatePart) }) is not MethodInfo m1)
throw new MissingMethodException(nameof(Concatenate));
constructorInfo = c1;
tagTemplateConstructorInfo = c2;
addMethodInfo = m1;
}
public IEnumerator<TemplatePart> GetEnumerator() public IEnumerator<TemplatePart> GetEnumerator()
{ {

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
#nullable enable
namespace FileManager namespace FileManager
{ {
public class PersistentDictionary public class PersistentDictionary
@ -13,19 +15,19 @@ namespace FileManager
public bool IsReadOnly { get; } public bool IsReadOnly { get; }
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else // optimize for strings. expectation is most settings will be strings and a rare exception will be something else
private Dictionary<string, string> stringCache { get; } = new Dictionary<string, string>(); private Dictionary<string, string?> stringCache { get; } = new();
private Dictionary<string, object> objectCache { get; } = new Dictionary<string, object>(); private Dictionary<string, object?> objectCache { get; } = new();
public PersistentDictionary(string filepath, bool isReadOnly = false) public PersistentDictionary(string filepath, bool isReadOnly = false)
{ {
Filepath = filepath; Filepath = filepath;
IsReadOnly = isReadOnly; IsReadOnly = isReadOnly;
if (File.Exists(Filepath)) if (File.Exists(Filepath) || Path.GetDirectoryName(Filepath) is not string dirName)
return; return;
// will create any missing directories, incl subdirectories. if all already exist: no action // will create any missing directories, incl subdirectories. if all already exist: no action
Directory.CreateDirectory(Path.GetDirectoryName(filepath)); Directory.CreateDirectory(dirName);
if (IsReadOnly) if (IsReadOnly)
return; return;
@ -33,13 +35,14 @@ namespace FileManager
createNewFile(); createNewFile();
} }
public string GetString(string propertyName, string defaultValue = null) [return: NotNullIfNotNull(nameof(defaultValue))]
public string? GetString(string propertyName, string? defaultValue = null)
{ {
if (!stringCache.ContainsKey(propertyName)) if (!stringCache.ContainsKey(propertyName))
{ {
var jObject = readFile(); var jObject = readFile();
if (jObject.ContainsKey(propertyName)) if (jObject.ContainsKey(propertyName))
stringCache[propertyName] = jObject[propertyName].Value<string>(); stringCache[propertyName] = jObject[propertyName]?.Value<string>();
else else
stringCache[propertyName] = defaultValue; stringCache[propertyName] = defaultValue;
} }
@ -47,7 +50,8 @@ namespace FileManager
return stringCache[propertyName]; return stringCache[propertyName];
} }
public T GetNonString<T>(string propertyName, T defaultValue = default) [return: NotNullIfNotNull(nameof(defaultValue))]
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
{ {
var obj = GetObject(propertyName); var obj = GetObject(propertyName);
@ -72,21 +76,21 @@ namespace FileManager
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}"); throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
} }
public object GetObject(string propertyName) public object? GetObject(string propertyName)
{ {
if (!objectCache.ContainsKey(propertyName)) if (!objectCache.ContainsKey(propertyName))
{ {
var jObject = readFile(); var jObject = readFile();
if (!jObject.ContainsKey(propertyName)) if (!jObject.ContainsKey(propertyName))
return null; return null;
objectCache[propertyName] = jObject[propertyName].Value<object>(); objectCache[propertyName] = jObject[propertyName]?.Value<object>();
} }
return objectCache[propertyName]; return objectCache[propertyName];
} }
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}"); public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
public string GetStringFromJsonPath(string jsonPath) public string? GetStringFromJsonPath(string jsonPath)
{ {
if (!stringCache.ContainsKey(jsonPath)) if (!stringCache.ContainsKey(jsonPath))
{ {
@ -96,7 +100,7 @@ namespace FileManager
var token = jObject.SelectToken(jsonPath); var token = jObject.SelectToken(jsonPath);
if (token is null) if (token is null)
return null; return null;
stringCache[jsonPath] = (string)token; stringCache[jsonPath] = token.Value<string>();
} }
catch catch
{ {
@ -110,7 +114,7 @@ namespace FileManager
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName); public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
private object locker { get; } = new object(); private object locker { get; } = new object();
public void SetString(string propertyName, string newValue) public void SetString(string propertyName, string? newValue)
{ {
// only do this check in string cache, NOT object cache // only do this check in string cache, NOT object cache
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue) if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
@ -122,7 +126,7 @@ namespace FileManager
writeFile(propertyName, newValue); writeFile(propertyName, newValue);
} }
public void SetNonString(string propertyName, object newValue) public void SetNonString(string propertyName, object? newValue)
{ {
// set cache // set cache
objectCache[propertyName] = newValue; objectCache[propertyName] = newValue;
@ -160,7 +164,7 @@ namespace FileManager
return success; return success;
} }
private void writeFile(string propertyName, JToken newValue) private void writeFile(string propertyName, JToken? newValue)
{ {
if (IsReadOnly) if (IsReadOnly)
return; return;
@ -190,7 +194,7 @@ namespace FileManager
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
/// <returns>Value was changed</returns> /// <returns>Value was changed</returns>
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
{ {
if (IsReadOnly) if (IsReadOnly)
return false; return false;
@ -242,7 +246,7 @@ namespace FileManager
return true; return true;
} }
private static string formatValueForLog(string value) private static string formatValueForLog(string? value)
=> value is null ? "[null]" => value is null ? "[null]"
: string.IsNullOrEmpty(value) ? "[empty]" : string.IsNullOrEmpty(value) ? "[empty]"
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]" : string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
@ -283,7 +287,6 @@ namespace FileManager
private void createNewFile() private void createNewFile()
{ {
File.WriteAllText(Filepath, "{}"); File.WriteAllText(Filepath, "{}");
System.Threading.Thread.Sleep(100);
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
#nullable enable
namespace FileManager namespace FileManager
{ {
public record Replacement public record Replacement
@ -59,7 +60,7 @@ namespace FileManager
[JsonConverter(typeof(ReplacementCharactersConverter))] [JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters public class ReplacementCharacters
{ {
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count) if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
{ {
@ -173,7 +174,7 @@ namespace FileManager
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray(); }).ToArray();
public IReadOnlyList<Replacement> Replacements { get; init; } required public IReadOnlyList<Replacement> Replacements { get; init; }
private string DefaultReplacement => Replacements[0].ReplacementString; private string DefaultReplacement => Replacements[0].ReplacementString;
private Replacement ForwardSlash => Replacements[1]; private Replacement ForwardSlash => Replacements[1];
private Replacement BackSlash => Replacements[2]; private Replacement BackSlash => Replacements[2];
@ -298,12 +299,14 @@ namespace FileManager
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
=> objectType == typeof(ReplacementCharacters); => objectType == typeof(ReplacementCharacters);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var jObj = JObject.Load(reader); var jObj = JObject.Load(reader);
var replaceArr = jObj[nameof(Replacement)]; var replaceArr = jObj[nameof(Replacement)];
IReadOnlyList<Replacement> dict = replaceArr var dict
.ToObject<Replacement[]>().ToList(); = replaceArr?.ToObject<Replacement[]>()?.ToList()
?? ReplacementCharacters.Default.Replacements;
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default. //If not, reset to default.
@ -325,9 +328,10 @@ namespace FileManager
return new ReplacementCharacters { Replacements = dict }; return new ReplacementCharacters { Replacements = dict };
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
ReplacementCharacters replacements = (ReplacementCharacters)value; if (value is not ReplacementCharacters replacements)
return;
var propertyNames = replacements.Replacements var propertyNames = replacements.Replacements
.Select(JObject.FromObject).ToList(); .Select(JObject.FromObject).ToList();

View File

@ -9,11 +9,12 @@ using System.Threading.Tasks;
using System.Threading; using System.Threading;
using FileManager; using FileManager;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public abstract class AudibleFileStorage public abstract class AudibleFileStorage
{ {
protected abstract LongPath GetFilePathCustom(string productId); protected abstract LongPath? GetFilePathCustom(string productId);
protected abstract List<LongPath> GetFilePathsCustom(string productId); protected abstract List<LongPath> GetFilePathsCustom(string productId);
#region static #region static
@ -57,7 +58,7 @@ namespace LibationFileManager
regexTemplate = $@"{{0}}.*?\.({extAggr})$"; regexTemplate = $@"{{0}}.*?\.({extAggr})$";
} }
protected LongPath GetFilePath(string productId) protected LongPath? GetFilePath(string productId)
{ {
// primary lookup // primary lookup
var cachedFile = FilePathCache.GetFirstPath(productId, FileType); var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
@ -87,7 +88,7 @@ namespace LibationFileManager
{ {
internal AaxcFileStorage() : base(FileType.AAXC) { } internal AaxcFileStorage() : base(FileType.AAXC) { }
protected override LongPath GetFilePathCustom(string productId) protected override LongPath? GetFilePathCustom(string productId)
=> GetFilePathsCustom(productId).FirstOrDefault(); => GetFilePathsCustom(productId).FirstOrDefault();
protected override List<LongPath> GetFilePathsCustom(string productId) protected override List<LongPath> GetFilePathsCustom(string productId)
@ -104,9 +105,9 @@ namespace LibationFileManager
public class AudioFileStorage : AudibleFileStorage public class AudioFileStorage : AudibleFileStorage
{ {
internal AudioFileStorage() : base(FileType.Audio) internal AudioFileStorage() : base(FileType.Audio)
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); => BookDirectoryFiles ??= newBookDirectoryFiles();
private static BackgroundFileSystem BookDirectoryFiles { get; set; } private static BackgroundFileSystem? BookDirectoryFiles { get; set; }
private static object bookDirectoryFilesLocker { get; } = new(); private static object bookDirectoryFilesLocker { get; } = new();
private static EnumerationOptions enumerationOptions { get; } = new() private static EnumerationOptions enumerationOptions { get; } = new()
{ {
@ -115,17 +116,20 @@ namespace LibationFileManager
MatchCasing = MatchCasing.CaseInsensitive MatchCasing = MatchCasing.CaseInsensitive
}; };
protected override LongPath GetFilePathCustom(string productId) protected override LongPath? GetFilePathCustom(string productId)
=> GetFilePathsCustom(productId).FirstOrDefault(); => GetFilePathsCustom(productId).FirstOrDefault();
protected override List<LongPath> GetFilePathsCustom(string productId) private static BackgroundFileSystem newBookDirectoryFiles()
=> new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
protected override List<LongPath> GetFilePathsCustom(string productId)
{ {
// If user changed the BooksDirectory: reinitialize // If user changed the BooksDirectory: reinitialize
lock (bookDirectoryFilesLocker) lock (bookDirectoryFilesLocker)
if (BooksDirectory != BookDirectoryFiles.RootDirectory) if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); BookDirectoryFiles = newBookDirectoryFiles();
var regex = GetBookSearchRegex(productId); var regex = GetBookSearchRegex(productId);
//Find all extant files matching the productId //Find all extant files matching the productId
//using both the file system and the file path cache //using both the file system and the file path cache
@ -138,9 +142,16 @@ namespace LibationFileManager
.ToList(); .ToList();
} }
public void Refresh() => BookDirectoryFiles.RefreshFiles(); public void Refresh()
{
if (BookDirectoryFiles is null)
lock (bookDirectoryFilesLocker)
BookDirectoryFiles = newBookDirectoryFiles();
else
BookDirectoryFiles?.RefreshFiles();
}
public LongPath GetPath(string productId) => GetFilePath(productId); public LongPath? GetPath(string productId) => GetFilePath(productId);
public static async IAsyncEnumerable<FilePathCache.CacheEntry> FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken) public static async IAsyncEnumerable<FilePathCache.CacheEntry> FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
@ -151,7 +162,7 @@ namespace LibationFileManager
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
yield break; yield break;
FilePathCache.CacheEntry audioFile = default; FilePathCache.CacheEntry? audioFile = default;
try try
{ {

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
[Flags] [Flags]
@ -20,7 +21,7 @@ namespace LibationFileManager
public static bool IsWindows { get; } = OperatingSystem.IsWindows(); public static bool IsWindows { get; } = OperatingSystem.IsWindows();
public static bool IsLinux { get; } = OperatingSystem.IsLinux(); public static bool IsLinux { get; } = OperatingSystem.IsLinux();
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS(); public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
public static Version LibationVersion { get; private set; } public static Version? LibationVersion { get; private set; }
public static void SetLibationVersion(Version version) => LibationVersion = version; public static void SetLibationVersion(Version version) => LibationVersion = version;
public static OS OS { get; } public static OS OS { get; }

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
@ -29,8 +30,7 @@ namespace LibationFileManager
} }
.AsReadOnly(); .AsReadOnly();
public static string GetHelpText(string settingName) public static string? GetHelpText(string settingName)
=> HelpText.TryGetValue(settingName, out var value) ? value : null; => HelpText.TryGetValue(settingName, out var value) ? value : null;
} }
} }

View File

@ -5,11 +5,12 @@ using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
{ {
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk); public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!;
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}"; public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY)); public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
@ -36,7 +37,7 @@ namespace LibationFileManager
LibationFiles = 5 LibationFiles = 5
} }
// use func calls so we always get the latest value of LibationFiles // use func calls so we always get the latest value of LibationFiles
private static List<(KnownDirectories directory, Func<string> getPathFunc)> directoryOptionsPaths { get; } = new() private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
{ {
(KnownDirectories.None, () => null), (KnownDirectories.None, () => null),
(KnownDirectories.UserProfile, () => UserProfile), (KnownDirectories.UserProfile, () => UserProfile),
@ -47,7 +48,7 @@ namespace LibationFileManager
// also, keep this at bottom of this list // also, keep this at bottom of this list
(KnownDirectories.LibationFiles, () => libationFilesPathCache) (KnownDirectories.LibationFiles, () => libationFilesPathCache)
}; };
public static string GetKnownDirectoryPath(KnownDirectories directory) public static string? GetKnownDirectoryPath(KnownDirectories directory)
{ {
var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory); var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory);
return dirFunc == default ? null : dirFunc.getPathFunc(); return dirFunc == default ? null : dirFunc.getPathFunc();

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Serilog; using Serilog;
using Dinah.Core.Logging; using Dinah.Core.Logging;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
@ -44,7 +45,7 @@ namespace LibationFileManager
} }
} }
private static string libationFilesPathCache { get; set; } private static string? libationFilesPathCache { get; set; }
/// <summary> /// <summary>
/// Try to find appsettings.json in the following locations: /// Try to find appsettings.json in the following locations:
@ -124,7 +125,10 @@ namespace LibationFileManager
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid // do not check whether directory exists. special/meta directory (eg: AppDir) is valid
// verify from live file. no try/catch. want failures to be visible // verify from live file. no try/catch. want failures to be visible
var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile)); var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile));
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string valueFinal)
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {AppsettingsJsonFile}");
return valueFinal; return valueFinal;
} }

View File

@ -1,19 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Dinah.Core;
using Dinah.Core.Logging; using Dinah.Core.Logging;
using FileManager; using FileManager;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
{ {
private IConfigurationRoot configuration; private IConfigurationRoot? configuration;
public void ConfigureLogging() public void ConfigureLogging()
{ {
@ -31,20 +30,20 @@ namespace LibationFileManager
{ {
get get
{ {
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel"); var logLevelStr = Settings.GetStringFromJsonPath("Serilog", "MinimumLevel");
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information; return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
} }
set set
{ {
OnPropertyChanging(nameof(LogLevel), LogLevel, value); OnPropertyChanging(nameof(LogLevel), LogLevel, value);
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString()); var valueWasChanged = Settings.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
if (!valueWasChanged) if (!valueWasChanged)
{ {
Log.Logger.Debug("LogLevel.set attempt. No change"); Log.Logger.Debug("LogLevel.set attempt. No change");
return; return;
} }
configuration.Reload(); configuration?.Reload();
OnPropertyChanged(nameof(LogLevel), value); OnPropertyChanged(nameof(LogLevel), value);

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -8,6 +9,7 @@ using FileManager;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
@ -18,34 +20,52 @@ namespace LibationFileManager
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
// exceptions: appsettings.json, LibationFiles dir, Settings.json // exceptions: appsettings.json, LibationFiles dir, Settings.json
private PersistentDictionary persistentDictionary; private PersistentDictionary? persistentDictionary;
public bool RemoveProperty(string propertyName) => persistentDictionary.RemoveProperty(propertyName); private PersistentDictionary Settings
{
get
{
if (persistentDictionary is null)
throw new InvalidOperationException($"{nameof(persistentDictionary)} must first be set by accessing {nameof(LibationFiles)} or calling {nameof(SettingsFileIsValid)}");
return persistentDictionary;
}
}
public T GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName, defaultValue); public bool RemoveProperty(string propertyName) => Settings.RemoveProperty(propertyName);
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
public string GetString(string defaultValue = null, [CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName, defaultValue); [return: NotNullIfNotNull(nameof(defaultValue))]
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "") public T? GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "")
=> Settings.GetNonString(propertyName, defaultValue);
[return: NotNullIfNotNull(nameof(defaultValue))]
public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "")
=> Settings.GetString(propertyName, defaultValue);
public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName);
public void SetNonString(object? newValue, [CallerMemberName] string propertyName = "")
{ {
var existing = getExistingValue(propertyName); var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return; if (existing?.Equals(newValue) is true) return;
OnPropertyChanging(propertyName, existing, newValue); OnPropertyChanging(propertyName, existing, newValue);
persistentDictionary.SetNonString(propertyName, newValue); Settings.SetNonString(propertyName, newValue);
OnPropertyChanged(propertyName, newValue); OnPropertyChanged(propertyName, newValue);
} }
public void SetString(string newValue, [CallerMemberName] string propertyName = "") public void SetString(string? newValue, [CallerMemberName] string propertyName = "")
{ {
var existing = getExistingValue(propertyName); var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return; if (existing?.Equals(newValue) is true) return;
OnPropertyChanging(propertyName, existing, newValue); OnPropertyChanging(propertyName, existing, newValue);
persistentDictionary.SetString(propertyName, newValue); Settings.SetString(propertyName, newValue);
OnPropertyChanged(propertyName, newValue); OnPropertyChanged(propertyName, newValue);
} }
private object getExistingValue(string propertyName) private object? getExistingValue(string propertyName)
{ {
var property = GetType().GetProperty(propertyName); var property = GetType().GetProperty(propertyName);
if (property is not null) return property.GetValue(this); if (property is not null) return property.GetValue(this);
@ -53,16 +73,16 @@ namespace LibationFileManager
} }
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) public void SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
{ {
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); var settingWasChanged = Settings.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
if (settingWasChanged) if (settingWasChanged)
configuration?.Reload(); configuration?.Reload();
} }
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
public static string GetDescription(string propertyName) public static string? GetDescription(string propertyName)
{ {
var attribute = typeof(Configuration) var attribute = typeof(Configuration)
.GetProperty(propertyName) .GetProperty(propertyName)
@ -73,7 +93,7 @@ namespace LibationFileManager
return attribute?.Description; return attribute?.Description;
} }
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); public bool Exists(string propertyName) => Settings.Exists(propertyName);
[Description("Set cover art as the folder's icon.")] [Description("Set cover art as the folder's icon.")]
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); } public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
@ -91,7 +111,7 @@ namespace LibationFileManager
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); } public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Location for book storage. Includes destination of newly liberated books")] [Description("Location for book storage. Includes destination of newly liberated books")]
public LongPath Books { get => GetString(); set => SetString(value); } public LongPath? Books { get => GetString(); set => SetString(value); }
[Description("Overwrite existing files if they already exist?")] [Description("Overwrite existing files if they already exist?")]
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); } public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
@ -9,17 +10,17 @@ namespace LibationFileManager
* and be sure to clone it before returning. This allows Configuration to * and be sure to clone it before returning. This allows Configuration to
* accurately detect if any of the Dictionary's elements have changed. * accurately detect if any of the Dictionary's elements have changed.
*/ */
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue> private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull
{ {
public EquatableDictionary() { } public EquatableDictionary() { }
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { } public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
public EquatableDictionary<TKey, TValue> Clone() => new(this); public EquatableDictionary<TKey, TValue> Clone() => new(this);
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (obj is Dictionary<TKey, TValue> dic && Count == dic.Count) if (obj is Dictionary<TKey, TValue> dic && Count == dic.Count)
{ {
foreach (var pair in this) foreach (var pair in this)
if (!dic.TryGetValue(pair.Key, out var value) || !pair.Value.Equals(value)) if (!dic.TryGetValue(pair.Key, out var value) || pair.Value?.Equals(value) is not true)
return false; return false;
return true; return true;

View File

@ -4,7 +4,7 @@ using System.Linq;
using Dinah.Core; using Dinah.Core;
using FileManager; using FileManager;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration : PropertyChangeFilter public partial class Configuration : PropertyChangeFilter
@ -24,9 +24,12 @@ namespace LibationFileManager
if (!Directory.Exists(booksDir)) if (!Directory.Exists(booksDir))
{ {
if (Path.GetDirectoryName(settingsFile) is not string dir)
throw new DirectoryNotFoundException(settingsFile);
//"Books" is not null, so setup has already been run. //"Books" is not null, so setup has already been run.
//Since Books can't be found, try to create it in Libation settings folder //Since Books can't be found, try to create it in Libation settings folder
booksDir = Path.Combine(Path.GetDirectoryName(settingsFile), nameof(Books)); booksDir = Path.Combine(dir, nameof(Books));
try try
{ {
Directory.CreateDirectory(booksDir); Directory.CreateDirectory(booksDir);

View File

@ -6,6 +6,7 @@ using Dinah.Core.Collections.Immutable;
using FileManager; using FileManager;
using Newtonsoft.Json; using Newtonsoft.Json;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public static class FilePathCache public static class FilePathCache
@ -14,8 +15,8 @@ namespace LibationFileManager
private const string FILENAME = "FileLocations.json"; private const string FILENAME = "FileLocations.json";
public static event EventHandler<CacheEntry> Inserted; public static event EventHandler<CacheEntry>? Inserted;
public static event EventHandler<CacheEntry> Removed; public static event EventHandler<CacheEntry>? Removed;
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>(); private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
@ -51,7 +52,7 @@ namespace LibationFileManager
.Select(entry => (entry.FileType, entry.Path)) .Select(entry => (entry.FileType, entry.Path))
.ToList(); .ToList();
public static LongPath GetFirstPath(string id, FileType type) public static LongPath? GetFirstPath(string id, FileType type)
=> getEntries(entry => entry.Id == id && entry.FileType == type) => getEntries(entry => entry.Id == id && entry.FileType == type)
?.FirstOrDefault() ?.FirstOrDefault()
?.Path; ?.Path;

View File

@ -2,9 +2,9 @@
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
#nullable enable
public interface IInteropFunctions public interface IInteropFunctions
{ {
/// <summary> /// <summary>

View File

@ -5,11 +5,12 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Dinah.Core; using Dinah.Core;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public static class InteropFactory public static class InteropFactory
{ {
public static Type InteropFunctionsType { get; } public static Type? InteropFunctionsType { get; }
public static IInteropFunctions Create() => _create(); public static IInteropFunctions Create() => _create();
@ -17,13 +18,17 @@ namespace LibationFileManager
//public static IInteropFunctions Create(string str, int i) => _create(str, i); //public static IInteropFunctions Create(string str, int i) => _create(str, i);
//public static IInteropFunctions Create(params object[] values) => _create(values); //public static IInteropFunctions Create(params object[] values) => _create(values);
private static IInteropFunctions instance { get; set; } private static IInteropFunctions? instance { get; set; }
private static IInteropFunctions _create(params object[] values) private static IInteropFunctions _create(params object[] values)
{ {
instance ??= instance ??=
InteropFunctionsType is null InteropFunctionsType is null
? new NullInteropFunctions() ? new NullInteropFunctions()
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions; : Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
if (instance is null)
throw new TypeLoadException();
return instance; return instance;
} }
@ -66,7 +71,7 @@ namespace LibationFileManager
.GetTypes() .GetTypes()
.FirstOrDefault(type.IsAssignableFrom); .FirstOrDefault(type.IsAssignableFrom);
} }
private static string getOSConfigApp() private static string? getOSConfigApp()
{ {
// find '*ConfigApp.dll' files // find '*ConfigApp.dll' files
var appName = var appName =
@ -76,8 +81,8 @@ namespace LibationFileManager
return appName; return appName;
} }
private static Dictionary<string, Assembly> lowEffortCache { get; } = new(); private static Dictionary<string, Assembly?> lowEffortCache { get; } = new();
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{ {
var asmName = new AssemblyName(args.Name); var asmName = new AssemblyName(args.Name);
var here = Configuration.ProcessDirectory; var here = Configuration.ProcessDirectory;
@ -97,7 +102,7 @@ namespace LibationFileManager
return assembly; return assembly;
} }
private static Assembly CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here) private static Assembly? CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
{ {
/* /*
* Find the requested assembly in the program files directory. * Find the requested assembly in the program files directory.

View File

@ -2,26 +2,27 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public class BookDto public class BookDto
{ {
public string AudibleProductId { get; set; } public string? AudibleProductId { get; set; }
public string Title { get; set; } public string? Title { get; set; }
public string Subtitle { get; set; } public string? Subtitle { get; set; }
public string TitleWithSubtitle { get; set; } public string? TitleWithSubtitle { get; set; }
public string Locale { get; set; } public string? Locale { get; set; }
public int? YearPublished { get; set; } public int? YearPublished { get; set; }
public IEnumerable<string> Authors { get; set; } public IEnumerable<string>? Authors { get; set; }
public string AuthorNames => string.Join(", ", Authors); public string? AuthorNames => Authors is null ? null : string.Join(", ", Authors);
public string FirstAuthor => Authors.FirstOrDefault(); public string? FirstAuthor => Authors?.FirstOrDefault();
public IEnumerable<string> Narrators { get; set; } public IEnumerable<string>? Narrators { get; set; }
public string NarratorNames => string.Join(", ", Narrators); public string? NarratorNames => Narrators is null? null: string.Join(", ", Narrators);
public string FirstNarrator => Narrators.FirstOrDefault(); public string? FirstNarrator => Narrators?.FirstOrDefault();
public string SeriesName { get; set; } public string? SeriesName { get; set; }
public float? SeriesNumber { get; set; } public float? SeriesNumber { get; set; }
public bool IsSeries => !string.IsNullOrEmpty(SeriesName); public bool IsSeries => !string.IsNullOrEmpty(SeriesName);
public bool IsPodcastParent { get; set; } public bool IsPodcastParent { get; set; }
@ -32,13 +33,13 @@ namespace LibationFileManager
public int Channels { get; set; } public int Channels { get; set; }
public DateTime FileDate { get; set; } = DateTime.Now; public DateTime FileDate { get; set; } = DateTime.Now;
public DateTime? DatePublished { get; set; } public DateTime? DatePublished { get; set; }
public string Language { get; set; } public string? Language { get; set; }
} }
public class LibraryBookDto : BookDto public class LibraryBookDto : BookDto
{ {
public DateTime? DateAdded { get; set; } public DateTime? DateAdded { get; set; }
public string Account { get; set; } public string? Account { get; set; }
public string AccountNickname { get; set; } public string? AccountNickname { get; set; }
} }
} }

View File

@ -5,13 +5,16 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
internal partial class NameListFormat internal partial class NameListFormat
{ {
public static string Formatter(ITemplateTag _, IEnumerable<string> names, string formatString) public static string Formatter(ITemplateTag _, IEnumerable<string>? names, string formatString)
{ {
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix)); if (names is null) return "";
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
var sortedNames = Sort(humanNames, formatString); var sortedNames = Sort(humanNames, formatString);
var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}"); var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}");

View File

@ -2,7 +2,6 @@
using System.Diagnostics; using System.Diagnostics;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public class NullInteropFunctions : IInteropFunctions public class NullInteropFunctions : IInteropFunctions

View File

@ -6,18 +6,25 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public enum PictureSize { Native, _80x80 = 80, _300x300 = 300, _500x500 = 500 } public enum PictureSize { Native, _80x80 = 80, _300x300 = 300, _500x500 = 500 }
public class PictureCachedEventArgs : EventArgs public class PictureCachedEventArgs : EventArgs
{ {
public PictureDefinition Definition { get; internal set; } public PictureDefinition Definition { get; }
public byte[] Picture { get; internal set; } public byte[] Picture { get; }
internal PictureCachedEventArgs(PictureDefinition definition, byte[] picture)
{
Definition = definition;
Picture = picture;
}
} }
public struct PictureDefinition : IEquatable<PictureDefinition> public struct PictureDefinition : IEquatable<PictureDefinition>
{ {
public string PictureId { get; } public string PictureId { get; init; }
public PictureSize Size { get; } public PictureSize Size { get; init; }
public PictureDefinition(string pictureId, PictureSize pictureSize) public PictureDefinition(string pictureId, PictureSize pictureSize)
{ {
@ -45,7 +52,7 @@ namespace LibationFileManager
.Start(); .Start();
} }
public static event EventHandler<PictureCachedEventArgs> PictureCached; public static event EventHandler<PictureCachedEventArgs>? PictureCached;
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>(); private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
private static object cacheLocker { get; } = new object(); private static object cacheLocker { get; } = new object();
@ -112,7 +119,7 @@ namespace LibationFileManager
lock (cacheLocker) lock (cacheLocker)
cache[def] = bytes; cache[def] = bytes;
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes }); PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs(def, bytes));
} }
} }

View File

@ -5,30 +5,30 @@ using System.Linq;
using Dinah.Core.Collections.Generic; using Dinah.Core.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public static class QuickFilters public static class QuickFilters
{ {
public static event EventHandler Updated; public static event EventHandler? Updated;
internal class FilterState public static event EventHandler? UseDefaultChanged;
internal class FilterState
{ {
public bool UseDefault { get; set; } public bool UseDefault { get; set; }
public List<string> Filters { get; set; } = new List<string>(); public List<string> Filters { get; set; } = new List<string>();
} }
static FilterState inMemoryState { get; } = new FilterState();
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json"); public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json");
static QuickFilters()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (File.Exists(JsonFile))
inMemoryState = JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile));
}
public static event EventHandler UseDefaultChanged; // load json into memory. if file doesn't exist, nothing to do. save() will create if needed
static FilterState inMemoryState { get; }
= File.Exists(JsonFile) && JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile)) is FilterState inMemState
? inMemState
: new FilterState();
public static bool UseDefault public static bool UseDefault
{ {
get => inMemoryState.UseDefault; get => inMemoryState.UseDefault;
@ -43,7 +43,7 @@ namespace LibationFileManager
save(false); save(false);
} }
UseDefaultChanged?.Invoke(null, null); UseDefaultChanged?.Invoke(null, EventArgs.Empty);
} }
} }
@ -121,7 +121,7 @@ namespace LibationFileManager
} }
if (invokeUpdatedEvent) if (invokeUpdatedEvent)
Updated?.Invoke(null, null); Updated?.Invoke(null, EventArgs.Empty);
} }
} }
} }

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System; using System;
using System.IO; using System.IO;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public interface ITemplateEditor public interface ITemplateEditor
@ -14,14 +15,11 @@ namespace LibationFileManager
string DefaultTemplate { get; } string DefaultTemplate { get; }
string TemplateName { get; } string TemplateName { get; }
string TemplateDescription { get; } string TemplateDescription { get; }
Templates Folder { get; }
Templates File { get; }
Templates Name { get; }
Templates EditingTemplate { get; } Templates EditingTemplate { get; }
void SetTemplateText(string templateText); bool SetTemplateText(string templateText);
string GetFolderName(); string? GetFolderName();
string GetFileName(); string? GetFileName();
string GetName(); string? GetName();
} }
public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate, new() public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate, new()
@ -32,9 +30,9 @@ namespace LibationFileManager
public string DefaultTemplate { get; private init; } public string DefaultTemplate { get; private init; }
public string TemplateName { get; private init; } public string TemplateName { get; private init; }
public string TemplateDescription { get; private init; } public string TemplateDescription { get; private init; }
public Templates Folder { get; private set; } private Templates? Folder { get; set; }
public Templates File { get; private set; } private Templates? File { get; set; }
public Templates Name { get; private set; } private Templates? Name { get; set; }
public Templates EditingTemplate public Templates EditingTemplate
{ {
get => _editingTemplate; get => _editingTemplate;
@ -43,10 +41,14 @@ namespace LibationFileManager
private Templates _editingTemplate; private Templates _editingTemplate;
public void SetTemplateText(string templateText) public bool SetTemplateText(string templateText)
{ {
Templates.TryGetTemplate<T>(templateText, out var template); if (Templates.TryGetTemplate<T>(templateText, out var template))
EditingTemplate = template; {
EditingTemplate = template;
return true;
}
return false;
} }
private static readonly LibraryBookDto libraryBookDto private static readonly LibraryBookDto libraryBookDto
@ -80,7 +82,7 @@ namespace LibationFileManager
Title = "A Flight for Life" Title = "A Flight for Life"
}; };
public string GetFolderName() public string? GetFolderName()
{ {
/* /*
* Path must be rooted for windows to allow long file paths. This is * Path must be rooted for windows to allow long file paths. This is
@ -88,49 +90,49 @@ namespace LibationFileManager
* subdirectories. Without rooting, we won't be allowed to create a * subdirectories. Without rooting, we won't be allowed to create a
* relative path longer than MAX_PATH. * relative path longer than MAX_PATH.
*/ */
var dir = Folder.GetFilename(libraryBookDto, BaseDirectory, ""); var dir = Folder?.GetFilename(libraryBookDto, BaseDirectory, "");
if (dir is null) return null;
return Path.GetRelativePath(BaseDirectory, dir); return Path.GetRelativePath(BaseDirectory, dir);
} }
public string GetFileName() public string? GetFileName()
=> File.GetFilename(libraryBookDto, partFileProperties, "", ""); => File?.GetFilename(libraryBookDto, partFileProperties, "", "");
public string GetName() public string? GetName()
=> Name.GetName(libraryBookDto, partFileProperties); => Name?.GetName(libraryBookDto, partFileProperties);
private TemplateEditor(
Templates editingTemplate,
LongPath baseDirectory,
string defaultTemplate,
string templateName,
string templateDescription)
{
_editingTemplate = editingTemplate;
BaseDirectory = baseDirectory;
DefaultTemplate = defaultTemplate;
TemplateName = templateName;
TemplateDescription = templateDescription;
}
public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText) public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText)
{ {
Templates.TryGetTemplate<T>(templateText, out var template); if (!Templates.TryGetTemplate<T>(templateText, out var template))
throw new ArgumentException($"Failed to parse {nameof(templateText)}");
var templateEditor = new TemplateEditor<T> var templateEditor = new TemplateEditor<T>(template, baseDir, T.DefaultTemplate, T.Name, T.Description);
{
_editingTemplate = template,
BaseDirectory = baseDir,
DefaultTemplate = T.DefaultTemplate,
TemplateName = T.Name,
TemplateDescription = T.Description
};
if (!templateEditor.IsFolder && !templateEditor.IsFilePath) if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates"); throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
templateEditor.Folder = templateEditor.IsFolder ? template : Templates.Folder;
templateEditor.File = templateEditor.IsFolder ? Templates.File : template;
return templateEditor; return templateEditor;
} }
public static ITemplateEditor CreateNameEditor(string templateText) public static ITemplateEditor CreateNameEditor(string templateText)
{ {
Templates.TryGetTemplate<T>(templateText, out var nameTemplate); if (!Templates.TryGetTemplate<T>(templateText, out var nameTemplate))
throw new ArgumentException($"Failed to parse {nameof(templateText)}");
var templateEditor = new TemplateEditor<T> var templateEditor = new TemplateEditor<T>(nameTemplate, "", T.DefaultTemplate, T.Name, T.Description);
{
_editingTemplate = nameTemplate,
DefaultTemplate = T.DefaultTemplate,
TemplateName = T.Name,
TemplateDescription = T.Description
};
if (templateEditor.IsFolder || templateEditor.IsFilePath) if (templateEditor.IsFolder || templateEditor.IsFilePath)
throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates"); throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates");

View File

@ -1,5 +1,6 @@
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public sealed class TemplateTags : ITemplateTag public sealed class TemplateTags : ITemplateTag
@ -10,7 +11,7 @@ namespace LibationFileManager
public string Description { get; } public string Description { get; }
public string Display { get; } public string Display { get; }
private TemplateTags(string tagName, string description, string defaultValue = null, string display = null) private TemplateTags(string tagName, string description, string? defaultValue = null, string? display = null)
{ {
TagName = tagName; TagName = tagName;
Description = description; Description = description;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using AaxDecrypter; using AaxDecrypter;
@ -8,6 +9,7 @@ using FileManager;
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
using NameParser; using NameParser;
#nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public interface ITemplate public interface ITemplate
@ -26,10 +28,10 @@ namespace LibationFileManager
//Assigning the properties in the static constructor will require all //Assigning the properties in the static constructor will require all
//Templates users to have a valid configuration file. To allow tests //Templates users to have a valid configuration file. To allow tests
//to work without access to Configuration, only load templates on demand. //to work without access to Configuration, only load templates on demand.
private static FolderTemplate _folder; private static FolderTemplate? _folder;
private static FileTemplate _file; private static FileTemplate? _file;
private static ChapterFileTemplate _chapterFile; private static ChapterFileTemplate? _chapterFile;
private static ChapterTitleTemplate _chapterTitle; private static ChapterTitleTemplate? _chapterTitle;
public static FolderTemplate Folder => _folder ??= GetTemplate<FolderTemplate>(Configuration.Instance.FolderTemplate); public static FolderTemplate Folder => _folder ??= GetTemplate<FolderTemplate>(Configuration.Instance.FolderTemplate);
public static FileTemplate File => _file ??= GetTemplate<FileTemplate>(Configuration.Instance.FileTemplate); public static FileTemplate File => _file ??= GetTemplate<FileTemplate>(Configuration.Instance.FileTemplate);
@ -38,10 +40,10 @@ namespace LibationFileManager
#region Template Parsing #region Template Parsing
public static T GetTemplate<T>(string templateText) where T : Templates, ITemplate, new() public static T GetTemplate<T>(string? templateText) where T : Templates, ITemplate, new()
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>(); => TryGetTemplate<T>(templateText ?? "", out var template) ? template : GetDefaultTemplate<T>();
public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new() public static bool TryGetTemplate<T>(string templateText, [NotNullWhen(true)] out T? template) where T : Templates, ITemplate, new()
{ {
var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections); var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections);
@ -56,19 +58,19 @@ namespace LibationFileManager
{ {
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))] [PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
(_,e) => _folder = GetTemplate<FolderTemplate>((string)e.NewValue); (_,e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FileTemplate))] [PropertyChangeFilter(nameof(Configuration.FileTemplate))]
(_, e) => _file = GetTemplate<FileTemplate>((string)e.NewValue); (_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))] [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue); (_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))] [PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue); (_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string);
HumanName.Suffixes.Add("ret"); HumanName.Suffixes.Add("ret");
HumanName.Titles.Add("professor"); HumanName.Titles.Add("professor");
@ -78,10 +80,18 @@ namespace LibationFileManager
#region Template Properties #region Template Properties
public IEnumerable<TemplateTags> TagsRegistered => NamingTemplate.TagsRegistered.Cast<TemplateTags>(); public IEnumerable<TemplateTags> TagsRegistered
public IEnumerable<TemplateTags> TagsInUse => NamingTemplate.TagsInUse.Cast<TemplateTags>(); => NamingTemplate?.TagsRegistered.Cast<TemplateTags>() ?? Enumerable.Empty<TemplateTags>();
public string TemplateText => NamingTemplate.TemplateText; public IEnumerable<TemplateTags> TagsInUse
protected NamingTemplate NamingTemplate { get; private set; } => NamingTemplate?.TagsInUse.Cast<TemplateTags>() ?? Enumerable.Empty<TemplateTags>();
public string TemplateText => NamingTemplate?.TemplateText ?? "";
private readonly NamingTemplate? _namingTemplate;
protected NamingTemplate NamingTemplate
{
get => _namingTemplate ?? throw new NullReferenceException(nameof(_namingTemplate));
private init => _namingTemplate = value;
}
#endregion #endregion
@ -104,7 +114,7 @@ namespace LibationFileManager
return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value)); return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
} }
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
{ {
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir)); ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
@ -114,7 +124,7 @@ namespace LibationFileManager
return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto); return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto);
} }
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
{ {
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
@ -246,7 +256,7 @@ namespace LibationFileManager
new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter, FloatFormatter) new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter, FloatFormatter)
{ {
//Don't allow formatting of Id //Don't allow formatting of Id
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v }, { TemplateTags.Id, lb => lb.AudibleProductId, v => v ?? "" },
{ TemplateTags.Title, lb => lb.TitleWithSubtitle }, { TemplateTags.Title, lb => lb.TitleWithSubtitle },
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
{ TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleTitle, lb => lb.Title },
@ -308,13 +318,13 @@ namespace LibationFileManager
#region Tag Formatters #region Tag Formatters
private static string getTitleShort(string title) private static string? getTitleShort(string? title)
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title; => title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
private static string getLanguageShort(string language) private static string getLanguageShort(string? language)
{ {
if (language is null) if (language is null)
return null; return "";
language = language.Trim(); language = language.Trim();
if (language.Length <= 3) if (language.Length <= 3)
@ -324,8 +334,9 @@ namespace LibationFileManager
private static string StringFormatter(ITemplateTag templateTag, string value, string formatString) private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
{ {
if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper(); if (value is null) return "";
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower(); else if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value.ToUpper();
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value.ToLower();
else return value; else return value;
} }
@ -358,7 +369,7 @@ namespace LibationFileManager
public class FolderTemplate : Templates, ITemplate public class FolderTemplate : Templates, ITemplate
{ {
public static string Name { get; }= "Folder Template"; public static string Name { get; }= "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections public static IEnumerable<TagCollection> TagCollections
=> new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags }; => new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags };
@ -378,7 +389,7 @@ namespace LibationFileManager
public class FileTemplate : Templates, ITemplate public class FileTemplate : Templates, ITemplate
{ {
public static string Name { get; } = "File Template"; public static string Name { get; } = "File Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)); public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>]"; public static string DefaultTemplate { get; } = "<title> [<id>]";
public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags };
} }
@ -386,7 +397,7 @@ namespace LibationFileManager
public class ChapterFileTemplate : Templates, ITemplate public class ChapterFileTemplate : Templates, ITemplate
{ {
public static string Name { get; } = "Chapter File Template"; public static string Name { get; } = "Chapter File Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
@ -399,7 +410,7 @@ namespace LibationFileManager
public class ChapterTitleTemplate : Templates, ITemplate public class ChapterTitleTemplate : Templates, ITemplate
{ {
public static string Name { get; } = "Chapter Title Template"; public static string Name { get; } = "Chapter Title Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? "";
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags); public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags);