Получение фактического имени файла (с правильным регистром) в Windows с.NET
Я хочу сделать точно так же, как в этом вопросе:
Файловая система Windows нечувствительна к регистру. Каким образом, учитывая имя файла / папки (например, "somefile"), я получаю фактическое имя этого файла / папки (например, оно должно возвращать "SomeFile", если Explorer отображает его так)?
Но мне нужно сделать это в.NET, и я хочу полный путь (D:/Temp/Foobar.xml
и не только Foobar.xml
).
я вижу это FullName
на FileInfo
класс не делает трюк.
8 ответов
Мне кажется, что поскольку NTFS нечувствительна к регистру, она всегда будет правильно принимать ваш ввод, независимо от того, правильно ли указано имя.
Кажется, что единственный способ получить правильное имя пути - найти файл, предложенный Джоном Сибли.
Я создал метод, который будет брать путь (папку или файл) и возвращать его правильную версию (для всего пути):
public static string GetExactPathName(string pathName)
{
if (!(File.Exists(pathName) || Directory.Exists(pathName)))
return pathName;
var di = new DirectoryInfo(pathName);
if (di.Parent != null) {
return Path.Combine(
GetExactPathName(di.Parent.FullName),
di.Parent.GetFileSystemInfos(di.Name)[0].Name);
} else {
return di.Name.ToUpper();
}
}
Вот несколько тестов, которые работали на моей машине:
static void Main(string[] args)
{
string file1 = @"c:\documents and settings\administrator\ntuser.dat";
string file2 = @"c:\pagefile.sys";
string file3 = @"c:\windows\system32\cmd.exe";
string file4 = @"c:\program files\common files";
string file5 = @"ddd";
Console.WriteLine(GetExactPathName(file1));
Console.WriteLine(GetExactPathName(file2));
Console.WriteLine(GetExactPathName(file3));
Console.WriteLine(GetExactPathName(file4));
Console.WriteLine(GetExactPathName(file5));
Console.ReadLine();
}
Метод вернет предоставленное значение, если файл не существует.
Могут быть более быстрые методы (это использует рекурсию), но я не уверен, есть ли очевидные способы сделать это.
Мне понравился ответ Йоны, но я хотел, чтобы он:
- Поддержка UNC путей
- Скажи мне, если путь не существовал
- Используйте итерацию вместо рекурсии (так как она использовала только хвостовую рекурсию)
- Минимизируйте количество вызовов Path.Combine (чтобы минимизировать конкатенацию строк).
/// <summary>
/// Gets the exact case used on the file system for an existing file or directory.
/// </summary>
/// <param name="path">A relative or absolute path.</param>
/// <param name="exactPath">The full path using the correct case if the path exists. Otherwise, null.</param>
/// <returns>True if the exact path was found. False otherwise.</returns>
/// <remarks>
/// This supports drive-lettered paths and UNC paths, but a UNC root
/// will be returned in title case (e.g., \\Server\Share).
/// </remarks>
public static bool TryGetExactPath(string path, out string exactPath)
{
bool result = false;
exactPath = null;
// DirectoryInfo accepts either a file path or a directory path, and most of its properties work for either.
// However, its Exists property only works for a directory path.
DirectoryInfo directory = new DirectoryInfo(path);
if (File.Exists(path) || directory.Exists)
{
List<string> parts = new List<string>();
DirectoryInfo parentDirectory = directory.Parent;
while (parentDirectory != null)
{
FileSystemInfo entry = parentDirectory.EnumerateFileSystemInfos(directory.Name).First();
parts.Add(entry.Name);
directory = parentDirectory;
parentDirectory = directory.Parent;
}
// Handle the root part (i.e., drive letter or UNC \\server\share).
string root = directory.FullName;
if (root.Contains(':'))
{
root = root.ToUpper();
}
else
{
string[] rootParts = root.Split('\\');
root = string.Join("\\", rootParts.Select(part => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(part)));
}
parts.Add(root);
parts.Reverse();
exactPath = Path.Combine(parts.ToArray());
result = true;
}
return result;
}
Для путей UNC это корень (\\Server\Share) в случае заголовка, а не точный случай, потому что было бы гораздо больше работы, чтобы попытаться определить точное имя дела на удаленном сервере и точное имя дела в общем ресурсе. Если вы заинтересованы в добавлении этой поддержки, вам придется использовать методы P/Invoke, такие как NetServerEnum и NetShareEnum. Но они могут быть медленными, и они не поддерживают предварительную фильтрацию только на сервере и обмениваются именами, которые вас интересуют.
Вот метод модульного теста для TryGetExactPath (с использованием расширений для тестирования Visual Studio):
[TestMethod]
public void TryGetExactPathNameTest()
{
string machineName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(Environment.MachineName.ToLower());
string[] testPaths = new[]
{
@"C:\Users\Public\desktop.ini",
@"C:\pagefile.sys",
@"C:\Windows\System32\cmd.exe",
@"C:\Users\Default\NTUSER.DAT",
@"C:\Program Files (x86)\Microsoft.NET\Primary Interop Assemblies",
@"C:\Program Files (x86)",
@"Does not exist",
@"\\Nas\Main\Setups",
@"\\Nas\Main\Setups\Microsoft\Visual Studio\VS 2015\vssdk_full.exe",
@"\\" + machineName + @"\C$\Windows\System32\ActionCenter.dll",
@"..",
};
Dictionary<string, string> expectedExactPaths = new Dictionary<string, string>()
{
{ @"..", Path.GetDirectoryName(Environment.CurrentDirectory) },
};
foreach (string testPath in testPaths)
{
string lowercasePath = testPath.ToLower();
bool expected = File.Exists(lowercasePath) || Directory.Exists(lowercasePath);
string exactPath;
bool actual = FileUtility.TryGetExactPath(lowercasePath, out exactPath);
actual.ShouldEqual(expected);
if (actual)
{
string expectedExactPath;
if (expectedExactPaths.TryGetValue(testPath, out expectedExactPath))
{
exactPath.ShouldEqual(expectedExactPath);
}
else
{
exactPath.ShouldEqual(testPath);
}
}
else
{
exactPath.ShouldBeNull();
}
}
}
Вдохновленный ответом Ивана, вот метод, который также обрабатывает регистр букв дисков:
public string FixFilePathCasing(string filePath)
{
string fullFilePath = Path.GetFullPath(filePath);
string fixedPath = "";
foreach(string token in fullFilePath.Split('\\'))
{
//first token should be drive token
if(fixedPath == "")
{
//fix drive casing
string drive = string.Concat(token, "\\");
drive = DriveInfo.GetDrives()
.First(driveInfo => driveInfo.Name.Equals(drive, StringComparison.OrdinalIgnoreCase)).Name;
fixedPath = drive;
}
else
{
fixedPath = Directory.GetFileSystemEntries(fixedPath, token).First();
}
}
return fixedPath;
}
Мой второй ответ здесь нерекурсивным методом. Он принимает как файлы, так и каталоги.
На этот раз переводится с VB на C#:
private string fnRealCAPS(string sDirOrFile)
{
string sTmp = "";
foreach (string sPth in sDirOrFile.Split("\\")) {
if (string.IsNullOrEmpty(sTmp)) {
sTmp = sPth + "\\";
continue;
}
sTmp = System.IO.Directory.GetFileSystemEntries(sTmp, sPth)[0];
}
return sTmp;
}
Я думаю, что единственный способ сделать это - использовать тот же Win32 API, а именно метод SHGetFileInfo, упомянутый в принятом ответе на вопрос, на который вы ссылаетесь. Для этого вам нужно будет использовать некоторые вызовы взаимодействия p/invoke. Посмотрите на pinvoke.net пример того, как это сделать, и какие дополнительные структуры вам понадобятся.
Интересная проблема.
Один из способов сделать это - "найти" файл на основе нечувствительного к регистру имени, а затем просмотреть свойство FileInfo.FullName. Я проверил это, используя следующую функцию, и это дает требуемый результат.
static string GetCaseSensitiveFileName(string filePath)
{
string caseSensitiveFilePath = null;
DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(filePath));
FileInfo[] files = dirInfo.GetFiles(Path.GetFileName(filePath));
if (files.Length > 0)
{
caseSensitiveFilePath = files[0].FullName;
}
return caseSensitiveFilePath;
}
Здесь нужно быть немного осторожнее - если у вас есть два файла с именованными именами, например file.xml и File.xml, он вернет только первый.
Конечно, без проблем...
public static string GetFileSystemName(string path) {
var parts = path.Split("\\/".ToCharArray());
if(path.StartsWith($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}")) parts[0] = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}{parts[0]}";
if(parts[0].EndsWith(":")) parts[0] = parts[0].ToUpper();
for(var ɪ = (parts.Length - 2); ɪ > -1; ɪ--) {
var path_ɪ = string.Join($"{Path.DirectorySeparatorChar}", parts, 0, (ɪ + 1));
var entries_ɪ = Directory.GetFileSystemEntries(path_ɪ, parts[ɪ + 1]);
parts[ɪ + 1] = Path.GetFileName(entries_ɪ[0]);
}
return string.Join($"{Path.DirectorySeparatorChar}", parts);
}
Похоже, что лучший способ - это перебрать все папки в пути и получить правильные заглавные буквы:
Public Function gfnProperPath(ByVal sPath As String) As String
If Not IO.File.Exists(sPath) AndAlso Not IO.Directory.Exists(sPath) Then Return sPath
Dim sarSplitPath() As String = sPath.Split("\")
Dim sAddPath As String = sarSplitPath(0).ToUpper & "\"
For i = 1 To sarSplitPath.Length - 1
sPath = sAddPath & "\" & sarSplitPath(i)
If IO.File.Exists(sPath) Then
Return IO.Directory.GetFiles(sAddPath, sarSplitPath(i), IO.SearchOption.TopDirectoryOnly)(0)
ElseIf IO.Directory.Exists(sPath) Then
sAddPath = IO.Directory.GetDirectories(sAddPath, sarSplitPath(i), IO.SearchOption.TopDirectoryOnly)(0)
End If
Next
Return sPath
End Function