在C#中确定两个路径引用是否指向同一文件的最佳方法

40
在即将发布的Java7中,有一个新API可用于检查两个文件对象是否是同一文件引用。
.NET框架中是否提供了类似的API?
我在MSDN上搜过了,但没有找到相关信息。
我想要一个简单的解决方案,但又不想通过文件名进行比较,因为这会导致硬链接/符号链接和不同路径风格(例如\\?\C:\C:\)出现问题。
我的目标是防止重复的文件被拖放到我的链接列表中。

什么?Java有一些针对Windows文件系统的功能,而.NET却没有? - nawfal
@nawfal,有很多Win32的东西没有被BCL公开,尽管人们想要使用它们。 - binki
7个回答

34
据我所见,JDK7的做法是通过调用GetFileInformationByHandle函数并比较dwVolumeSerialNumber、nFileIndexHigh和nFileIndexLow来实现(1) (2) (3) (4)
根据MSDN:

您可以比较BY_HANDLE_FILE_INFORMATION结构中返回的VolumeSerialNumber和FileIndex成员,以确定两个路径是否映射到同一个目标;例如,您可以比较两个文件路径并确定它们是否映射到同一个目录。

我认为这个函数没有被.NET封装,因此您需要使用P/Invoke

它可能适用于网络文件,也可能不适用。根据MSDN:

取决于操作系统的底层网络组件和连接的服务器类型,GetFileInformationByHandle函数可能会失败、返回部分信息或完整信息。

一个快速测试显示,使用SMB/Samba连接的Linux系统上的符号链接可以正常工作(具有相同的值),但是当使用指向同一文件的不同共享访问文件时,它无法检测到文件是否相同(FileIndex相同,但VolumeSerialNumber不同)。


1
这绝对是一个可行的方案。MSDN表示这三个字段唯一标识了一个文件。但是你需要使用Win32 API 来获取它们。 - Dana Robinson
谢谢,我添加了MSDN引用。 - Rasmus Faber
好的,FileRef只是一个接口(由WindowsPath实现),所以我认为在源代码中不会有任何有趣的东西。但如果你认为值得一看,我可以添加它。 - Rasmus Faber
我找到了这个链接:https://dev59.com/G3VC5IYBdhLWcg3whBaj?answer=274652#274652 (虽然我自己没有测试过,但它使用了GetFileInformationByHandle函数)。 - tuinstoel
有一件事我没有看到提到:如果你有一个硬链接,那么即使两个文件的内容完全相同,它们也被视为不同的文件。这是需要记住的一点。无论这是否重要,都非常取决于你对文件的处理方式。 - Alexis Wilke
显示剩余2条评论

8

编辑:请注意,@Rasmus Faber在他的回答中提到了Win32 api中的GetFileInformationByHandle函数,它可以实现你想要的功能,请查看并点赞他的回答以获取更多信息。


我认为你需要一个操作系统函数来获得所需的信息,否则无论你做什么都会出现一些错误的结果。

例如,以下两个路径是否指向同一个文件?

  • \server\share\path\filename.txt
  • \server\d$\temp\path\filename.txt

我建议你先考虑不在列表中出现重复文件的重要性,然后尽力而为。

话虽如此,Path类中有一个方法可以完成部分工作:Path.GetFullPath,它至少可以将路径扩展为长名称,根据现有结构。之后,你只需比较字符串。但这并不是绝对可靠的,也无法处理我上面示例中的两个链接。


文档还说:“否则,此方法将检查FileRefs是否定位到同一文件,并根据实现方式可能需要打开或访问两个文件。” 我非常想看看这可以如何完成! - Hosam Aly
使用Path.GetFullPath无效,尝试一下 if (Path.GetFullPath(@"c:\vobp.log") == Path.GetFullPath(@"c:\vobp.log".ToUpper())) {} - tuinstoel
是的,注意一下,我说的是一部分工作,.NET 中没有一个方法可以替你完成所有工作。 - Lasse V. Karlsen

5
这里是使用GetFileInformationByHandle的C#实现IsSameFile

NativeMethods.cs

public static class NativeMethods
{
  [StructLayout(LayoutKind.Explicit)]
  public struct BY_HANDLE_FILE_INFORMATION
  {
    [FieldOffset(0)]
    public uint FileAttributes;

    [FieldOffset(4)]
    public FILETIME CreationTime;

    [FieldOffset(12)]
    public FILETIME LastAccessTime;

    [FieldOffset(20)]
    public FILETIME LastWriteTime;

    [FieldOffset(28)]
    public uint VolumeSerialNumber;

    [FieldOffset(32)]
    public uint FileSizeHigh;

    [FieldOffset(36)]
    public uint FileSizeLow;

    [FieldOffset(40)]
    public uint NumberOfLinks;

    [FieldOffset(44)]
    public uint FileIndexHigh;

