Нужно по именам бизнес-сущностей создавать папки и подпапки в них, чтобы названия были максимально близки к оригинальным.
При этом, надо учитывать ограничения операционной системы, что добавляет мороки - если на linux-macos ограничений фактически нет, то на windows их более чем полно.
Получился вот такой код, недопустимые символы заменяются на точку.
private static readonly string NormalizationPattern = string.Format(@"([{0}]*\.+$)|([{0}]+)", Regex.Escape(string.Concat(new string(Path.GetInvalidPathChars()), "?", "/", "*", "\"")));
private static readonly string[] DosReservedNames = { "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
public static string NormalizePath(string name)
{
if (Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX)
return name;
const string replacement = ".";
var matchesCount = Regex.Matches(name, @":\\").Count;
string correctName;
if (matchesCount > 0)
{
var regex = new Regex(@":", RegexOptions.RightToLeft);
correctName = regex.Replace(name, replacement, regex.Matches(name).Count - matchesCount);
}
else
correctName = name.Replace(":", replacement);
var replace = Regex.Replace(correctName, NormalizationPattern, replacement);
foreach (var reservedName in DosReservedNames)
{
var builder = new List<string>();
foreach (var folder in replace.Split(Path.DirectorySeparatorChar))
{
var changedName = folder;
if (string.Equals(folder, reservedName, StringComparison.InvariantCultureIgnoreCase))
changedName = replacement + reservedName;
var value = reservedName + '.';
if (folder.StartsWith(value, StringComparison.InvariantCultureIgnoreCase))
changedName = replacement + value + folder.Remove(0, value.Length);
builder.Add(changedName);
}
replace = string.Join<string>(Path.DirectorySeparatorChar.ToString(), builder);
}
return replace.TrimEnd(' ', '.');
}
Корень папки обычно выбирается в системе и он уже существует. А дальше все уровни вложенности создаются через нормализацию. Поэтому например тримминг точек и пробелов сделан только в конце, а не на каждом уровне. Может и не стоит так делать и стоит триммить имя каждой папки.
На это всё написаны тесты, кейсы в целом выглядят вот так:
[Test, Sequential]
public void CheckNotAllowedNames([Values(
"test"
,@"C:\somename\somename:name"
,@"usr\home\somename:name"
,@"start < > : "" / \ | ? * end"
,"\x15\x3D" // less than ASCII space
,"\x21\x3D" // HEX of !, valid
,"\x3F\x3D" // HEX of ?, not valid
,@"C:\somename\ trailing space "
,@"C:\somename\...trailing period..."
,@"C:\somename\CON"
,@"C:\somename\CON.txt"
,@"CON"
,@"C:\somename\con.txt\context"
,@"home\NUL.liza"
,@"home\ NUL.liza"
,@"C:\somename\..." // Bad name get the root folder, bug =_=
,@"root\..\sub"
,@"root\..\"
,@".\..\some?folder"
,@"root\.." // relative path trimmed, bug =_=
)] string name, [Values(
"test"
,@"C:\somename\somename.name"
,@"usr\home\somename.name"
,@"start . . . . . \ . . . end"
,".="
,"!="
,".="
,@"C:\somename\ trailing space"
,@"C:\somename\...trailing period"
,@"C:\somename\.CON"
,@"C:\somename\.CON.txt"
,@".CON"
,@"C:\somename\.CON.txt\context"
,@"home\.NUL.liza"
,@"home\ NUL.liza"
,@"C:\somename\"
,@"root\..\sub"
,@"root\..\"
,@".\..\some.folder"
,@"root\"
)] string expected)
{
Assert.AreEqual(expected, NormalizePath(name));
}
Собственно, хочется во первых чтобы кто-то посмотрел и может нашел пропущенные мной ошибки.
А во вторых - может я изобретаю велосипед и где то есть готовая нормализация? Гуглил долго и упорно, но мог пропустить, велосипедов кругом полно.
UPD1: найдена проблема с относительными путями и её пока не представляю как решать, добавил тестов с текущим поведнием. Апи дотнета позволяет запросить создание папки root\folder\.....
и возвращает при этом папку root
. Справка на msdn говорит что точки через апи создать можно, но не стоит, чтобы не вызывать проблем. Как в итоге обрабатывать правильнее относительные пути - тот ещё вопрос.
В итоге, разделил явно логику по работе с корневой папкой, указанной в настройках и с папками бизнес объектов.
Папки настроек валидируются просто:
/// <summary>
/// Проверить путь к папке хранения указываемый в настройках.
/// </summary>
/// <param name="path">Путь к папке.</param>
/// <returns>True, если путь в порядке.</returns>
/// <remarks>Путь должен существовать или быть значением по умолчанию. Не должен заканчиваться относительными путями.</remarks>
public static bool ValidateSettingPath(string path)
{
if (Equals(path, AppConfig.DownloadFolderName) || Equals(path, AppConfig.DownloadFolder))
return true;
if (path.TrimEnd(Path.PathSeparator, Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar, Path.VolumeSeparatorChar).EndsWith("."))
return false;
return Directory.Exists(GetAbsoluteFolderPath(path));
}
А валидация папок от бизнес объектов фактически та, что в вопросе, но её получилось сильно упростить:
private static readonly string NormalizationPattern = string.Format(@"([{0}]*\.+$)|([{0}]+)", Regex.Escape(string.Concat(new string(Path.GetInvalidPathChars()), "?", "/", "*", "\"", ":", "\\")));
private static readonly string[] DosReservedNames = { "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
/// <summary>
/// Очистка имени от недопустимых символов.
/// </summary>
/// <param name="name">Имя.</param>
/// <returns>Имя без недопустимых символов.</returns>
/// <remarks>В имени не должно быть разделителей, они будут восприниматься как часть имени.</remarks>
public static string RemoveInvalidCharsFromName(string name)
{
if (Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX)
return name;
const string replacement = ".";
var folder = Regex.Replace(name, NormalizationPattern, replacement);
foreach (var reservedName in DosReservedNames)
{
var reservedNameWithDot = reservedName + '.';
if (string.Equals(folder, reservedName, StringComparison.InvariantCultureIgnoreCase))
folder = replacement + reservedName;
else if (folder.StartsWith(reservedNameWithDot, StringComparison.InvariantCultureIgnoreCase))
folder = replacement + reservedNameWithDot + folder.Remove(0, reservedNameWithDot.Length);
}
// Если имя оказалось целиком из точек и\или пробелов - заменяем на константу.
folder = folder.TrimEnd(' ', '.');
if (string.IsNullOrWhiteSpace(folder))
folder = "invalid name";
return folder;
}
Текущая реализация требует вызова RemoveInvalidCharsFromName
на каждый уровень вложенности. Т.е. если логика требует Folder1/Subfolder2/Subfolder3, то "нормализовать"
надо каждую папку отдельно:
var path = Path.Combine(settingFolder, RemoveInvalidCharsFromName("Folder1"));
path = Path.Combine(path, RemoveInvalidCharsFromName("Subfolder2"));
path = Path.Combine(path, RemoveInvalidCharsFromName("Subfolder3"));
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Цикл для обнаружения элементаПотом, когда кнопка пропадает, нужно просто выполнить другие действия и т
Создал в Unity игру на андроид по локальной сетиИспользовал Network Manager
использую данный код для отправки команды в командную строку: