Skip to content

Commit

Permalink
Unify hard link and cow detection logic
Browse files Browse the repository at this point in the history
  • Loading branch information
huoyaoyuan committed Oct 12, 2024
1 parent 4af8dbb commit 1a89df5
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 42 deletions.
50 changes: 8 additions & 42 deletions osu.Game/IO/CopyOnWriteHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace osu.Game.IO
{
Expand All @@ -16,32 +14,17 @@ public static bool CheckAvailability(string testDestinationPath, string testSour
{
// Starting from Windows 11 24H2, the CopyFile syscall will perform CoW cloning on ReFS drives.

const string test_filename = "_cow_test";

testDestinationPath = Path.Combine(testDestinationPath, test_filename);
testSourcePath = Path.Combine(testSourcePath, test_filename);

try
{
using var sourceFileHandle = File.OpenHandle(testSourcePath, FileMode.CreateNew, FileAccess.Write, FileShare.Write, FileOptions.DeleteOnClose);
using var destinationFileHandle = File.OpenHandle(testDestinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.Write, FileOptions.DeleteOnClose);

// Attention: Windows supports mounting volume into folders. Don't detect volumn from the volume letter of path.
if (!GetVolumeInformationByHandle(sourceFileHandle, IntPtr.Zero, 0, out uint sourceVolume, out _, out uint flags, IntPtr.Zero, 0))
return false;

if (!GetVolumeInformationByHandle(destinationFileHandle, IntPtr.Zero, 0, out uint destinationVolume, out _, out _, IntPtr.Zero, 0))
return false;
// Attention: Windows supports mounting volume into folders. Don't detect volumn from the volume letter of path.
if (!HardLinkHelper.GetVolumeInformationForDirectory(testSourcePath, out uint sourceVolume, out uint flags))
return false;

if (sourceVolume != destinationVolume)
return false;
if (!HardLinkHelper.GetVolumeInformationForDirectory(testDestinationPath, out uint destinationVolume, out _))
return false;

return (flags & FILE_SUPPORTS_BLOCK_REFCOUNTING) != 0;
}
catch
{
if (sourceVolume != destinationVolume)
return false;
}

return (flags & HardLinkHelper.FILE_SUPPORTS_HARD_LINKS) != 0;
}

return false;
Expand All @@ -58,22 +41,5 @@ public static bool TryCloneFile(string destinationPath, string sourcePath)

return false;
}

#region Windows native methods

public const uint FILE_SUPPORTS_BLOCK_REFCOUNTING = 0x08000000;

[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool GetVolumeInformationByHandle(
SafeFileHandle hFile,
IntPtr lpVolumeNameBuffer,
uint nVolumeNameSize,
out uint lpVolumeSerialNumber,
out uint lpMaximumComponentLength,
out uint lpFileSystemFlags,
IntPtr lpFileSystemNameBuffer,
uint nFileSystemNameSize);

#endregion
}
}
57 changes: 57 additions & 0 deletions osu.Game/IO/HardLinkHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ public static bool CheckAvailability(string testDestinationPath, string testSour
if (!RuntimeInfo.IsDesktop)
return false;

if (OperatingSystem.IsWindows())
{
// Attention: Windows supports mounting volume into folders. Don't detect volumn from the volume letter of path.
if (!GetVolumeInformationForDirectory(testSourcePath, out uint sourceVolume, out uint flags))
return false;

if (!GetVolumeInformationForDirectory(testDestinationPath, out uint destinationVolume, out _))
return false;

if (sourceVolume != destinationVolume)
return false;

return (flags & FILE_SUPPORTS_HARD_LINKS) != 0;
}

const string test_filename = "_hard_link_test";

testDestinationPath = Path.Combine(testDestinationPath, test_filename);
Expand Down Expand Up @@ -110,6 +125,48 @@ public static int GetFileLinkCount(string filePath)
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);

// https://learn.microsoft.com/windows/win32/fileio/obtaining-a-handle-to-a-directory
// The flag is required when opening directory as a handle. It's not accepted by File.OpenHandle.
public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
FileAccess dwDesiredAccess,
FileShare dwShareMode,
IntPtr lpSecurityAttributes,
FileMode dwCreationDisposition,
FileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);

public const uint FILE_SUPPORTS_HARD_LINKS = 0x00400000;
public const uint FILE_SUPPORTS_BLOCK_REFCOUNTING = 0x08000000;

[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool GetVolumeInformationByHandle(
SafeFileHandle hFile,
IntPtr lpVolumeNameBuffer,
uint nVolumeNameSize,
out uint lpVolumeSerialNumber,
out uint lpMaximumComponentLength,
out uint lpFileSystemFlags,
IntPtr lpFileSystemNameBuffer,
uint nFileSystemNameSize);

public static bool GetVolumeInformationForDirectory(string directoryPath, out uint volumeSerialNumber, out uint fileSystemFlags)
{
using var handle = CreateFile(directoryPath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, (FileAttributes)FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);

if (!handle.IsInvalid)
{
volumeSerialNumber = 0;
fileSystemFlags = 0;
return false;
}

return GetVolumeInformationByHandle(handle, IntPtr.Zero, 0, out volumeSerialNumber, out _, out fileSystemFlags, IntPtr.Zero, 0);
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation);

Expand Down

0 comments on commit 1a89df5

Please sign in to comment.