在 .NET 中,如何在 NTFS 中创建连接(而不是符号链接)?

发布于 2024-08-04 20:18:42 字数 1617 浏览 10 评论 0原文

我正在尝试创建一个 NTFS 连接。从 cmd 行,我可以使用 sysinternals 中的 junction.exe 工具。连接点的 DIR cmd 的输出如下所示:

 Volume in drive C has no label.
 Volume Serial Number is C8BC-2EBD

 Directory of c:\users\cheeso\Documents

03/22/2009  09:45 PM    <JUNCTION>     My Music [\??\c:\users\cheeso\Music]
05/11/2007  05:42 PM    <DIR>          My Received Files
03/22/2009  09:46 PM    <JUNCTION>     my videos [\??\c:\users\cheeso\Videos]

我在某处读到连接点是符号链接的子集。

因此,我尝试使用 CreateSymbolicLink 创建一个连接点。当我这样做时,我实际上得到的是符号链接,而不是连接点。

09/09/2009  11:50 AM    <SYMLINKD>     newLink [.\]

还有 CreateHardLink。那里的文档说结点(又名“重新解析点”)是硬链接的子集。但我似乎无法让这个电话工作。它已完成,但没有创建硬链接或连接。

我正在使用 .NET/C#,导入如下所示:

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateSymbolicLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern int CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateHardLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern bool CreateHardLink(string lpFileName,
                                             string lpExistingFileName,
                                             IntPtr mustBeNull);

我做错了什么?
如何从 C# 中创建连接?

I'm trying to create an NTFS Junction. From the cmd line I can do this using the junction.exe tool from sysinternals. The output of a DIR cmd for a junction looks like this:

 Volume in drive C has no label.
 Volume Serial Number is C8BC-2EBD

 Directory of c:\users\cheeso\Documents

03/22/2009  09:45 PM    <JUNCTION>     My Music [\??\c:\users\cheeso\Music]
05/11/2007  05:42 PM    <DIR>          My Received Files
03/22/2009  09:46 PM    <JUNCTION>     my videos [\??\c:\users\cheeso\Videos]

I read somewhere that Junctions are a subset of Symbolic Links.

So I tried using CreateSymbolicLink to create a Junction. When I do this, I actually get a Symlink, not a junction.

09/09/2009  11:50 AM    <SYMLINKD>     newLink [.\]

There is also CreateHardLink. The doc there says junctions (aka "Reparse Points") are a subset of hardlinks. but I can't seem to get this call to work. It completes but there is no hardlink or junction created.

I'm using .NET/C# and the imports look like this:

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateSymbolicLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern int CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);

    [Interop.DllImport("kernel32.dll", EntryPoint="CreateHardLinkW", CharSet=Interop.CharSet.Unicode)]
    public static extern bool CreateHardLink(string lpFileName,
                                             string lpExistingFileName,
                                             IntPtr mustBeNull);

What am I doing wrong?
How can I create a Junction from within C#?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

冷情 2024-08-11 20:18:42

看起来可以,并且有人在 CodeProject 上创建了一个库,其中包含许多用 C# 构建的函数来处理连接点。

http://www.codeproject.com/KB/files/JunctionPointsNet.aspx

看起来他实际上是使用以下 DllImport 来完成它:

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
        IntPtr InBuffer, int nInBufferSize,
        IntPtr OutBuffer, int nOutBufferSize,
        out int pBytesReturned, IntPtr lpOverlapped);

It looks like you can, and somebody's created a library on CodeProject that has a number of functions they've built in C# to work with Junction points.

http://www.codeproject.com/KB/files/JunctionPointsNet.aspx

It looks like he's actually using the following DllImport to accomplish it:

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
        IntPtr InBuffer, int nInBufferSize,
        IntPtr OutBuffer, int nOutBufferSize,
        out int pBytesReturned, IntPtr lpOverlapped);
给妤﹃绝世温柔 2024-08-11 20:18:42

我已经简化/更新了 CodeProject 上 Jeff Brown 的 CreateJunction 代码,例如使用自动编组将结构传递给DeviceIoControl,而不必手动管理内存。我这样做只是为了创建一个连接,因为您可以使用 Directory.Delete() 进行删除,并且 .Net 的 GetAttributes 返回它是否具有重新分析点。

我还删除了目标目录存在检查,因为我发现能够创建到不存在或稍后将存在的文件夹的连接非常有用。 (不同的驱动器等)

我无法弄清楚的一件事是,添加到结构成员和 nInBufferSize DeviceIoControl 参数的字符串长度中的大小,它们似乎没有加起来等于 Marshal.SizeOf 返回值。

我在 VB.Net 中完成了此操作,因此我使用 IC#Code 的 CodeConverter 扩展将其转换为 C#:

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

public class Junction
{
    [Flags]
    private enum Win32FileAccess : uint
    {
        GenericRead = 0x80000000U,
        GenericWrite = 0x40000000U,
        GenericExecute = 0x20000000U,
        GenericAll = 0x10000000U
    }

    [Flags]
    private enum Win32FileAttribute : uint
    {
        AttributeReadOnly = 0x1U,
        AttributeHidden = 0x2U,
        AttributeSystem = 0x4U,
        AttributeDirectory = 0x10U,
        AttributeArchive = 0x20U,
        AttributeDevice = 0x40U,
        AttributeNormal = 0x80U,
        AttributeTemporary = 0x100U,
        AttributeSparseFile = 0x200U,
        AttributeReparsePoint = 0x400U,
        AttributeCompressed = 0x800U,
        AttributeOffline = 0x1000U,
        AttributeNotContentIndexed = 0x2000U,
        AttributeEncrypted = 0x4000U,
        AttributeIntegrityStream = 0x8000U,
        AttributeVirtual = 0x10000U,
        AttributeNoScrubData = 0x20000U,
        AttributeEA = 0x40000U,
        AttributeRecallOnOpen = 0x40000U,
        AttributePinned = 0x80000U,
        AttributeUnpinned = 0x100000U,
        AttributeRecallOnDataAccess = 0x400000U,
        FlagOpenNoRecall = 0x100000U,
        /// <summary>
        /// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
        /// whether or not the filter that controls the reparse point is operational.
        /// <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
        /// <br />If the file is not a reparse point, then this flag is ignored.
        /// </summary>
        FlagOpenReparsePoint = 0x200000U,
        FlagSessionAware = 0x800000U,
        FlagPosixSemantics = 0x1000000U,
        /// <summary>
        /// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
        /// </summary>
        FlagBackupSemantics = 0x2000000U,
        FlagDeleteOnClose = 0x4000000U,
        FlagSequentialScan = 0x8000000U,
        FlagRandomAccess = 0x10000000U,
        FlagNoBuffering = 0x20000000U,
        FlagOverlapped = 0x40000000U,
        FlagWriteThrough = 0x80000000U
    }

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

    // Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct ReparseDataBuffer
    {
        /// <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
        public uint ReparseTag;
        /// <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
        public ushort ReparseDataLength;
        /// <summary>Reserved; do not use.</summary>
        private ushort Reserved;
        /// <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
        public ushort SubstituteNameOffset;
        /// <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
        public ushort SubstituteNameLength;
        /// <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
        public ushort PrintNameOffset;
        /// <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
        public ushort PrintNameLength;
        /// <summary>
        /// A buffer containing the unicode-encoded path string. The path string contains the substitute name
        /// string and print name string. The substitute name and print name strings can appear in any order.
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8184)]
        internal string PathBuffer;
        // with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
        // 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
    }

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode,
        [In] ReparseDataBuffer lpInBuffer, uint nInBufferSize,
        IntPtr lpOutBuffer, uint nOutBufferSize,
        [Out] uint lpBytesReturned, IntPtr lpOverlapped);

    public static void Create(string junctionPath, string targetDir, bool overwrite = false)
    {
        const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003U;
        const uint FSCTL_SET_REPARSE_POINT = 0x900A4U;
        // This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
        const string NonInterpretedPathPrefix = @"\??\";

        if (Directory.Exists(junctionPath))
        {
            if (!overwrite)
                throw new IOException("Directory already exists and overwrite parameter is false.");
        }
        else
        {
            Directory.CreateDirectory(junctionPath);
        }
        targetDir = NonInterpretedPathPrefix + Path.GetFullPath(targetDir);

        using (var reparsePointHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
            FileShare.Read | FileShare.Write | FileShare.Delete, IntPtr.Zero, FileMode.Open,
            Win32FileAttribute.FlagBackupSemantics | Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero))
        {
            if (reparsePointHandle.IsInvalid || Marshal.GetLastWin32Error() != 0)
            {
                throw new IOException("Unable to open reparse point.", new Win32Exception());
            }

            // unicode string is 2 bytes per character, so *2 to get byte length
            ushort byteLength = (ushort)(targetDir.Length * 2);
            var reparseDataBuffer = new ReparseDataBuffer()
            {
                ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
                ReparseDataLength = (ushort)(byteLength + 12u),
                SubstituteNameOffset = 0,
                SubstituteNameLength = byteLength,
                PrintNameOffset = (ushort)(byteLength + 2u),
                PrintNameLength = 0,
                PathBuffer = targetDir
            };

            bool result = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, (uint)(byteLength + 20), IntPtr.Zero, 0u, 0u, IntPtr.Zero);
            if (!result)
                throw new IOException("Unable to create junction point.", new Win32Exception());
        }
    }
}

以及源 VB.Net:

Imports System
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles

Public Class Junction
    <Flags>
    Private Enum Win32FileAccess As UInteger
        GenericRead = &H80000000UI
        GenericWrite = &H40000000
        GenericExecute = &H20000000
        GenericAll = &H10000000
    End Enum

    <Flags>
    Private Enum Win32FileAttribute As UInteger
        AttributeReadOnly = &H1
        AttributeHidden = &H2
        AttributeSystem = &H4
        AttributeDirectory = &H10
        AttributeArchive = &H20
        AttributeDevice = &H40
        AttributeNormal = &H80
        AttributeTemporary = &H100
        AttributeSparseFile = &H200
        AttributeReparsePoint = &H400
        AttributeCompressed = &H800
        AttributeOffline = &H1000
        AttributeNotContentIndexed = &H2000
        AttributeEncrypted = &H4000
        AttributeIntegrityStream = &H8000
        AttributeVirtual = &H10000
        AttributeNoScrubData = &H20000
        AttributeEA = &H40000
        AttributeRecallOnOpen = &H40000
        AttributePinned = &H80000
        AttributeUnpinned = &H100000
        AttributeRecallOnDataAccess = &H400000
        FlagOpenNoRecall = &H100000
        ''' <summary>
        ''' Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
        ''' whether or not the filter that controls the reparse point is operational.
        ''' <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
        ''' <br />If the file is not a reparse point, then this flag is ignored.
        ''' </summary>
        FlagOpenReparsePoint = &H200000
        FlagSessionAware = &H800000
        FlagPosixSemantics = &H1000000
        ''' <summary>
        ''' You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
        ''' </summary>
        FlagBackupSemantics = &H2000000
        FlagDeleteOnClose = &H4000000
        FlagSequentialScan = &H8000000
        FlagRandomAccess = &H10000000
        FlagNoBuffering = &H20000000
        FlagOverlapped = &H40000000
        FlagWriteThrough = &H80000000UI
    End Enum

    <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function CreateFile(lpFileName As String, dwDesiredAccess As Win32FileAccess,
                                       dwShareMode As FileShare, lpSecurityAttributes As IntPtr,
                                       dwCreationDisposition As FileMode, dwFlagsAndAttributes As Win32FileAttribute,
                                       hTemplateFile As IntPtr) As SafeFileHandle
    End Function

    ' Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
    Private Structure ReparseDataBuffer
        ''' <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
        Public ReparseTag As UInteger
        ''' <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
        Public ReparseDataLength As UShort
        ''' <summary>Reserved; do not use.</summary>
        Private Reserved As UShort
        ''' <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
        Public SubstituteNameOffset As UShort
        ''' <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
        Public SubstituteNameLength As UShort
        ''' <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
        Public PrintNameOffset As UShort
        ''' <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
        Public PrintNameLength As UShort
        ''' <summary>
        ''' A buffer containing the unicode-encoded path string. The path string contains the substitute name
        ''' string and print name string. The substitute name and print name strings can appear in any order.
        ''' </summary>
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=8184)>
        Friend PathBuffer As String
        ' with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
        ' 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
    End Structure

    <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function DeviceIoControl(hDevice As SafeFileHandle, dwIoControlCode As UInteger,
                                            <[In]> ByRef lpInBuffer As ReparseDataBuffer, nInBufferSize As UInteger,
                                            lpOutBuffer As IntPtr, nOutBufferSize As UInteger,
                                            <Out> ByRef lpBytesReturned As UInteger, lpOverlapped As IntPtr) As Boolean
    End Function

    Public Shared Sub Create(junctionPath As String, targetDir As String, Optional overwrite As Boolean = False)
        Const IO_REPARSE_TAG_MOUNT_POINT As UInteger = &HA0000003UI
        Const FSCTL_SET_REPARSE_POINT As UInteger = &H900A4
        'This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
        Const NonInterpretedPathPrefix As String = "\??\"

        If Directory.Exists(junctionPath) Then
            If Not overwrite Then Throw New IOException("Directory already exists and overwrite parameter is false.")
        Else
            Directory.CreateDirectory(junctionPath)
        End If
        targetDir = NonInterpretedPathPrefix & Path.GetFullPath(targetDir)

        Using reparsePointHandle As SafeFileHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
                FileShare.Read Or FileShare.Write Or FileShare.Delete, IntPtr.Zero, FileMode.Open,
                Win32FileAttribute.FlagBackupSemantics Or Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero)

            If reparsePointHandle.IsInvalid OrElse Marshal.GetLastWin32Error() <> 0 Then
                Throw New IOException("Unable to open reparse point.", New Win32Exception())
            End If

            ' unicode string is 2 bytes per character, so *2 to get byte length
            Dim byteLength As UShort = CType(targetDir.Length * 2, UShort)
            Dim reparseDataBuffer As New ReparseDataBuffer With {
                .ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
                .ReparseDataLength = byteLength + 12US,
                .SubstituteNameOffset = 0,
                .SubstituteNameLength = byteLength,
                .PrintNameOffset = byteLength + 2US,
                .PrintNameLength = 0,
                .PathBuffer = targetDir
            }

            Dim result As Boolean = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, byteLength + 20US, IntPtr.Zero, 0, 0, IntPtr.Zero)
            If Not result Then Throw New IOException("Unable to create junction point.", New Win32Exception())
        End Using
    End Sub
End Class

I have simplified/updated the CreateJunction code by Jeff Brown on CodeProject, e.g. using automatic marshalling to pass the structure along to DeviceIoControl instead of having to manually manage the memory. I've only done this for creating a junction, as you can delete with Directory.Delete() and .Net's GetAttributes returns whether it has a reparse point.

I've also removed the target dir exists check, as I find it useful to be able to create a junction to a folder that doesn't exist or will exist at some later point. (Different drive e.t.c.)

One thing I wasn't able to figure out, was the sizes added to the string length for the structure members and nInBufferSize DeviceIoControl parameter, they don't seem to add up to a Marshal.SizeOf return value.

I did this in VB.Net, so I used IC#Code's CodeConverter extension to convert it to C#:

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

public class Junction
{
    [Flags]
    private enum Win32FileAccess : uint
    {
        GenericRead = 0x80000000U,
        GenericWrite = 0x40000000U,
        GenericExecute = 0x20000000U,
        GenericAll = 0x10000000U
    }

    [Flags]
    private enum Win32FileAttribute : uint
    {
        AttributeReadOnly = 0x1U,
        AttributeHidden = 0x2U,
        AttributeSystem = 0x4U,
        AttributeDirectory = 0x10U,
        AttributeArchive = 0x20U,
        AttributeDevice = 0x40U,
        AttributeNormal = 0x80U,
        AttributeTemporary = 0x100U,
        AttributeSparseFile = 0x200U,
        AttributeReparsePoint = 0x400U,
        AttributeCompressed = 0x800U,
        AttributeOffline = 0x1000U,
        AttributeNotContentIndexed = 0x2000U,
        AttributeEncrypted = 0x4000U,
        AttributeIntegrityStream = 0x8000U,
        AttributeVirtual = 0x10000U,
        AttributeNoScrubData = 0x20000U,
        AttributeEA = 0x40000U,
        AttributeRecallOnOpen = 0x40000U,
        AttributePinned = 0x80000U,
        AttributeUnpinned = 0x100000U,
        AttributeRecallOnDataAccess = 0x400000U,
        FlagOpenNoRecall = 0x100000U,
        /// <summary>
        /// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
        /// whether or not the filter that controls the reparse point is operational.
        /// <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
        /// <br />If the file is not a reparse point, then this flag is ignored.
        /// </summary>
        FlagOpenReparsePoint = 0x200000U,
        FlagSessionAware = 0x800000U,
        FlagPosixSemantics = 0x1000000U,
        /// <summary>
        /// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
        /// </summary>
        FlagBackupSemantics = 0x2000000U,
        FlagDeleteOnClose = 0x4000000U,
        FlagSequentialScan = 0x8000000U,
        FlagRandomAccess = 0x10000000U,
        FlagNoBuffering = 0x20000000U,
        FlagOverlapped = 0x40000000U,
        FlagWriteThrough = 0x80000000U
    }

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

    // Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct ReparseDataBuffer
    {
        /// <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
        public uint ReparseTag;
        /// <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
        public ushort ReparseDataLength;
        /// <summary>Reserved; do not use.</summary>
        private ushort Reserved;
        /// <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
        public ushort SubstituteNameOffset;
        /// <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
        public ushort SubstituteNameLength;
        /// <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
        public ushort PrintNameOffset;
        /// <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
        public ushort PrintNameLength;
        /// <summary>
        /// A buffer containing the unicode-encoded path string. The path string contains the substitute name
        /// string and print name string. The substitute name and print name strings can appear in any order.
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8184)]
        internal string PathBuffer;
        // with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
        // 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
    }

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode,
        [In] ReparseDataBuffer lpInBuffer, uint nInBufferSize,
        IntPtr lpOutBuffer, uint nOutBufferSize,
        [Out] uint lpBytesReturned, IntPtr lpOverlapped);

    public static void Create(string junctionPath, string targetDir, bool overwrite = false)
    {
        const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003U;
        const uint FSCTL_SET_REPARSE_POINT = 0x900A4U;
        // This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
        const string NonInterpretedPathPrefix = @"\??\";

        if (Directory.Exists(junctionPath))
        {
            if (!overwrite)
                throw new IOException("Directory already exists and overwrite parameter is false.");
        }
        else
        {
            Directory.CreateDirectory(junctionPath);
        }
        targetDir = NonInterpretedPathPrefix + Path.GetFullPath(targetDir);

        using (var reparsePointHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
            FileShare.Read | FileShare.Write | FileShare.Delete, IntPtr.Zero, FileMode.Open,
            Win32FileAttribute.FlagBackupSemantics | Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero))
        {
            if (reparsePointHandle.IsInvalid || Marshal.GetLastWin32Error() != 0)
            {
                throw new IOException("Unable to open reparse point.", new Win32Exception());
            }

            // unicode string is 2 bytes per character, so *2 to get byte length
            ushort byteLength = (ushort)(targetDir.Length * 2);
            var reparseDataBuffer = new ReparseDataBuffer()
            {
                ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
                ReparseDataLength = (ushort)(byteLength + 12u),
                SubstituteNameOffset = 0,
                SubstituteNameLength = byteLength,
                PrintNameOffset = (ushort)(byteLength + 2u),
                PrintNameLength = 0,
                PathBuffer = targetDir
            };

            bool result = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, (uint)(byteLength + 20), IntPtr.Zero, 0u, 0u, IntPtr.Zero);
            if (!result)
                throw new IOException("Unable to create junction point.", new Win32Exception());
        }
    }
}

And the source VB.Net:

Imports System
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles

Public Class Junction
    <Flags>
    Private Enum Win32FileAccess As UInteger
        GenericRead = &H80000000UI
        GenericWrite = &H40000000
        GenericExecute = &H20000000
        GenericAll = &H10000000
    End Enum

    <Flags>
    Private Enum Win32FileAttribute As UInteger
        AttributeReadOnly = &H1
        AttributeHidden = &H2
        AttributeSystem = &H4
        AttributeDirectory = &H10
        AttributeArchive = &H20
        AttributeDevice = &H40
        AttributeNormal = &H80
        AttributeTemporary = &H100
        AttributeSparseFile = &H200
        AttributeReparsePoint = &H400
        AttributeCompressed = &H800
        AttributeOffline = &H1000
        AttributeNotContentIndexed = &H2000
        AttributeEncrypted = &H4000
        AttributeIntegrityStream = &H8000
        AttributeVirtual = &H10000
        AttributeNoScrubData = &H20000
        AttributeEA = &H40000
        AttributeRecallOnOpen = &H40000
        AttributePinned = &H80000
        AttributeUnpinned = &H100000
        AttributeRecallOnDataAccess = &H400000
        FlagOpenNoRecall = &H100000
        ''' <summary>
        ''' Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned,
        ''' whether or not the filter that controls the reparse point is operational.
        ''' <br />This flag cannot be used with the <see cref="FileMode.Create"/> flag.
        ''' <br />If the file is not a reparse point, then this flag is ignored.
        ''' </summary>
        FlagOpenReparsePoint = &H200000
        FlagSessionAware = &H800000
        FlagPosixSemantics = &H1000000
        ''' <summary>
        ''' You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle.
        ''' </summary>
        FlagBackupSemantics = &H2000000
        FlagDeleteOnClose = &H4000000
        FlagSequentialScan = &H8000000
        FlagRandomAccess = &H10000000
        FlagNoBuffering = &H20000000
        FlagOverlapped = &H40000000
        FlagWriteThrough = &H80000000UI
    End Enum

    <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function CreateFile(lpFileName As String, dwDesiredAccess As Win32FileAccess,
                                       dwShareMode As FileShare, lpSecurityAttributes As IntPtr,
                                       dwCreationDisposition As FileMode, dwFlagsAndAttributes As Win32FileAttribute,
                                       hTemplateFile As IntPtr) As SafeFileHandle
    End Function

    ' Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union.
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
    Private Structure ReparseDataBuffer
        ''' <summary>Reparse point tag. Must be a Microsoft reparse point tag.</summary>
        Public ReparseTag As UInteger
        ''' <summary>Size, in bytes, of the reparse data in the buffer that <see cref="PathBuffer"/> points to.</summary>
        Public ReparseDataLength As UShort
        ''' <summary>Reserved; do not use.</summary>
        Private Reserved As UShort
        ''' <summary>Offset, in bytes, of the substitute name string in the <see cref="PathBuffer"/> array.</summary>
        Public SubstituteNameOffset As UShort
        ''' <summary>Length, in bytes, of the substitute name string. If this string is null-terminated, <see cref="SubstituteNameLength"/> does not include space for the null character.</summary>
        Public SubstituteNameLength As UShort
        ''' <summary>Offset, in bytes, of the print name string in the <see cref="PathBuffer"/> array.</summary>
        Public PrintNameOffset As UShort
        ''' <summary>Length, in bytes, of the print name string. If this string is null-terminated, <see cref="PrintNameLength"/> does not include space for the null character.</summary>
        Public PrintNameLength As UShort
        ''' <summary>
        ''' A buffer containing the unicode-encoded path string. The path string contains the substitute name
        ''' string and print name string. The substitute name and print name strings can appear in any order.
        ''' </summary>
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=8184)>
        Friend PathBuffer As String
        ' with <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16368)> Public PathBuffer As Byte()
        ' 16368 is the amount of bytes. since a unicode string uses 2 bytes per character, constrain to 16368/2 = 8184 characters.
    End Structure

    <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function DeviceIoControl(hDevice As SafeFileHandle, dwIoControlCode As UInteger,
                                            <[In]> ByRef lpInBuffer As ReparseDataBuffer, nInBufferSize As UInteger,
                                            lpOutBuffer As IntPtr, nOutBufferSize As UInteger,
                                            <Out> ByRef lpBytesReturned As UInteger, lpOverlapped As IntPtr) As Boolean
    End Function

    Public Shared Sub Create(junctionPath As String, targetDir As String, Optional overwrite As Boolean = False)
        Const IO_REPARSE_TAG_MOUNT_POINT As UInteger = &HA0000003UI
        Const FSCTL_SET_REPARSE_POINT As UInteger = &H900A4
        'This prefix indicates to NTFS that the path is to be treated as a non-interpreted path in the virtual file system.
        Const NonInterpretedPathPrefix As String = "\??\"

        If Directory.Exists(junctionPath) Then
            If Not overwrite Then Throw New IOException("Directory already exists and overwrite parameter is false.")
        Else
            Directory.CreateDirectory(junctionPath)
        End If
        targetDir = NonInterpretedPathPrefix & Path.GetFullPath(targetDir)

        Using reparsePointHandle As SafeFileHandle = CreateFile(junctionPath, Win32FileAccess.GenericWrite,
                FileShare.Read Or FileShare.Write Or FileShare.Delete, IntPtr.Zero, FileMode.Open,
                Win32FileAttribute.FlagBackupSemantics Or Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero)

            If reparsePointHandle.IsInvalid OrElse Marshal.GetLastWin32Error() <> 0 Then
                Throw New IOException("Unable to open reparse point.", New Win32Exception())
            End If

            ' unicode string is 2 bytes per character, so *2 to get byte length
            Dim byteLength As UShort = CType(targetDir.Length * 2, UShort)
            Dim reparseDataBuffer As New ReparseDataBuffer With {
                .ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
                .ReparseDataLength = byteLength + 12US,
                .SubstituteNameOffset = 0,
                .SubstituteNameLength = byteLength,
                .PrintNameOffset = byteLength + 2US,
                .PrintNameLength = 0,
                .PathBuffer = targetDir
            }

            Dim result As Boolean = DeviceIoControl(reparsePointHandle, FSCTL_SET_REPARSE_POINT, reparseDataBuffer, byteLength + 20US, IntPtr.Zero, 0, 0, IntPtr.Zero)
            If Not result Then Throw New IOException("Unable to create junction point.", New Win32Exception())
        End Using
    End Sub