    [FieldOffset(48)]
    public uint FileIndexLow;
  }

  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern bool GetFileInformationByHandle(SafeFileHandle hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  public static extern SafeFileHandle CreateFile([MarshalAs(UnmanagedType.LPTStr)] string filename,
    [MarshalAs(UnmanagedType.U4)] FileAccess access,
    [MarshalAs(UnmanagedType.U4)] FileShare share,
    IntPtr securityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
    IntPtr templateFile);
}

PathUtility.cs

public static bool IsSameFile(string path1, string path2)
{
  using (SafeFileHandle sfh1 = NativeMethods.CreateFile(path1, FileAccess.Read, FileShare.ReadWrite, 
      IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
  {
    if (sfh1.IsInvalid)
      Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

    using (SafeFileHandle sfh2 = NativeMethods.CreateFile(path2, FileAccess.Read, FileShare.ReadWrite,
      IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
    {
      if (sfh2.IsInvalid)
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

      NativeMethods.BY_HANDLE_FILE_INFORMATION fileInfo1;
      bool result1 = NativeMethods.GetFileInformationByHandle(sfh1, out fileInfo1);
      if (!result1)
        throw new IOException(string.Format("GetFileInformationByHandle has failed on {0}", path1));

      NativeMethods.BY_HANDLE_FILE_INFORMATION fileInfo2;
      bool result2 = NativeMethods.GetFileInformationByHandle(sfh2, out fileInfo2);
      if (!result2)
        throw new IOException(string.Format("GetFileInformationByHandle has failed on {0}", path2));

      return fileInfo1.VolumeSerialNumber == fileInfo2.VolumeSerialNumber
        && fileInfo1.FileIndexHigh == fileInfo2.FileIndexHigh
        && fileInfo1.FileIndexLow == fileInfo2.FileIndexLow;
    }
  }
}

2
答案:没有绝对可靠的方法可以比较两个字符串基本路径,以确定它们是否指向同一个文件。
主要原因是,由于文件系统重定向(联接、符号链接等),看似不相关的路径可能会指向完全相同的文件。例如:

"d:\temp\foo.txt" "c:\othertemp\foo.txt"

这些路径有可能指向同一个文件。在这种情况下,任何字符串比较函数都无法作为确定两个路径是否指向同一文件的依据。
下一步是比较操作系统文件信息。打开两个路径的文件并比较句柄信息。在Windows中,可以使用GetFileInformationByHandle来完成此操作。Lucian Wischik在这个主题上写了一篇出色的文章
但是,这种方法仍然存在问题。它只适用于执行检查的用户帐户能够打开两个文件进行阅读的情况。有许多因素可能会防止用户打开一个或两个文件,包括但不限于...
- 文件权限不足 - 路径中某个目录的权限不足 - 第一个文件打开和第二个文件打开之间发生的文件系统更改,例如网络断开连接。
当你开始看所有这些问题时,你就会明白为什么Windows没有提供一种方法来确定两个路径是否相同。这只是一个不容易/无法回答的问题。

1
GetFileInformationByHandle 的文档称:“nFileIndexLow:与文件相关联的唯一标识符的低位部分。仅当至少有一个进程打开文件时,此值才有用。如果没有进程打开它,则下次打开该文件时索引可能会更改。” - Integer Poet

1

一开始我认为这很容易, 但是不起作用:

  string fileName1 = @"c:\vobp.log";
  string fileName2 = @"c:\vobp.log".ToUpper();
  FileInfo fileInfo1 = new FileInfo(fileName1);
  FileInfo fileInfo2 = new FileInfo(fileName2);

  if (!fileInfo1.Exists || !fileInfo2.Exists)
  {
    throw new Exception("one of the files does not exist");
  }

  if (fileInfo1.FullName == fileInfo2.FullName)
  {
    MessageBox.Show("equal"); 
  }

也许这个库可以帮助你 http://www.codeplex.com/FileDirectoryPath。我自己没有使用过。

编辑:在该网站上看到了这个例子:

  //
  // Path comparison
  //
  filePathAbsolute1 = new FilePathAbsolute(@"C:/Dir1\\File.txt");
  filePathAbsolute2 = new FilePathAbsolute(@"C:\DIR1\FILE.TXT");
  Debug.Assert(filePathAbsolute1.Equals(filePathAbsolute2));
  Debug.Assert(filePathAbsolute1 == filePathAbsolute2);

我不知道,我自己没有使用过它。 - tuinstoel
在第一种情况下,它不起作用,因为您正在进行字符串比较。您尝试比较 FileInfo 本身了吗? - nawfal

0
如果您需要反复比较相同的文件名,我建议您考虑规范化这些名称。
在Unix系统下,有一个realpath()函数可以规范化您的路径。我认为这通常是最好的选择,特别是对于复杂的路径。然而,它可能无法处理通过网络连接挂载的卷。
然而,基于realpath()方法,如果您想支持多个卷,包括网络卷,您可以编写自己的函数来检查路径中的每个目录名,并确定两个路径中的卷引用是否相同。话虽如此,挂载点可能不同(即目标卷上的路径可能不是该卷的根),所以解决所有问题并不容易,但绝对是可行的(否则首先它怎么能工作呢?!)。
一旦文件名被正确地规范化,简单的字符串比较就能给出正确的答案。
如果您不需要反复比较相同的文件名,Rasmus的回答可能是最快的方法。

-4

4
为什么要使用MD5?他可以直接比较内容。对于相同的情况,这样做需要相同的时间;对于大多数不同的情况,这样做会更快失败。 - configurator
5
同时,这无法区分相同文件的复制品。 - configurator
好的观点。我主要是基于MD5结果可以被缓存并且随着新结果的加入更容易进行比较这一基础。 - Soviut
你可能误解了我的问题。我不是在问文件内容是否相同,而是两个文件路径引用是否指向同一个文件。如果我锁定/修改其中一个,另一个也会受到影响。 - Dennis C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接