// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using Microsoft.Win32.SafeHandles; using osu.Framework; namespace osu.Game.IO { internal static class HardLinkHelper { public static bool CheckAvailability(string testDestinationPath, string testSourcePath) { // TODO: Add macOS support for hardlinks. if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows && RuntimeInfo.OS != RuntimeInfo.Platform.Linux) return false; const string test_filename = "_hard_link_test"; testDestinationPath = Path.Combine(testDestinationPath, test_filename); testSourcePath = Path.Combine(testSourcePath, test_filename); cleanupFiles(); try { File.WriteAllText(testSourcePath, string.Empty); // Test availability by creating an arbitrary hard link between the source and destination paths. return TryCreateHardLink(testDestinationPath, testSourcePath); } catch { return false; } finally { cleanupFiles(); } void cleanupFiles() { try { File.Delete(testDestinationPath); File.Delete(testSourcePath); } catch { } } } /// /// Attempts to create a hard link from to , /// using platform-specific native methods. /// /// /// Hard links are only available on Windows and Linux. /// /// Whether the hard link was successfully created. public static bool TryCreateHardLink(string destinationPath, string sourcePath) { switch (RuntimeInfo.OS) { case RuntimeInfo.Platform.Windows: return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero); case RuntimeInfo.Platform.Linux: return link(sourcePath, destinationPath) == 0; default: return false; } } // For future use (to detect if a file is a hard link with other references existing on disk). public static int GetFileLinkCount(string filePath) { int result = 0; switch (RuntimeInfo.OS) { case RuntimeInfo.Platform.Windows: SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero); ByHandleFileInformation fileInfo; if (GetFileInformationByHandle(handle, out fileInfo)) result = (int)fileInfo.NumberOfLinks; CloseHandle(handle); break; case RuntimeInfo.Platform.Linux: if (stat(filePath, out var statbuf) == 0) result = (int)statbuf.st_nlink; break; } return result; } #region Windows native methods [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern SafeFileHandle CreateFile( string lpFileName, [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode, IntPtr lpSecurityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition, [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(SafeHandle hObject); [StructLayout(LayoutKind.Sequential)] private struct ByHandleFileInformation { public readonly uint FileAttributes; public readonly FILETIME CreationTime; public readonly FILETIME LastAccessTime; public readonly FILETIME LastWriteTime; public readonly uint VolumeSerialNumber; public readonly uint FileSizeHigh; public readonly uint FileSizeLow; public readonly uint NumberOfLinks; public readonly uint FileIndexHigh; public readonly uint FileIndexLow; } #endregion #region Linux native methods [DllImport("libc", SetLastError = true)] public static extern int link(string oldpath, string newpath); [DllImport("libc", SetLastError = true)] private static extern int stat(string pathname, out struct_stat statbuf); // ReSharper disable once InconsistentNaming // Struct layout is likely non-portable across unices. Tread with caution. [StructLayout(LayoutKind.Sequential)] private struct struct_stat { public readonly long st_dev; public readonly long st_ino; public readonly long st_nlink; public readonly int st_mode; public readonly int st_uid; public readonly int st_gid; public readonly long st_rdev; public readonly long st_size; public readonly long st_blksize; public readonly long st_blocks; public readonly timespec st_atim; public readonly timespec st_mtim; public readonly timespec st_ctim; } // ReSharper disable once InconsistentNaming [StructLayout(LayoutKind.Sequential)] private struct timespec { public readonly long tv_sec; public readonly long tv_nsec; } #endregion } }