End Class
迷你仙 2024-08-11 20:18:42

该代码来自 http://www.codeproject.com/KB/files/JunctionPointsNet。 aspx 作为那些无法访问此链接或备份的快捷方式,以防原始页面关闭。

不要投票赞成这个答案,因为我不是这个代码的作者。

感谢原作者jeff.brown@代码项目

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

namespace Monitor.Core.Utilities
{
    /// <summary>
    /// Provides access to NTFS junction points in .Net.
    /// </summary>
    public static class JunctionPoint
    {
        /// <summary>
        /// The file or directory is not a reparse point.
        /// </summary>
        private const int ERROR_NOT_A_REPARSE_POINT = 4390;

        /// <summary>
        /// The reparse point attribute cannot be set because it conflicts with an existing attribute.
        /// </summary>
        private const int ERROR_REPARSE_ATTRIBUTE_CONFLICT = 4391;

        /// <summary>
        /// The data present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_INVALID_REPARSE_DATA = 4392;

        /// <summary>
        /// The tag present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_REPARSE_TAG_INVALID = 4393;

        /// <summary>
        /// There is a mismatch between the tag specified in the request and the tag present in the reparse point.
        /// </summary>
        private const int ERROR_REPARSE_TAG_MISMATCH = 4394;

        /// <summary>
        /// Command to set the reparse point data block.
        /// </summary>
        private const int FSCTL_SET_REPARSE_POINT = 0x000900A4;

        /// <summary>
        /// Command to get the reparse point data block.
        /// </summary>
        private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;

        /// <summary>
        /// Command to delete the reparse point data base.
        /// </summary>
        private const int FSCTL_DELETE_REPARSE_POINT = 0x000900AC;

        /// <summary>
        /// Reparse point tag used to identify mount points and junction points.
        /// </summary>
        private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;

        /// <summary>
        /// This prefix indicates to NTFS that the path is to be treated as a non-interpreted
        /// path in the virtual file system.
        /// </summary>
        private const string NonInterpretedPathPrefix = @"\??\";

        [Flags]
        private enum EFileAccess : uint
        {
            GenericRead = 0x80000000,
            GenericWrite = 0x40000000,
            GenericExecute = 0x20000000,
            GenericAll = 0x10000000,
        }

        [Flags]
        private enum EFileShare : uint
        {
            None = 0x00000000,
            Read = 0x00000001,
            Write = 0x00000002,
            Delete = 0x00000004,
        }

        private enum ECreationDisposition : uint
        {
            New = 1,
            CreateAlways = 2,
            OpenExisting = 3,
            OpenAlways = 4,
            TruncateExisting = 5,
        }

