Custom illegal character replacement
This commit is contained in:
parent
184ba84600
commit
2ab466c570
24 changed files with 838 additions and 218 deletions
|
|
@ -79,8 +79,13 @@ namespace FileManager
|
|||
//Stop raising events
|
||||
fileSystemWatcher?.Dispose();
|
||||
|
||||
//Calling CompleteAdding() will cause background scanner to terminate.
|
||||
directoryChangesEvents?.CompleteAdding();
|
||||
try
|
||||
{
|
||||
//Calling CompleteAdding() will cause background scanner to terminate.
|
||||
directoryChangesEvents?.CompleteAdding();
|
||||
}
|
||||
// if directoryChangesEvents is non-null and isDisposed, this exception is thrown. there's no other way to check >:(
|
||||
catch (ObjectDisposedException) { }
|
||||
|
||||
//Wait for background scanner to terminate before reinitializing.
|
||||
backgroundScanner?.Wait();
|
||||
|
|
|
|||
|
|
@ -12,17 +12,13 @@ namespace FileManager
|
|||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileNamingTemplate(string template) : base(template) { }
|
||||
|
||||
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
|
||||
public string IllegalCharacterReplacements { get; set; }
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
public LongPath GetFilePath(bool returnFirstExisting = false)
|
||||
public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
{
|
||||
|
||||
string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template;
|
||||
List<string> pathParts = new();
|
||||
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value));
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements));
|
||||
|
||||
while (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
|
|
@ -43,7 +39,7 @@ namespace FileManager
|
|||
|
||||
pathParts.Reverse();
|
||||
|
||||
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting);
|
||||
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting);
|
||||
}
|
||||
|
||||
private string replaceFileName(string filename, Dictionary<string,string> paramReplacements)
|
||||
|
|
@ -92,17 +88,14 @@ namespace FileManager
|
|||
return string.Join("", filenameParts);
|
||||
}
|
||||
|
||||
private string formatValue(object value)
|
||||
private string formatValue(object value, ReplacementCharacters replacements)
|
||||
{
|
||||
if (value is null)
|
||||
return "";
|
||||
|
||||
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
||||
// Esp important for file templates.
|
||||
return value
|
||||
.ToString()
|
||||
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
||||
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
||||
return replacements.ReplaceInvalidFilenameChars(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ namespace FileManager
|
|||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
|
||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, illegalCharacterReplacements);
|
||||
path = GetSafePath(path, replacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
|
|
@ -77,36 +77,19 @@ namespace FileManager
|
|||
return fullfilename;
|
||||
}
|
||||
|
||||
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
|
||||
|
||||
/// <summary>Use with file name, not full path. Valid path characters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
|
||||
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
|
||||
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
||||
|
||||
/// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||
public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "")
|
||||
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
var pathNoPrefix = path.PathWithoutPrefix;
|
||||
|
||||
pathNoPrefix = replaceColons(pathNoPrefix, "꞉");
|
||||
pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix);
|
||||
pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements);
|
||||
pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix);
|
||||
pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
|
||||
|
||||
return pathNoPrefix;
|
||||
}
|
||||
|
||||
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?',
|
||||
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
|
||||
// In live code, Path.GetInvalidPathChars() does not include them
|
||||
'"', '<', '>'
|
||||
}).ToArray();
|
||||
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
||||
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
||||
|
||||
private static string removeDoubleSlashes(string path)
|
||||
{
|
||||
if (path.Length < 2)
|
||||
|
|
@ -122,60 +105,6 @@ namespace FileManager
|
|||
return path[0] + remainder;
|
||||
}
|
||||
|
||||
private static string replaceIllegalWithUnicodeAnalog(string path)
|
||||
{
|
||||
char[] replaced = path.ToCharArray();
|
||||
|
||||
char GetQuote(int position)
|
||||
{
|
||||
if (
|
||||
position == 0
|
||||
|| (position > 0
|
||||
&& position < replaced.Length
|
||||
&& !char.IsLetter(replaced[position - 1])
|
||||
&& !char.IsNumber(replaced[position - 1])
|
||||
)
|
||||
) return '“';
|
||||
else if (
|
||||
position == replaced.Length - 1
|
||||
|| (position >= 0
|
||||
&& position < replaced.Length - 1
|
||||
&& !char.IsLetter(replaced[position + 1])
|
||||
&& !char.IsNumber(replaced[position + 1])
|
||||
)
|
||||
) return '”';
|
||||
else return '"';
|
||||
}
|
||||
|
||||
for (int i = 0; i < replaced.Length; i++)
|
||||
{
|
||||
replaced[i] = replaced[i] switch
|
||||
{
|
||||
'?' => '?',
|
||||
'*' => '✱',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'"' => GetQuote(i),
|
||||
_ => replaced[i]
|
||||
};
|
||||
}
|
||||
return new string(replaced);
|
||||
}
|
||||
|
||||
private static string replaceColons(string path, string illegalCharacterReplacements)
|
||||
{
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < path.Length; i++)
|
||||
{
|
||||
var c = path[i];
|
||||
if (i >= 2 && c == ':')
|
||||
builder.Append(illegalCharacterReplacements);
|
||||
else
|
||||
builder.Append(c);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
|
||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
|
|
@ -206,9 +135,9 @@ namespace FileManager
|
|||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination)
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
|
||||
{
|
||||
destination = GetValidFilename(destination);
|
||||
destination = GetValidFilename(destination, replacements);
|
||||
SaferMove(source, destination);
|
||||
return destination;
|
||||
}
|
||||
|
|
|
|||
269
Source/FileManager/ReplacementCharacters.cs
Normal file
269
Source/FileManager/ReplacementCharacters.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class Replacement
|
||||
{
|
||||
public const int FIXED_COUNT = 6;
|
||||
|
||||
internal const char QUOTE_MARK = '"';
|
||||
[JsonIgnore] public bool Mandatory { get; internal set; }
|
||||
[JsonProperty] public char CharacterToReplace { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; private set; }
|
||||
[JsonProperty] public string Description { get; private set; }
|
||||
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
|
||||
|
||||
public Replacement(char charToReplace, string replacementString, string description)
|
||||
{
|
||||
CharacterToReplace = charToReplace;
|
||||
ReplacementString = replacementString;
|
||||
Description = description;
|
||||
}
|
||||
private Replacement(char charToReplace, string replacementString, string description, bool mandatory)
|
||||
: this(charToReplace, replacementString, description)
|
||||
{
|
||||
Mandatory = mandatory;
|
||||
}
|
||||
|
||||
public void Update(char charToReplace, string replacementString, string description)
|
||||
{
|
||||
ReplacementString = replacementString;
|
||||
|
||||
if (!Mandatory)
|
||||
{
|
||||
CharacterToReplace = charToReplace;
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
|
||||
public static Replacement OtherInvalid(string replacement) => new(default, replacement, "All other invalid characters", true);
|
||||
public static Replacement FilenameForwardSlash(string replacement) => new('/', replacement, "Forward Slash (Filename Only)", true);
|
||||
public static Replacement FilenameBackSlash(string replacement) => new('\\', replacement, "Back Slash (Filename Only)", true);
|
||||
public static Replacement OpenQuote(string replacement) => new('"', replacement, "Open Quote", true);
|
||||
public static Replacement CloseQuote(string replacement) => new('"', replacement, "Close Quote", true);
|
||||
public static Replacement OtherQuote(string replacement) => new('"', replacement, "Other Quote", true);
|
||||
public static Replacement Colon(string replacement) => new(':', replacement, "Colon");
|
||||
public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk");
|
||||
public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark");
|
||||
public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket");
|
||||
public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket");
|
||||
public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line");
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||
public class ReplacementCharacters
|
||||
{
|
||||
public static readonly ReplacementCharacters Default = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("∕"),
|
||||
Replacement.FilenameBackSlash(""),
|
||||
Replacement.OpenQuote("“"),
|
||||
Replacement.CloseQuote("”"),
|
||||
Replacement.OtherQuote("""),
|
||||
Replacement.OpenAngleBracket("<"),
|
||||
Replacement.CloseAngleBracket(">"),
|
||||
Replacement.Colon("꞉"),
|
||||
Replacement.Asterisk("✱"),
|
||||
Replacement.QuestionMark("?"),
|
||||
Replacement.Pipe("⏐"),
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters LoFiDefault = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
Replacement.OpenQuote("'"),
|
||||
Replacement.CloseQuote("'"),
|
||||
Replacement.OtherQuote("'"),
|
||||
Replacement.OpenAngleBracket("{"),
|
||||
Replacement.CloseAngleBracket("}"),
|
||||
Replacement.Colon("-"),
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters Minimum = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
Replacement.OpenQuote("_"),
|
||||
Replacement.CloseQuote("_"),
|
||||
Replacement.OtherQuote("_"),
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?', ':',
|
||||
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
|
||||
// In live code, Path.GetInvalidPathChars() does not include them
|
||||
'"', '<', '>'
|
||||
}).ToArray();
|
||||
|
||||
public IReadOnlyList<Replacement> Replacements { get; init; }
|
||||
private string DefaultReplacement => Replacements[0].ReplacementString;
|
||||
private Replacement ForwardSlash => Replacements[1];
|
||||
private Replacement BackSlash => Replacements[2];
|
||||
private string OpenQuote => Replacements[3].ReplacementString;
|
||||
private string CloseQuote => Replacements[4].ReplacementString;
|
||||
private string OtherQuote => Replacements[5].ReplacementString;
|
||||
|
||||
private string GetFilenameCharReplacement(char toReplace, char preceding, char succeding)
|
||||
{
|
||||
if (toReplace == ForwardSlash.CharacterToReplace)
|
||||
return ForwardSlash.ReplacementString;
|
||||
else if (toReplace == BackSlash.CharacterToReplace)
|
||||
return BackSlash.ReplacementString;
|
||||
else return GetPathCharReplacement(toReplace, preceding, succeding);
|
||||
}
|
||||
private string GetPathCharReplacement(char toReplace, char preceding, char succeding)
|
||||
{
|
||||
if (toReplace == Replacement.QUOTE_MARK)
|
||||
{
|
||||
if (
|
||||
preceding != default
|
||||
&& !char.IsLetter(preceding)
|
||||
&& !char.IsNumber(preceding)
|
||||
&& (char.IsLetter(succeding) || char.IsNumber(succeding))
|
||||
)
|
||||
return OpenQuote;
|
||||
else if (
|
||||
succeding != default
|
||||
&& !char.IsLetter(succeding)
|
||||
&& !char.IsNumber(succeding)
|
||||
&& (char.IsLetter(preceding) || char.IsNumber(preceding))
|
||||
)
|
||||
return CloseQuote;
|
||||
else
|
||||
return OtherQuote;
|
||||
}
|
||||
|
||||
for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++)
|
||||
{
|
||||
var r = Replacements[i];
|
||||
if (r.CharacterToReplace == toReplace)
|
||||
return r.ReplacementString;
|
||||
}
|
||||
return DefaultReplacement;
|
||||
}
|
||||
|
||||
|
||||
public static bool ContainsInvalid(string path)
|
||||
=> path.Any(c => invalidChars.Contains(c));
|
||||
|
||||
public string ReplaceInvalidFilenameChars(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName)) return string.Empty;
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < fileName.Length; i++)
|
||||
{
|
||||
var c = fileName[i];
|
||||
|
||||
if (invalidChars.Contains(c) || c == ForwardSlash.CharacterToReplace || c == BackSlash.CharacterToReplace)
|
||||
{
|
||||
char preceding = i > 0 ? fileName[i - 1] : default;
|
||||
char succeeding = i < fileName.Length - 1 ? fileName[i + 1] : default;
|
||||
builder.Append(GetFilenameCharReplacement(c, preceding, succeeding));
|
||||
}
|
||||
else
|
||||
builder.Append(c);
|
||||
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public string ReplaceInvalidChars(string pathStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pathStr)) return string.Empty;
|
||||
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < pathStr.Length; i++)
|
||||
{
|
||||
var c = pathStr[i];
|
||||
|
||||
if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr)))
|
||||
builder.Append(c);
|
||||
else
|
||||
{
|
||||
char preceding = i > 0 ? pathStr[i - 1] : default;
|
||||
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
|
||||
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
|
||||
}
|
||||
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#region JSON Converter
|
||||
internal class ReplacementCharactersConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(ReplacementCharacters);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var replaceArr = jObj[nameof(Replacement)];
|
||||
IReadOnlyList<Replacement> dict = replaceArr
|
||||
.ToObject<Replacement[]>().ToList();
|
||||
|
||||
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
||||
//If not, reset to default.
|
||||
|
||||
var default0 = Replacement.OtherInvalid("");
|
||||
var default1 = Replacement.FilenameForwardSlash("");
|
||||
var default2 = Replacement.FilenameBackSlash("");
|
||||
var default3 = Replacement.OpenQuote("");
|
||||
var default4 = Replacement.CloseQuote("");
|
||||
var default5 = Replacement.OtherQuote("");
|
||||
|
||||
if (dict.Count < Replacement.FIXED_COUNT ||
|
||||
dict[0].CharacterToReplace != default0.CharacterToReplace || dict[0].Description != default0.Description ||
|
||||
dict[1].CharacterToReplace != default1.CharacterToReplace || dict[1].Description != default1.Description ||
|
||||
dict[2].CharacterToReplace != default2.CharacterToReplace || dict[2].Description != default2.Description ||
|
||||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
|
||||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
|
||||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString))
|
||||
)
|
||||
{
|
||||
dict = ReplacementCharacters.Default.Replacements;
|
||||
}
|
||||
//First FIXED_COUNT are mandatory
|
||||
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
|
||||
dict[i].Mandatory = true;
|
||||
|
||||
return new ReplacementCharacters { Replacements = dict };
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
ReplacementCharacters replacements = (ReplacementCharacters)value;
|
||||
|
||||
var propertyNames = replacements.Replacements
|
||||
.Select(c => JObject.FromObject(c)).ToList();
|
||||
|
||||
var prop = new JProperty(nameof(Replacement), new JArray(propertyNames));
|
||||
|
||||
var obj = new JObject();
|
||||
obj.AddFirst(prop);
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue