yotiky Tech Blog

とあるエンジニアの備忘録

C# - シンボリックリンクを操作する(.NET 5以前)

TL;DR

.NET 6 でDirectoryクラスにCreateSymbolikLinkメソッドが追加されたが、5以前では使えないため Win32API を使って操作する。 注意する点は実行に管理者権限が必要なため、Unity などから利用するのは難しいかもしれない。

.NET 6 以降は以下を参照。

yotiky.hatenablog.com

目次

検証環境

シンボリックリンクの操作

作成する

CreateSymbolicLinkを使用する。

まずは定義部分。

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags);

enum SymbolicLink
{
    File = 0,
    Directory = 1
}

続いて使い方。 作成に成功すればtrue、失敗すればfalseが返ってくる。

void Main()
{
    var src = @"C:\Workspace\SymLinkWork\folder1";
    var dest = @"C:\Workspace\SymLinkWork\folder2";

    var result = CreateSymbolicLink(dest, src, SymbolicLink.Directory);
    result.Dump();
}

注意が必要なのはコードの実行には管理者権限が必要なことである。LINQPadで実行する場合もLINQPad自体を管理者権限で実行しないと失敗する。 現在管理者権限で実行されてるかどうかは、以下のようにして取れる。

public static bool IsRunningAsAdmin()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        WindowsPrincipal principal = new WindowsPrincipal(identity);

        return principal.IsInRole(WindowsBuiltInRole.Administrator);
    }
    //for mac and linux
    return true;
}

リンク先を取得する

GetFinalPathNameByHandleを使用する。

まずは定義部分。

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetFinalPathNameByHandle([In] SafeFileHandle hFile, [Out] StringBuilder lpszFilePath, [In] int cchFilePath, [In] int dwFlags);

private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

public static string ResolveLinkTarget(string path)
{
    if (!Directory.Exists(path) && !File.Exists(path))
    {
        throw new IOException("Path not found");
    }

    SafeFileHandle directoryHandle = CreateFile(path, 0, 2, IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); //Handle file / folder

    if (directoryHandle.IsInvalid)
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    StringBuilder result = new StringBuilder(512);
    int mResult = GetFinalPathNameByHandle(directoryHandle, result, result.Capacity, 0);

    if (mResult < 0)
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    if (result.Length >= 4 && result[0] == '\\' && result[1] == '\\' && result[2] == '?' && result[3] == '\\')
    {
        return result.ToString().Substring(4); // "\\?\" remove
    }
    return result.ToString();
}

続いて使い方。

    var src =   @"C:\Workspace\SymLinkWork\folder1";
    var dest2 = @"C:\Workspace\SymLinkWork\folder2";
    var dest3 = @"C:\Workspace\SymLinkWork\folder3";

    CreateSymbolicLink(dest2, src, SymbolicLink.Directory);
    CreateSymbolicLink(dest3, dest2, SymbolicLink.Directory);

    ResolveLinkTarget(dest2).Dump();
    ResolveLinkTarget(dest3).Dump();

実行結果。
FinalPathの名前の通り、リンク先がシンボリックリンクだった場合は最後のリンク先が返ってくる。つまり今回はどちらもfolder1になる。

C:\Workspace\SymLinkWork\folder1
C:\Workspace\SymLinkWork\folder1

削除する

ディレクトリを消すだけ。

    Directory.Delete(dest);

指定したディレクトリ配下のシンボリックリンクを確認にする

    var dirs = Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories);
    dirs.Select(path =>
        {
            var link = ResolveLinkTarget(path);
            return new { Path = path, SymLink = path != link, Link = link };
        })
        .Dump("LinkList");

検証に使用したコード全文(LINQPad)

void Main()
{

    var root = @"C:\Workspace\SymLinkWork";
    var src = @"C:\Workspace\SymLinkWork\folder1";
    var dest2 = @"C:\Workspace\SymLinkWork\folder2";
    var dest3 = @"C:\Workspace\SymLinkWork\folder3";

    IsRunningAsAdmin().Dump("Admin");
    CreateSymbolicLink(dest2, src, SymbolicLink.Directory).Dump("Create");
    CreateSymbolicLink(dest3, dest2, SymbolicLink.Directory).Dump("Create");

    ResolveLinkTarget(dest2);
    ResolveLinkTarget(dest3);

    GetSymLinkList(root);

    DeleteSymlink(dest2);
    DeleteSymlink(dest3);
    
    GetSymLinkList(root);
}

void DeleteSymlink(string path)
{
    Directory.Delete(path);
}

void GetSymLinkList(string root)
{
    var dirs = Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories);
    dirs.Select(path =>
        {
            var link = ResolveLinkTarget(path);
            return new { Path = path, SymLink = path != link, Link = link };
        })
        .Dump("LinkList");
}


// Create
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags);

enum SymbolicLink
{
    File = 0,
    Directory = 1
}

// ResolveLinkTarget
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetFinalPathNameByHandle([In] SafeFileHandle hFile, [Out] StringBuilder lpszFilePath, [In] int cchFilePath, [In] int dwFlags);

private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

public static string ResolveLinkTarget(string path)
{
    if (!Directory.Exists(path) && !File.Exists(path))
    {
        throw new IOException("Path not found");
    }

    SafeFileHandle directoryHandle = CreateFile(path, 0, 2, IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); //Handle file / folder

    if (directoryHandle.IsInvalid)
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    StringBuilder result = new StringBuilder(512);
    int mResult = GetFinalPathNameByHandle(directoryHandle, result, result.Capacity, 0);

    if (mResult < 0)
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    if (result.Length >= 4 && result[0] == '\\' && result[1] == '\\' && result[2] == '?' && result[3] == '\\')
    {
        return result.ToString().Substring(4); // "\\?\" remove
    }
    return result.ToString();
}

// Check Admin
public static bool IsRunningAsAdmin()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        WindowsPrincipal principal = new WindowsPrincipal(identity);

        return principal.IsInRole(WindowsBuiltInRole.Administrator);
    }
    //for mac and linux
    return true;

}