        [Flags]
        private enum EFileAttributes : uint
        {
            Readonly = 0x00000001,
            Hidden = 0x00000002,
            System = 0x00000004,
            Directory = 0x00000010,
            Archive = 0x00000020,
            Device = 0x00000040,
            Normal = 0x00000080,
            Temporary = 0x00000100,
            SparseFile = 0x00000200,
            ReparsePoint = 0x00000400,
            Compressed = 0x00000800,
            Offline = 0x00001000,
            NotContentIndexed = 0x00002000,
            Encrypted = 0x00004000,
            Write_Through = 0x80000000,
            Overlapped = 0x40000000,
            NoBuffering = 0x20000000,
            RandomAccess = 0x10000000,
            SequentialScan = 0x08000000,
            DeleteOnClose = 0x04000000,
            BackupSemantics = 0x02000000,
            PosixSemantics = 0x01000000,
            OpenReparsePoint = 0x00200000,
            OpenNoRecall = 0x00100000,
            FirstPipeInstance = 0x00080000
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct REPARSE_DATA_BUFFER
        {
            /// <summary>
            /// Reparse point tag. Must be a Microsoft reparse point tag.
            /// </summary>
            public uint ReparseTag;

            /// <summary>
            /// Size, in bytes, of the data after the Reserved member. This can be calculated by:
            /// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength + 
            /// (namesAreNullTerminated ? 2 * sizeof(char) : 0);
            /// </summary>
            public ushort ReparseDataLength;

            /// <summary>
            /// Reserved; do not use. 
            /// </summary>
            public ushort Reserved;

            /// <summary>
            /// Offset, in bytes, of the substitute name string in the PathBuffer array.
            /// </summary>
            public ushort SubstituteNameOffset;

            /// <summary>
            /// Length, in bytes, of the substitute name string. If this string is null-terminated,
            /// SubstituteNameLength does not include space for the null character.
            /// </summary>
            public ushort SubstituteNameLength;

            /// <summary>
            /// Offset, in bytes, of the print name string in the PathBuffer array.
            /// </summary>
            public ushort PrintNameOffset;

            /// <summary>
            /// Length, in bytes, of the print name string. If this string is null-terminated,
            /// PrintNameLength does not include space for the null character. 
            /// </summary>
            public ushort PrintNameLength;

            /// <summary>
            /// A buffer containing the unicode-encoded path string. The path string contains
            /// the substitute name string and print name string.
            /// </summary>
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)]
            public byte[] PathBuffer;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
            IntPtr InBuffer, int nInBufferSize,
            IntPtr OutBuffer, int nOutBufferSize,
            out int pBytesReturned, IntPtr lpOverlapped);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFile(
            string lpFileName,
            EFileAccess dwDesiredAccess,
            EFileShare dwShareMode,
            IntPtr lpSecurityAttributes,
            ECreationDisposition dwCreationDisposition,
            EFileAttributes dwFlagsAndAttributes,
            IntPtr hTemplateFile);

        /// <summary>
        /// Creates a junction point from the specified directory to the specified target directory.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <param name="targetDir">The target directory</param>
        /// <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
        /// <exception cref="IOException">Thrown when the junction point could not be created or when
        /// an existing directory was found and <paramref name="overwrite" /> if false</exception>
        public static void Create(string junctionPoint, string targetDir, bool overwrite)
        {
            targetDir = Path.GetFullPath(targetDir);

            if (!Directory.Exists(targetDir))
                throw new IOException("Target path does not exist or is not a directory.");

            if (Directory.Exists(junctionPoint))
            {
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");
            }
            else
            {
                Directory.CreateDirectory(junctionPoint);
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                byte[] targetDirBytes = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix + Path.GetFullPath(targetDir));

                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = (ushort)(targetDirBytes.Length + 12);
                reparseDataBuffer.SubstituteNameOffset = 0;
                reparseDataBuffer.SubstituteNameLength = (ushort)targetDirBytes.Length;
                reparseDataBuffer.PrintNameOffset = (ushort)(targetDirBytes.Length + 2);
                reparseDataBuffer.PrintNameLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];
                Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length);

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);

                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT,
                        inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to create junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }
            }
        }

        /// <summary>
        /// Deletes a junction point at the specified source directory along with the directory itself.
        /// Does nothing if the junction point does not exist.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        public static void Delete(string junctionPoint)
        {
            if (!Directory.Exists(junctionPoint))
            {
                if (File.Exists(junctionPoint))
                    throw new IOException("Path is not a junction point.");

                return;
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);
                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT,
                        inBuffer, 8, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to delete junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }

                try
                {
                    Directory.Delete(junctionPoint);
                }
                catch (IOException ex)
                {
                    throw new IOException("Unable to delete junction point.", ex);
                }
            }
        }

        /// <summary>
        /// Determines whether the specified path exists and refers to a junction point.
        /// </summary>
        /// <param name="path">The junction point path</param>
        /// <returns>True if the specified path represents a junction point</returns>
        /// <exception cref="IOException">Thrown if the specified path is invalid
        /// or some other error occurs</exception>
        public static bool Exists(string path)
        {
            if (! Directory.Exists(path))
                return false;

            using (SafeFileHandle handle = OpenReparsePoint(path, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                return target != null;
            }
        }

        /// <summary>
        /// Gets the target of the specified junction point.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <returns>The target of the junction point</returns>
        /// <exception cref="IOException">Thrown when the specified path does not
        /// exist, is invalid, is not a junction point, or some other error occurs</exception>
        public static string GetTarget(string junctionPoint)
        {
            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                if (target == null)
                    throw new IOException("Path is not a junction point.");

                return target;
            }
        }

        private static string InternalGetTarget(SafeFileHandle handle)
        {
            int outBufferSize = Marshal.SizeOf(typeof(REPARSE_DATA_BUFFER));
            IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize);

            try
            {
                int bytesReturned;
                bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT,
                    IntPtr.Zero, 0, outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);

                if (!result)
                {
                    int error = Marshal.GetLastWin32Error();
                    if (error == ERROR_NOT_A_REPARSE_POINT)
                        return null;

                    ThrowLastWin32Error("Unable to get information about junction point.");
                }

                REPARSE_DATA_BUFFER reparseDataBuffer = (REPARSE_DATA_BUFFER)
                    Marshal.PtrToStructure(outBuffer, typeof(REPARSE_DATA_BUFFER));

                if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
                    return null;

                string targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
                    reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);

                if (targetDir.StartsWith(NonInterpretedPathPrefix))
                    targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length);

                return targetDir;
            }
            finally
            {
                Marshal.FreeHGlobal(outBuffer);
            }
        }

        private static SafeFileHandle OpenReparsePoint(string reparsePoint, EFileAccess accessMode)
        {
            SafeFileHandle reparsePointHandle = new SafeFileHandle(CreateFile(reparsePoint, accessMode,
                EFileShare.Read | EFileShare.Write | EFileShare.Delete,
                IntPtr.Zero, ECreationDisposition.OpenExisting,
                EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint, IntPtr.Zero), true);

            if (Marshal.GetLastWin32Error() != 0)
                ThrowLastWin32Error("Unable to open reparse point.");

            return reparsePointHandle;
        }

        private static void ThrowLastWin32Error(string message)
        {
            throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
        }
    }
}

The code is from http://www.codeproject.com/KB/files/JunctionPointsNet.aspx as a shortcut for those who can not reach this link or a backup in case the original page is down.

Do not vote up this answser, since I'm not author of this code.

Thanks to the original author jeff.brown@codeproject.

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

