Как получить путь с учетом регистра в Windows?

Мне нужно знать, каков реальный путь данного пути.

Например:

Реальный путь: d:\src\File.txt
И пользователь дал мне: D:\src\file.txt
Мне нужно в результате: d:\src\File.txt

9 ответов

Решение

Вы можете использовать эту функцию:

[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
static extern uint GetLongPathName(string ShortPath, StringBuilder sb, int buffer);

[DllImport("kernel32.dll")]
static extern uint GetShortPathName(string longpath, StringBuilder sb, int buffer); 

protected static string GetWindowsPhysicalPath(string path)
{
        StringBuilder builder = new StringBuilder(255);

        // names with long extension can cause the short name to be actually larger than
        // the long name.
        GetShortPathName(path, builder, builder.Capacity);

        path = builder.ToString();

        uint result = GetLongPathName(path, builder, builder.Capacity);

        if (result > 0 && result < builder.Capacity)
        {
            //Success retrieved long file name
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        if (result > 0)
        {
            //Need more capacity in the buffer
            //specified in the result variable
            builder = new StringBuilder((int)result);
            result = GetLongPathName(path, builder, builder.Capacity);
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        return null;
}

Как старый таймер, я всегда использовал FindFirstFile для этой цели. Перевод.Net:

Directory.GetFiles(Path.GetDirectoryName(userSuppliedName), Path.GetFileName(userSuppliedName)).FirstOrDefault();

Это только дает вам правильный регистр для части пути в имени файла, а не для всего пути.

Комментарий JeffreyLWhitledge предоставляет ссылку на рекурсивную версию, которая может работать (хотя и не всегда) для определения полного пути.

Альтернативное решение

Вот решение, которое помогло мне переместить файлы между Windows и сервером, используя пути с учетом регистра. Он идет по дереву каталогов и исправляет каждую запись GetFileSystemEntries(), Если часть пути недопустима (UNC или имя папки), то он исправляет путь только до этой точки, а затем использует исходный путь для того, что он не может найти. В любом случае, надеюсь, это сэкономит время других людей при решении той же проблемы.

private string GetCaseSensitivePath(string path)
{
    var root = Path.GetPathRoot(path);
    try
    {
        foreach (var name in path.Substring(root.Length).Split(Path.DirectorySeparatorChar))
            root = Directory.GetFileSystemEntries(root, name).First();
    }
    catch (Exception e)
    {
        // Log("Path not found: " + path);
        root += path.Substring(root.Length);
    }
    return root;
}

Вот альтернативное решение, работает с файлами и каталогами. Использует GetFinalPathNameByHandle, который поддерживается только для настольных приложений на Vista/Server2008 или выше в соответствии с документами.

Обратите внимание, что он разрешит символическую ссылку, если вы дадите ей ссылку, которая является частью поиска "окончательного" пути.

// http://www.pinvoke.net/default.aspx/shell32/GetFinalPathNameByHandle.html
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint GetFinalPathNameByHandle(SafeFileHandle hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
private const uint FILE_NAME_NORMALIZED = 0x0;

static string GetFinalPathNameByHandle(SafeFileHandle fileHandle)
{
    StringBuilder outPath = new StringBuilder(1024);

    var size = GetFinalPathNameByHandle(fileHandle, outPath, (uint)outPath.Capacity, FILE_NAME_NORMALIZED);
    if (size == 0 || size > outPath.Capacity)
        throw new Win32Exception(Marshal.GetLastWin32Error());

    // may be prefixed with \\?\, which we don't want
    if (outPath[0] == '\\' && outPath[1] == '\\' && outPath[2] == '?' && outPath[3] == '\\')
        return outPath.ToString(4, outPath.Length - 4);

    return outPath.ToString();
}

// http://www.pinvoke.net/default.aspx/kernel32.createfile
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFileHandle CreateFile(
     [MarshalAs(UnmanagedType.LPTStr)] string filename,
     [MarshalAs(UnmanagedType.U4)] FileAccess access,
     [MarshalAs(UnmanagedType.U4)] FileShare share,
     IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
     [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
     [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
     IntPtr templateFile);
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

public static string GetFinalPathName(string dirtyPath)
{
    // use 0 for access so we can avoid error on our metadata-only query (see dwDesiredAccess docs on CreateFile)
    // use FILE_FLAG_BACKUP_SEMANTICS for attributes so we can operate on directories (see Directories in remarks section for CreateFile docs)

    using (var directoryHandle = CreateFile(
        dirtyPath, 0, FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, FileMode.Open,
        (FileAttributes)FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero))
    {
        if (directoryHandle.IsInvalid)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        return GetFinalPathNameByHandle(directoryHandle);
    }
}

Чтобы получить фактический путь к файлу (это не будет работать для папок), выполните следующие действия:

  1. Вызов CreateFileMapping создать сопоставление для файла.
  2. Вызов GetMappedFileName чтобы получить имя файла.
  3. использование QueryDosDevice преобразовать его в имя пути в стиле MS-DOS.

Если вы хотите написать более надежную программу, которая также работает с каталогами (но с большей трудностью и несколькими недокументированными функциями), выполните следующие действия:

  1. Получить дескриптор файла / папки с CreateFile или же NtOpenFile,
  2. Вызов NtQueryObject чтобы получить полное имя пути.
  3. Вызов NtQueryInformationFile с FileNameInformation чтобы получить объемно-относительный путь.
  4. Используя два пути выше, получите компонент пути, который представляет сам том. Например, если вы получаете \Device\HarddiskVolume1\Hello.txt для первого пути и \Hello.txt во-вторых, теперь вы знаете, путь тома \Device\HarddiskVolume1,
  5. Используйте плохо документированные контрольные коды ввода-вывода Mount Manager или QueryDosDevice для преобразования замените часть тома полного пути в стиле NT буквой диска.

Теперь у вас есть реальный путь к файлу.

Поскольку ответ Борха не работает для томов, в которых имена 8.3 отключены, здесь предлагается рекурсивная реализация, которую предлагает Tergiver (работает с файлами и папками, а также с файлами и папками общих папок UNC, но не на именах их компьютеров или именах общих папок).

Несуществующий файл или папки не являются проблемой, то, что существует, проверяется и исправляется, но вы можете столкнуться с проблемами перенаправления папок, например, при попытке получить правильный путь к "C:\WinDoWs\sYsteM32\driVErs\eTC\Hosts" вы получите "C:\Windows\System32\drivers\eTC\hosts" в 64-битных окнах, так как нет папки "etc" с "C:\Windows\sysWOW64\drivers".

Тестовый сценарий:

        Directory.CreateDirectory(@"C:\Temp\SomeFolder");
        File.WriteAllLines(@"C:\Temp\SomeFolder\MyTextFile.txt", new String[] { "Line1", "Line2" });

Использование:

        FileInfo myInfo = new FileInfo(@"C:\TEMP\SOMEfolder\MyTeXtFiLe.TxT");
        String myResult = myInfo.GetFullNameWithCorrectCase(); //Returns "C:\Temp\SomeFolder\MyTextFile.txt"

Код:

public static class FileSystemInfoExt {

    public static String GetFullNameWithCorrectCase(this FileSystemInfo fileOrFolder) {
        //Check whether null to simulate instance method behavior
        if (Object.ReferenceEquals(fileOrFolder, null)) throw new NullReferenceException();
        //Initialize common variables
        String myResult = GetCorrectCaseOfParentFolder(fileOrFolder.FullName);
        return myResult;
    }

    private static String GetCorrectCaseOfParentFolder(String fileOrFolder) {
        String myParentFolder = Path.GetDirectoryName(fileOrFolder);
        String myChildName = Path.GetFileName(fileOrFolder);
        if (Object.ReferenceEquals(myParentFolder, null)) return fileOrFolder.TrimEnd(new char[]{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
        if (Directory.Exists(myParentFolder)) {
            //myParentFolder = GetLongPathName.Invoke(myFullName);
            String myFileOrFolder = Directory.GetFileSystemEntries(myParentFolder, myChildName).FirstOrDefault();
            if (!Object.ReferenceEquals(myFileOrFolder, null)) {
                myChildName = Path.GetFileName(myFileOrFolder);
            }
        }
        return GetCorrectCaseOfParentFolder(myParentFolder) + Path.DirectorySeparatorChar + myChildName;
    }

}

Я пытался избежать импорта dll, поэтому лучшим способом для меня было использование System.Linq и класса System.IO.Directory.

Для вашего примера Реальный путь: d: \ src \ File.txt Пользователь дал мне: D: \ src \ file.txt

Код для этого:

using System.Linq;

public static class PathUtils
{
    public static string RealPath(string inputPath)
    {
        return Directory.GetFiles(Path.GetDirectoryName(inputPath))
            .FirstOrDefault(p => String.Equals(Path.GetFileName(p), 
                Path.GetFileName(inputPath), StringComparison.OrdinalIgnoreCase));
    }
}

var p = PathUtils.RealPath(@"D: \ src \ file.txt");

Метод должен возвращать путь "d: \ src \ File.txt" или "D: \ src \ File.txt".

Вот как я это делаю. Изначально я зависел от GetFinalPathNameByHandleчто очень хорошо, но, к сожалению, некоторые пользовательские файловые системы не поддерживают его (конечно, NTFS поддерживает). Я тоже пробовал NtQueryObject с участием ObjectNameInformation но опять же, они не обязательно сообщают исходное имя файла.

Итак, вот еще один «ручной» способ:

      public static string GetRealPath(string fullPath)
{
    if (fullPath == null)
        return null; // invalid

    var pos = fullPath.LastIndexOf(Path.DirectorySeparatorChar);
    if (pos < 0 || pos == (fullPath.Length - 1))
        return fullPath.ToUpperInvariant(); // drive letter

    var dirPath = fullPath.Substring(0, pos);
    var realPath = GetRealPath(dirPath); // go recursive, we want the final full path
    if (realPath == null)
        return null; // doesn't exist

    var dir = new DirectoryInfo(realPath);
    if (!dir.Exists)
        return null; // doesn't exist
    
    var fileName = fullPath.Substring(pos + 1);
    if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) // avoid wildcard calls
        return null;

    return dir.EnumerateFileSystemInfos(fileName).FirstOrDefault()?.FullName; // may return null
}

В Windows пути нечувствительны к регистру. Так что оба пути одинаково реальны.

Если вы хотите получить какой-либо путь с канонической заглавной буквой (то есть, как Windows считает, что он должен быть заглавной), вы можете вызвать FindFirstFile() с путем в качестве маски, а затем взять полное имя найденного файла. Если путь неверен, то вы не получите каноническое имя, естественно.

Другие вопросы по тегам