namespace Monitor.Core.Utilities
{
    /// <summary>
    /// Provides access to NTFS junction points in .Net.
    /// </summary>
    public static class JunctionPoint
    {
        /// <summary>
        /// The file or directory is not a reparse point.
        /// </summary>
        private const int ERROR_NOT_A_REPARSE_POINT = 4390;

        /// <summary>
        /// The reparse point attribute cannot be set because it conflicts with an existing attribute.
        /// </summary>
        private const int ERROR_REPARSE_ATTRIBUTE_CONFLICT = 4391;

        /// <summary>
        /// The data present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_INVALID_REPARSE_DATA = 4392;

        /// <summary>
        /// The tag present in the reparse point buffer is invalid.
        /// </summary>
        private const int ERROR_REPARSE_TAG_INVALID = 4393;

        /// <summary>
        /// There is a mismatch between the tag specified in the request and the tag present in the reparse point.
        /// </summary>
        private const int ERROR_REPARSE_TAG_MISMATCH = 4394;

        /// <summary>
        /// Command to set the reparse point data block.
        /// </summary>
        private const int FSCTL_SET_REPARSE_POINT = 0x000900A4;

        /// <summary>
        /// Command to get the reparse point data block.
        /// </summary>
        private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;

        /// <summary>
        /// Command to delete the reparse point data base.
        /// </summary>
        private const int FSCTL_DELETE_REPARSE_POINT = 0x000900AC;

        /// <summary>
        /// Reparse point tag used to identify mount points and junction points.
        /// </summary>
        private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;

        /// <summary>
        /// This prefix indicates to NTFS that the path is to be treated as a non-interpreted
        /// path in the virtual file system.
        /// </summary>
        private const string NonInterpretedPathPrefix = @"\??\";

        [Flags]
        private enum EFileAccess : uint
        {
            GenericRead = 0x80000000,
            GenericWrite = 0x40000000,
            GenericExecute = 0x20000000,
            GenericAll = 0x10000000,
        }

        [Flags]
        private enum EFileShare : uint
        {
            None = 0x00000000,
            Read = 0x00000001,
            Write = 0x00000002,
            Delete = 0x00000004,
        }

        private enum ECreationDisposition : uint
        {
            New = 1,
            CreateAlways = 2,
            OpenExisting = 3,
            OpenAlways = 4,
            TruncateExisting = 5,
        }

        [Flags]
        private enum EFileAttributes : uint
        {
            Readonly = 0x00000001,
            Hidden = 0x00000002,
            System = 0x00000004,
            Directory = 0x00000010,
            Archive = 0x00000020,
            Device = 0x00000040,
            Normal = 0x00000080,
            Temporary = 0x00000100,
            SparseFile = 0x00000200,
            ReparsePoint = 0x00000400,
            Compressed = 0x00000800,
            Offline = 0x00001000,
            NotContentIndexed = 0x00002000,
            Encrypted = 0x00004000,
            Write_Through = 0x80000000,
            Overlapped = 0x40000000,
            NoBuffering = 0x20000000,
            RandomAccess = 0x10000000,
            SequentialScan = 0x08000000,
            DeleteOnClose = 0x04000000,
            BackupSemantics = 0x02000000,
            PosixSemantics = 0x01000000,
            OpenReparsePoint = 0x00200000,
            OpenNoRecall = 0x00100000,
            FirstPipeInstance = 0x00080000
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct REPARSE_DATA_BUFFER
        {
            /// <summary>
            /// Reparse point tag. Must be a Microsoft reparse point tag.
            /// </summary>
            public uint ReparseTag;

            /// <summary>
            /// Size, in bytes, of the data after the Reserved member. This can be calculated by:
            /// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength + 
            /// (namesAreNullTerminated ? 2 * sizeof(char) : 0);
            /// </summary>
            public ushort ReparseDataLength;

            /// <summary>
            /// Reserved; do not use. 
            /// </summary>
            public ushort Reserved;

            /// <summary>
            /// Offset, in bytes, of the substitute name string in the PathBuffer array.
            /// </summary>
            public ushort SubstituteNameOffset;

            /// <summary>
            /// Length, in bytes, of the substitute name string. If this string is null-terminated,
            /// SubstituteNameLength does not include space for the null character.
            /// </summary>
            public ushort SubstituteNameLength;

            /// <summary>
            /// Offset, in bytes, of the print name string in the PathBuffer array.
            /// </summary>
            public ushort PrintNameOffset;

            /// <summary>
            /// Length, in bytes, of the print name string. If this string is null-terminated,
            /// PrintNameLength does not include space for the null character. 
            /// </summary>
            public ushort PrintNameLength;

            /// <summary>
            /// A buffer containing the unicode-encoded path string. The path string contains
            /// the substitute name string and print name string.
            /// </summary>
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)]
            public byte[] PathBuffer;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
            IntPtr InBuffer, int nInBufferSize,
            IntPtr OutBuffer, int nOutBufferSize,
            out int pBytesReturned, IntPtr lpOverlapped);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFile(
            string lpFileName,
            EFileAccess dwDesiredAccess,
            EFileShare dwShareMode,
            IntPtr lpSecurityAttributes,
            ECreationDisposition dwCreationDisposition,
            EFileAttributes dwFlagsAndAttributes,
            IntPtr hTemplateFile);

        /// <summary>
        /// Creates a junction point from the specified directory to the specified target directory.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <param name="targetDir">The target directory</param>
        /// <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
        /// <exception cref="IOException">Thrown when the junction point could not be created or when
        /// an existing directory was found and <paramref name="overwrite" /> if false</exception>
        public static void Create(string junctionPoint, string targetDir, bool overwrite)
        {
            targetDir = Path.GetFullPath(targetDir);

            if (!Directory.Exists(targetDir))
                throw new IOException("Target path does not exist or is not a directory.");

            if (Directory.Exists(junctionPoint))
            {
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");
            }
            else
            {
                Directory.CreateDirectory(junctionPoint);
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                byte[] targetDirBytes = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix + Path.GetFullPath(targetDir));

                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = (ushort)(targetDirBytes.Length + 12);
                reparseDataBuffer.SubstituteNameOffset = 0;
                reparseDataBuffer.SubstituteNameLength = (ushort)targetDirBytes.Length;
                reparseDataBuffer.PrintNameOffset = (ushort)(targetDirBytes.Length + 2);
                reparseDataBuffer.PrintNameLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];
                Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length);

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);

                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT,
                        inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to create junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }
            }
        }

        /// <summary>
        /// Deletes a junction point at the specified source directory along with the directory itself.
        /// Does nothing if the junction point does not exist.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        public static void Delete(string junctionPoint)
        {
            if (!Directory.Exists(junctionPoint))
            {
                if (File.Exists(junctionPoint))
                    throw new IOException("Path is not a junction point.");

                return;
            }

            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite))
            {
                REPARSE_DATA_BUFFER reparseDataBuffer = new REPARSE_DATA_BUFFER();

                reparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
                reparseDataBuffer.ReparseDataLength = 0;
                reparseDataBuffer.PathBuffer = new byte[0x3ff0];

                int inBufferSize = Marshal.SizeOf(reparseDataBuffer);
                IntPtr inBuffer = Marshal.AllocHGlobal(inBufferSize);
                try
                {
                    Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);

                    int bytesReturned;
                    bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT,
                        inBuffer, 8, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);

                    if (!result)
                        ThrowLastWin32Error("Unable to delete junction point.");
                }
                finally
                {
                    Marshal.FreeHGlobal(inBuffer);
                }

                try
                {
                    Directory.Delete(junctionPoint);
                }
                catch (IOException ex)
                {
                    throw new IOException("Unable to delete junction point.", ex);
                }
            }
        }

        /// <summary>
        /// Determines whether the specified path exists and refers to a junction point.
        /// </summary>
        /// <param name="path">The junction point path</param>
        /// <returns>True if the specified path represents a junction point</returns>
        /// <exception cref="IOException">Thrown if the specified path is invalid
        /// or some other error occurs</exception>
        public static bool Exists(string path)
        {
            if (! Directory.Exists(path))
                return false;

            using (SafeFileHandle handle = OpenReparsePoint(path, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                return target != null;
            }
        }

        /// <summary>
        /// Gets the target of the specified junction point.
        /// </summary>
        /// <remarks>
        /// Only works on NTFS.
        /// </remarks>
        /// <param name="junctionPoint">The junction point path</param>
        /// <returns>The target of the junction point</returns>
        /// <exception cref="IOException">Thrown when the specified path does not
        /// exist, is invalid, is not a junction point, or some other error occurs</exception>
        public static string GetTarget(string junctionPoint)
        {
            using (SafeFileHandle handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead))
            {
                string target = InternalGetTarget(handle);
                if (target == null)
                    throw new IOException("Path is not a junction point.");

                return target;
            }
        }

        private static string InternalGetTarget(SafeFileHandle handle)
        {
            int outBufferSize = Marshal.SizeOf(typeof(REPARSE_DATA_BUFFER));
            IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize);

            try
            {
                int bytesReturned;
                bool result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT,
                    IntPtr.Zero, 0, outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);

                if (!result)
                {
                    int error = Marshal.GetLastWin32Error();
                    if (error == ERROR_NOT_A_REPARSE_POINT)
                        return null;

                    ThrowLastWin32Error("Unable to get information about junction point.");
                }

                REPARSE_DATA_BUFFER reparseDataBuffer = (REPARSE_DATA_BUFFER)
                    Marshal.PtrToStructure(outBuffer, typeof(REPARSE_DATA_BUFFER));

                if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
                    return null;

                string targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
                    reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);

                if (targetDir.StartsWith(NonInterpretedPathPrefix))
                    targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length);

                return targetDir;
            }
            finally
            {
                Marshal.FreeHGlobal(outBuffer);
            }
        }

        private static SafeFileHandle OpenReparsePoint(string reparsePoint, EFileAccess accessMode)
        {
            SafeFileHandle reparsePointHandle = new SafeFileHandle(CreateFile(reparsePoint, accessMode,
                EFileShare.Read | EFileShare.Write | EFileShare.Delete,
                IntPtr.Zero, ECreationDisposition.OpenExisting,
                EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint, IntPtr.Zero), true);

            if (Marshal.GetLastWin32Error() != 0)
                ThrowLastWin32Error("Unable to open reparse point.");

            return reparsePointHandle;
        }

        private static void ThrowLastWin32Error(string message)
        {
            throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
        }
    }
}
徒留西风 2024-08-11 20:18:42

对于使用本文的任何人 https://www.codeproject .com/Articles/15633/Manipulated-NTFS-Junction-Points-in-NET
正如 @Walkman 和 @SqlRyan 答案中提到的,我在 Jeff Brown 的文章中添加了一个修复,用于在尝试创建自身的连接点时。

例如:
目录结构是:
应用程序文件夹
当前(连接点指向appFolder)
当尝试创建一个指向“当前”的连接点“当前”(带有覆盖标志)时,连接点会变得混乱,

解决方法如下:

if (Directory.Exists(junctionPoint))
{
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");

                var junctionPointFullPath = Path.GetFullPath(junctionPoint);
                var targetDirFullPath = Path.GetFullPath(targetDir);
                if (junctionPointFullPath.Equals(targetDirFullPath))
                    throw new RecursiveJunctionPointException("Junction point path and target dir can't be identical");
}

For whoever uses this article https://www.codeproject.com/Articles/15633/Manipulating-NTFS-Junction-Points-in-NET
As mentioned in @Walkman and @SqlRyan answers, I added a fix to Jeff Brown's article for when trying to create a junction point to itself.

for example:
dir structure is:
appFolder
current (junction point points to appFolder)
When trying to create a junction point 'current' that points to 'current' (with the overwrite flag), the junction point gets messed up

Here's the fix:

if (Directory.Exists(junctionPoint))
{
                if (!overwrite)
                    throw new IOException("Directory already exists and overwrite parameter is false.");

                var junctionPointFullPath = Path.GetFullPath(junctionPoint);
                var targetDirFullPath = Path.GetFullPath(targetDir);
                if (junctionPointFullPath.Equals(targetDirFullPath))
                    throw new RecursiveJunctionPointException("Junction point path and target dir can't be identical");
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文