mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-26 12:21:26 -07:00
added powershell symlink util helper (#27153)
* Added symbolic link util for powershell * updated module_util license to BSD
This commit is contained in:
parent
e16e6313c7
commit
1bc4940ee1
3 changed files with 682 additions and 0 deletions
|
@ -0,0 +1,506 @@
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||||
|
|
||||||
|
Function Load-LinkUtils() {
|
||||||
|
Add-Type -TypeDefinition @'
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ansible
|
||||||
|
{
|
||||||
|
public enum LinkType
|
||||||
|
{
|
||||||
|
SymbolicLink,
|
||||||
|
JunctionPoint,
|
||||||
|
HardLink
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LinkUtilWin32Exception : System.ComponentModel.Win32Exception
|
||||||
|
{
|
||||||
|
private string _msg;
|
||||||
|
|
||||||
|
public LinkUtilWin32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||||
|
|
||||||
|
public LinkUtilWin32Exception(int errorCode, string message) : base(errorCode)
|
||||||
|
{
|
||||||
|
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Message { get { return _msg; } }
|
||||||
|
public static explicit operator LinkUtilWin32Exception(string message) { return new LinkUtilWin32Exception(message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LinkInfo
|
||||||
|
{
|
||||||
|
public LinkType Type { get; internal set; }
|
||||||
|
public string PrintName { get; internal set; }
|
||||||
|
public string SubstituteName { get; internal set; }
|
||||||
|
public string AbsolutePath { get; internal set; }
|
||||||
|
public string TargetPath { get; internal set; }
|
||||||
|
public string[] HardTargets { get; internal set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct LUID
|
||||||
|
{
|
||||||
|
public UInt32 LowPart;
|
||||||
|
public Int32 HighPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct TOKEN_PRIVILEGES
|
||||||
|
{
|
||||||
|
public UInt32 PrivilegeCount;
|
||||||
|
public LUID Luid;
|
||||||
|
public UInt32 Attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public struct REPARSE_DATA_BUFFER
|
||||||
|
{
|
||||||
|
public UInt32 ReparseTag;
|
||||||
|
public UInt16 ReparseDataLength;
|
||||||
|
public UInt16 Reserved;
|
||||||
|
public UInt16 SubstituteNameOffset;
|
||||||
|
public UInt16 SubstituteNameLength;
|
||||||
|
public UInt16 PrintNameOffset;
|
||||||
|
public UInt16 PrintNameLength;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = LinkUtil.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)]
|
||||||
|
public char[] PathBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LinkUtil
|
||||||
|
{
|
||||||
|
public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 1024 * 16;
|
||||||
|
|
||||||
|
private const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
|
||||||
|
private const int TOKEN_QUERY = 0x00000008;
|
||||||
|
private const int SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||||
|
|
||||||
|
private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
||||||
|
private const UInt32 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
||||||
|
|
||||||
|
private const UInt32 FSCTL_GET_REPARSE_POINT = 0x000900A8;
|
||||||
|
private const UInt32 FSCTL_SET_REPARSE_POINT = 0x000900A4;
|
||||||
|
private const UInt32 FILE_DEVICE_FILE_SYSTEM = 0x00090000;
|
||||||
|
|
||||||
|
private const UInt32 IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
|
||||||
|
private const UInt32 IO_REPARSE_TAG_SYMLINK = 0xA000000C;
|
||||||
|
|
||||||
|
private const UInt32 SYMLINK_FLAG_RELATIVE = 0x00000001;
|
||||||
|
|
||||||
|
private const Int64 INVALID_HANDLE_VALUE = -1;
|
||||||
|
|
||||||
|
private const UInt32 SIZE_OF_WCHAR = 2;
|
||||||
|
|
||||||
|
private const UInt32 SYMBOLIC_LINK_FLAG_FILE = 0x00000000;
|
||||||
|
private const UInt32 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x00000001;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern IntPtr GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern bool CloseHandle(
|
||||||
|
IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll")]
|
||||||
|
private static extern bool OpenProcessToken(
|
||||||
|
IntPtr ProcessHandle,
|
||||||
|
UInt32 DesiredAccess,
|
||||||
|
out IntPtr TokenHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool LookupPrivilegeValue(
|
||||||
|
string lpSystemName,
|
||||||
|
string lpName,
|
||||||
|
[MarshalAs(UnmanagedType.Struct)] out LUID lpLuid);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll")]
|
||||||
|
private static extern bool AdjustTokenPrivileges(
|
||||||
|
IntPtr TokenHandle,
|
||||||
|
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
|
||||||
|
ref TOKEN_PRIVILEGES NewState,
|
||||||
|
UInt32 BufferLength,
|
||||||
|
IntPtr PreviousState,
|
||||||
|
IntPtr ReturnLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", 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,
|
||||||
|
UInt32 dwFlagsAndAttributes,
|
||||||
|
IntPtr hTemplateFile);
|
||||||
|
|
||||||
|
// Used by GetReparsePointInfo()
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool DeviceIoControl(
|
||||||
|
SafeFileHandle hDevice,
|
||||||
|
UInt32 dwIoControlCode,
|
||||||
|
IntPtr lpInBuffer,
|
||||||
|
UInt32 nInBufferSize,
|
||||||
|
out REPARSE_DATA_BUFFER lpOutBuffer,
|
||||||
|
UInt32 nOutBufferSize,
|
||||||
|
out UInt32 lpBytesReturned,
|
||||||
|
IntPtr lpOverlapped);
|
||||||
|
|
||||||
|
// Used by CreateJunctionPoint()
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool DeviceIoControl(
|
||||||
|
SafeFileHandle hDevice,
|
||||||
|
UInt32 dwIoControlCode,
|
||||||
|
REPARSE_DATA_BUFFER lpInBuffer,
|
||||||
|
UInt32 nInBufferSize,
|
||||||
|
IntPtr lpOutBuffer,
|
||||||
|
UInt32 nOutBufferSize,
|
||||||
|
out UInt32 lpBytesReturned,
|
||||||
|
IntPtr lpOverlapped);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool GetVolumePathName(
|
||||||
|
string lpszFileName,
|
||||||
|
StringBuilder lpszVolumePathName,
|
||||||
|
ref UInt32 cchBufferLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern IntPtr FindFirstFileNameW(
|
||||||
|
string lpFileName,
|
||||||
|
UInt32 dwFlags,
|
||||||
|
ref UInt32 StringLength,
|
||||||
|
StringBuilder LinkName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool FindNextFileNameW(
|
||||||
|
IntPtr hFindStream,
|
||||||
|
ref UInt32 StringLength,
|
||||||
|
StringBuilder LinkName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool FindClose(
|
||||||
|
IntPtr hFindFile);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool RemoveDirectory(
|
||||||
|
string lpPathName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool DeleteFile(
|
||||||
|
string lpFileName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool CreateSymbolicLink(
|
||||||
|
string lpSymlinkFileName,
|
||||||
|
string lpTargetFileName,
|
||||||
|
UInt32 dwFlags);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
private static extern bool CreateHardLink(
|
||||||
|
string lpFileName,
|
||||||
|
string lpExistingFileName,
|
||||||
|
IntPtr lpSecurityAttributes);
|
||||||
|
|
||||||
|
public static void EnablePrivilege(string privilege)
|
||||||
|
{
|
||||||
|
TOKEN_PRIVILEGES tkpPrivileges;
|
||||||
|
|
||||||
|
IntPtr hToken;
|
||||||
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken))
|
||||||
|
throw new LinkUtilWin32Exception("OpenProcessToken failed");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LUID luid;
|
||||||
|
if (!LookupPrivilegeValue(null, privilege, out luid))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("LookupPrivilegeValue({0}) failed", privilege));
|
||||||
|
|
||||||
|
tkpPrivileges.PrivilegeCount = 1;
|
||||||
|
tkpPrivileges.Luid = luid;
|
||||||
|
tkpPrivileges.Attributes = SE_PRIVILEGE_ENABLED;
|
||||||
|
|
||||||
|
if (!AdjustTokenPrivileges(hToken, false, ref tkpPrivileges, 0, IntPtr.Zero, IntPtr.Zero))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("AdjustTokenPrivileges({0}) failed", privilege));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(hToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkInfo GetLinkInfo(string linkPath)
|
||||||
|
{
|
||||||
|
FileAttributes attr = File.GetAttributes(linkPath);
|
||||||
|
if (attr.HasFlag(FileAttributes.ReparsePoint))
|
||||||
|
return GetReparsePointInfo(linkPath);
|
||||||
|
|
||||||
|
if (!attr.HasFlag(FileAttributes.Directory))
|
||||||
|
return GetHardLinkInfo(linkPath);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteLink(string linkPath)
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
FileAttributes attr = File.GetAttributes(linkPath);
|
||||||
|
if (attr.HasFlag(FileAttributes.Directory))
|
||||||
|
{
|
||||||
|
success = RemoveDirectory(linkPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
success = DeleteFile(linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("Failed to delete link at {0}", linkPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateLink(string linkPath, String linkTarget, LinkType linkType)
|
||||||
|
{
|
||||||
|
switch (linkType)
|
||||||
|
{
|
||||||
|
case LinkType.SymbolicLink:
|
||||||
|
UInt32 linkFlags;
|
||||||
|
FileAttributes attr = File.GetAttributes(linkTarget);
|
||||||
|
if (attr.HasFlag(FileAttributes.Directory))
|
||||||
|
linkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY;
|
||||||
|
else
|
||||||
|
linkFlags = SYMBOLIC_LINK_FLAG_FILE;
|
||||||
|
|
||||||
|
if (!CreateSymbolicLink(linkPath, linkTarget, linkFlags))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("CreateSymbolicLink({0}, {1}, {2}) failed", linkPath, linkTarget, linkFlags));
|
||||||
|
break;
|
||||||
|
case LinkType.JunctionPoint:
|
||||||
|
CreateJunctionPoint(linkPath, linkTarget);
|
||||||
|
break;
|
||||||
|
case LinkType.HardLink:
|
||||||
|
if (!CreateHardLink(linkPath, linkTarget, IntPtr.Zero))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("CreateHardLink({0}, {1}) failed", linkPath, linkTarget));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkInfo GetHardLinkInfo(string linkPath)
|
||||||
|
{
|
||||||
|
UInt32 maxPath = 260;
|
||||||
|
List<string> result = new List<string>();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder((int)maxPath);
|
||||||
|
UInt32 stringLength = maxPath;
|
||||||
|
if (!GetVolumePathName(linkPath, sb, ref stringLength))
|
||||||
|
throw new LinkUtilWin32Exception("GetVolumePathName() failed");
|
||||||
|
string volume = sb.ToString();
|
||||||
|
|
||||||
|
stringLength = maxPath;
|
||||||
|
IntPtr findHandle = FindFirstFileNameW(linkPath, 0, ref stringLength, sb);
|
||||||
|
if (findHandle.ToInt64() != INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
string hardLinkPath = sb.ToString();
|
||||||
|
if (hardLinkPath.StartsWith("\\"))
|
||||||
|
hardLinkPath = hardLinkPath.Substring(1, hardLinkPath.Length - 1);
|
||||||
|
|
||||||
|
result.Add(Path.Combine(volume, hardLinkPath));
|
||||||
|
stringLength = maxPath;
|
||||||
|
|
||||||
|
} while (FindNextFileNameW(findHandle, ref stringLength, sb));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
FindClose(findHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count > 1)
|
||||||
|
return new LinkInfo
|
||||||
|
{
|
||||||
|
Type = LinkType.HardLink,
|
||||||
|
HardTargets = result.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkInfo GetReparsePointInfo(string linkPath)
|
||||||
|
{
|
||||||
|
SafeFileHandle fileHandle = CreateFile(
|
||||||
|
linkPath,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.None,
|
||||||
|
IntPtr.Zero,
|
||||||
|
FileMode.Open,
|
||||||
|
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
|
||||||
|
IntPtr.Zero);
|
||||||
|
|
||||||
|
if (fileHandle.IsInvalid)
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
|
||||||
|
|
||||||
|
REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
|
||||||
|
UInt32 bytesReturned;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!DeviceIoControl(
|
||||||
|
fileHandle,
|
||||||
|
FSCTL_GET_REPARSE_POINT,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
out buffer,
|
||||||
|
MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
|
||||||
|
out bytesReturned,
|
||||||
|
IntPtr.Zero))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed for file at {0}", linkPath));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileHandle.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isRelative = false;
|
||||||
|
int pathOffset = 0;
|
||||||
|
LinkType linkType;
|
||||||
|
if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
|
||||||
|
{
|
||||||
|
UInt32 bufferFlags = Convert.ToUInt32(buffer.PathBuffer[0]) + Convert.ToUInt32(buffer.PathBuffer[1]);
|
||||||
|
if (bufferFlags == SYMLINK_FLAG_RELATIVE)
|
||||||
|
isRelative = true;
|
||||||
|
pathOffset = 2;
|
||||||
|
linkType = LinkType.SymbolicLink;
|
||||||
|
}
|
||||||
|
else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
|
||||||
|
{
|
||||||
|
linkType = LinkType.JunctionPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string errorMessage = String.Format("Invalid Reparse Tag: {0}", buffer.ReparseTag.ToString());
|
||||||
|
throw new Exception(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
string printName = new string(buffer.PathBuffer, (int)(buffer.PrintNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.PrintNameLength / SIZE_OF_WCHAR));
|
||||||
|
string substituteName = new string(buffer.PathBuffer, (int)(buffer.SubstituteNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.SubstituteNameLength / SIZE_OF_WCHAR));
|
||||||
|
|
||||||
|
// TODO: should we check for \?\UNC\server for convert it to the NT style \\server path
|
||||||
|
// Remove the leading Windows object directory \?\ from the path if present
|
||||||
|
string targetPath = substituteName;
|
||||||
|
if (targetPath.StartsWith("\\??\\"))
|
||||||
|
targetPath = targetPath.Substring(4, targetPath.Length - 4);
|
||||||
|
|
||||||
|
string absolutePath = targetPath;
|
||||||
|
if (isRelative)
|
||||||
|
absolutePath = Path.GetFullPath(Path.Combine(new FileInfo(linkPath).Directory.FullName, targetPath));
|
||||||
|
|
||||||
|
return new LinkInfo
|
||||||
|
{
|
||||||
|
Type = linkType,
|
||||||
|
PrintName = printName,
|
||||||
|
SubstituteName = substituteName,
|
||||||
|
AbsolutePath = absolutePath,
|
||||||
|
TargetPath = targetPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateJunctionPoint(string linkPath, string linkTarget)
|
||||||
|
{
|
||||||
|
// We need to create the link as a dir beforehand
|
||||||
|
Directory.CreateDirectory(linkPath);
|
||||||
|
SafeFileHandle fileHandle = CreateFile(
|
||||||
|
linkPath,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.Read | FileShare.Write | FileShare.None,
|
||||||
|
IntPtr.Zero,
|
||||||
|
FileMode.Open,
|
||||||
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
||||||
|
IntPtr.Zero);
|
||||||
|
|
||||||
|
if (fileHandle.IsInvalid)
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string substituteName = "\\??\\" + Path.GetFullPath(linkTarget);
|
||||||
|
string printName = linkTarget;
|
||||||
|
|
||||||
|
REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
|
||||||
|
buffer.SubstituteNameOffset = 0;
|
||||||
|
buffer.SubstituteNameLength = (UInt16)(substituteName.Length * SIZE_OF_WCHAR);
|
||||||
|
buffer.PrintNameOffset = (UInt16)(buffer.SubstituteNameLength + 2);
|
||||||
|
buffer.PrintNameLength = (UInt16)(printName.Length * SIZE_OF_WCHAR);
|
||||||
|
|
||||||
|
buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
|
||||||
|
buffer.ReparseDataLength = (UInt16)(buffer.SubstituteNameLength + buffer.PrintNameLength + 12);
|
||||||
|
buffer.PathBuffer = new char[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
|
||||||
|
|
||||||
|
byte[] unicodeBytes = Encoding.Unicode.GetBytes(substituteName + "\0" + printName);
|
||||||
|
char[] pathBuffer = Encoding.Unicode.GetChars(unicodeBytes);
|
||||||
|
Array.Copy(pathBuffer, buffer.PathBuffer, pathBuffer.Length);
|
||||||
|
|
||||||
|
UInt32 bytesReturned;
|
||||||
|
if (!DeviceIoControl(
|
||||||
|
fileHandle,
|
||||||
|
FSCTL_SET_REPARSE_POINT,
|
||||||
|
buffer,
|
||||||
|
(UInt32)(buffer.ReparseDataLength + 8),
|
||||||
|
IntPtr.Zero, 0,
|
||||||
|
out bytesReturned,
|
||||||
|
IntPtr.Zero))
|
||||||
|
throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed to create junction point at {0} to {1}", linkPath, linkTarget));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileHandle.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
|
||||||
|
[Ansible.LinkUtil]::EnablePrivilege("SeBackupPrivilege")
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-Link($link_path) {
|
||||||
|
$link_info = [Ansible.LinkUtil]::GetLinkInfo($link_path)
|
||||||
|
return $link_info
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Remove-Link($link_path) {
|
||||||
|
[Ansible.LinkUtil]::DeleteLink($link_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function New-Link($link_path, $link_target, $link_type) {
|
||||||
|
if (-not (Test-Path -Path $link_target)) {
|
||||||
|
throw "link_target '$link_target' does not exist, cannot create link"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch($link_type) {
|
||||||
|
"link" {
|
||||||
|
$type = [Ansible.LinkType]::SymbolicLink
|
||||||
|
}
|
||||||
|
"junction" {
|
||||||
|
if (Test-Path -Path $link_target -PathType Leaf) {
|
||||||
|
throw "cannot set the target for a junction point to a file"
|
||||||
|
}
|
||||||
|
$type = [Ansible.LinkType]::JunctionPoint
|
||||||
|
}
|
||||||
|
"hard" {
|
||||||
|
if (Test-Path -Path $link_target -PathType Container) {
|
||||||
|
throw "cannot set the target for a hard link to a directory"
|
||||||
|
}
|
||||||
|
$type = [Ansible.LinkType]::HardLink
|
||||||
|
}
|
||||||
|
default { throw "invalid link_type option $($link_type): expecting link, junction, hard" }
|
||||||
|
}
|
||||||
|
[Ansible.LinkUtil]::CreateLink($link_path, $link_target, $type)
|
||||||
|
}
|
||||||
|
|
||||||
|
# this line must stay at the bottom to ensure all defined module parts are exported
|
||||||
|
Export-ModuleMember -Alias * -Function * -Cmdlet *
|
|
@ -0,0 +1,167 @@
|
||||||
|
#!powershell
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
#Requires -Module Ansible.ModuleUtils.LinkUtil
|
||||||
|
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$params = Parse-Args $args;
|
||||||
|
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true
|
||||||
|
|
||||||
|
$folder_target = "$path\folder"
|
||||||
|
$file_target = "$path\file"
|
||||||
|
$symlink_file_path = "$path\file-symlink"
|
||||||
|
$symlink_folder_path = "$path\folder-symlink"
|
||||||
|
$hardlink_path = "$path\hardlink"
|
||||||
|
$hardlink_path_2 = "$path\hardlink2"
|
||||||
|
$junction_point_path = "$path\junction"
|
||||||
|
|
||||||
|
if (Test-Path -Path $path) {
|
||||||
|
Remove-Item -Path $path -Force -Recurse | Out-Null
|
||||||
|
}
|
||||||
|
New-Item -Path $path -ItemType Directory | Out-Null
|
||||||
|
New-Item -Path $folder_target -ItemType Directory | Out-Null
|
||||||
|
New-Item -Path $file_target -ItemType File | Out-Null
|
||||||
|
Set-Content -Path $file_target -Value "a"
|
||||||
|
|
||||||
|
Function Assert-Equals($actual, $expected) {
|
||||||
|
if ($actual -ne $expected) {
|
||||||
|
Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Assert-True($expression, $message) {
|
||||||
|
if ($expression -ne $true) {
|
||||||
|
Fail-Json @{} $message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# need to manually set this
|
||||||
|
Load-LinkUtils
|
||||||
|
|
||||||
|
# path is not a link
|
||||||
|
$no_link_result = Get-Link -link_path $path
|
||||||
|
Assert-True -expression ($no_link_result -eq $null) -message "did not return null result for a non link"
|
||||||
|
|
||||||
|
# fail to create hard link pointed to a directory
|
||||||
|
try {
|
||||||
|
New-Link -link_path "$path\folder-hard" -link_target $folder_target -link_type "hard"
|
||||||
|
Assert-True -expression $false -message "creation of hard link should have failed if target was a directory"
|
||||||
|
} catch {
|
||||||
|
Assert-Equals -actual $_.Exception.Message -expected "cannot set the target for a hard link to a directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fail to create a junction point pointed to a file
|
||||||
|
try {
|
||||||
|
New-Link -link_path "$path\junction-fail" -link_target $file_target -link_type "junction"
|
||||||
|
Assert-True -expression $false -message "creation of junction point should have failed if target was a file"
|
||||||
|
} catch {
|
||||||
|
Assert-Equals -actual $_.Exception.Message -expected "cannot set the target for a junction point to a file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fail to create a symbolic link with non-existent target
|
||||||
|
try {
|
||||||
|
New-Link -link_path "$path\symlink-fail" -link_target "$path\fake-folder" -link_type "link"
|
||||||
|
Assert-True -expression $false -message "creation of symbolic link should have failed if target did not exist"
|
||||||
|
} catch {
|
||||||
|
Assert-Equals -actual $_.Exception.Message -expected "link_target '$path\fake-folder' does not exist, cannot create link"
|
||||||
|
}
|
||||||
|
|
||||||
|
# create recursive symlink
|
||||||
|
Run-Command -command "cmd.exe /c mklink /D symlink-rel folder" -working_directory $path | Out-Null
|
||||||
|
$rel_link_result = Get-Link -link_path "$path\symlink-rel"
|
||||||
|
Assert-Equals -actual $rel_link_result.Type -expected "SymbolicLink"
|
||||||
|
Assert-Equals -actual $rel_link_result.SubstituteName -expected "folder"
|
||||||
|
Assert-Equals -actual $rel_link_result.PrintName -expected "folder"
|
||||||
|
Assert-Equals -actual $rel_link_result.TargetPath -expected "folder"
|
||||||
|
Assert-Equals -actual $rel_link_result.AbsolutePath -expected $folder_target
|
||||||
|
Assert-Equals -actual $rel_link_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# create a symbolic file test
|
||||||
|
New-Link -link_path $symlink_file_path -link_target $file_target -link_type "link"
|
||||||
|
$file_link_result = Get-Link -link_path $symlink_file_path
|
||||||
|
Assert-Equals -actual $file_link_result.Type -expected "SymbolicLink"
|
||||||
|
Assert-Equals -actual $file_link_result.SubstituteName -expected "\??\$file_target"
|
||||||
|
Assert-Equals -actual $file_link_result.PrintName -expected $file_target
|
||||||
|
Assert-Equals -actual $file_link_result.TargetPath -expected $file_target
|
||||||
|
Assert-Equals -actual $file_link_result.AbsolutePath -expected $file_target
|
||||||
|
Assert-Equals -actual $file_link_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# create a symbolic link folder test
|
||||||
|
New-Link -link_path $symlink_folder_path -link_target $folder_target -link_type "link"
|
||||||
|
$folder_link_result = Get-Link -link_path $symlink_folder_path
|
||||||
|
Assert-Equals -actual $folder_link_result.Type -expected "SymbolicLink"
|
||||||
|
Assert-Equals -actual $folder_link_result.SubstituteName -expected "\??\$folder_target"
|
||||||
|
Assert-Equals -actual $folder_link_result.PrintName -expected $folder_target
|
||||||
|
Assert-Equals -actual $folder_link_result.TargetPath -expected $folder_target
|
||||||
|
Assert-Equals -actual $folder_link_result.AbsolutePath -expected $folder_target
|
||||||
|
Assert-Equals -actual $folder_link_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# create a junction point test
|
||||||
|
New-Link -link_path $junction_point_path -link_target $folder_target -link_type "junction"
|
||||||
|
$junction_point_result = Get-Link -link_path $junction_point_path
|
||||||
|
Assert-Equals -actual $junction_point_result.Type -expected "JunctionPoint"
|
||||||
|
Assert-Equals -actual $junction_point_result.SubstituteName -expected "\??\$folder_target"
|
||||||
|
Assert-Equals -actual $junction_point_result.PrintName -expected $folder_target
|
||||||
|
Assert-Equals -actual $junction_point_result.TargetPath -expected $folder_target
|
||||||
|
Assert-Equals -actual $junction_point_result.AbsolutePath -expected $folder_target
|
||||||
|
Assert-Equals -actual $junction_point_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# create a hard link test
|
||||||
|
New-Link -link_path $hardlink_path -link_target $file_target -link_type "hard"
|
||||||
|
$hardlink_result = Get-Link -link_path $hardlink_path
|
||||||
|
Assert-Equals -actual $hardlink_result.Type -expected "HardLink"
|
||||||
|
Assert-Equals -actual $hardlink_result.SubstituteName -expected $null
|
||||||
|
Assert-Equals -actual $hardlink_result.PrintName -expected $null
|
||||||
|
Assert-Equals -actual $hardlink_result.TargetPath -expected $null
|
||||||
|
Assert-Equals -actual $hardlink_result.AbsolutePath -expected $null
|
||||||
|
if ($hardlink_result.HardTargets[0] -ne $hardlink_path -and $hardlink_result.HardTargets[1] -ne $hardlink_path) {
|
||||||
|
Assert-True -expression $false -message "file $hardlink_path is not a target of the hard link"
|
||||||
|
}
|
||||||
|
if ($hardlink_result.HardTargets[0] -ne $file_target -and $hardlink_result.HardTargets[1] -ne $file_target) {
|
||||||
|
Assert-True -expression $false -message "file $file_target is not a target of the hard link"
|
||||||
|
}
|
||||||
|
Assert-equals -actual (Get-Content -Path $hardlink_path -Raw) -expected (Get-Content -Path $file_target -Raw)
|
||||||
|
|
||||||
|
# create a new hard link and verify targets go to 3
|
||||||
|
New-Link -link_path $hardlink_path_2 -link_target $file_target -link_type "hard"
|
||||||
|
$hardlink_result_2 = Get-Link -link_path $hardlink_path
|
||||||
|
Assert-True -expression ($hardlink_result_2.HardTargets.Count -eq 3) -message "did not return 3 targets for the hard link, actual $($hardlink_result_2.Targets.Count)"
|
||||||
|
|
||||||
|
# check if broken symbolic link still works
|
||||||
|
Remove-Item -Path $folder_target -Force | Out-Null
|
||||||
|
$broken_link_result = Get-Link -link_path $symlink_folder_path
|
||||||
|
Assert-Equals -actual $broken_link_result.Type -expected "SymbolicLink"
|
||||||
|
Assert-Equals -actual $broken_link_result.SubstituteName -expected "\??\$folder_target"
|
||||||
|
Assert-Equals -actual $broken_link_result.PrintName -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_link_result.TargetPath -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_link_result.AbsolutePath -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_link_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# check if broken junction point still works
|
||||||
|
$broken_junction_result = Get-Link -link_path $junction_point_path
|
||||||
|
Assert-Equals -actual $broken_junction_result.Type -expected "JunctionPoint"
|
||||||
|
Assert-Equals -actual $broken_junction_result.SubstituteName -expected "\??\$folder_target"
|
||||||
|
Assert-Equals -actual $broken_junction_result.PrintName -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_junction_result.TargetPath -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_junction_result.AbsolutePath -expected $folder_target
|
||||||
|
Assert-Equals -actual $broken_junction_result.HardTargets -expected $null
|
||||||
|
|
||||||
|
# delete file symbolic link
|
||||||
|
Remove-Link -link_path $symlink_file_path
|
||||||
|
Assert-True -expression (-not (Test-Path -Path $symlink_file_path)) -message "failed to delete file symbolic link"
|
||||||
|
|
||||||
|
# delete folder symbolic link
|
||||||
|
Remove-Link -link_path $symlink_folder_path
|
||||||
|
Assert-True -expression (-not (Test-Path -Path $symlink_folder_path)) -message "failed to delete folder symbolic link"
|
||||||
|
|
||||||
|
# delete junction point
|
||||||
|
Remove-Link -link_path $junction_point_path
|
||||||
|
Assert-True -expression (-not (Test-Path -Path $junction_point_path)) -message "failed to delete junction point"
|
||||||
|
|
||||||
|
# delete hard link
|
||||||
|
Remove-Link -link_path $hardlink_path
|
||||||
|
Assert-True -expression (-not (Test-Path -Path $hardlink_path)) -message "failed to delete hard link"
|
||||||
|
|
||||||
|
Exit-Json @{ data = "success" }
|
|
@ -76,6 +76,15 @@
|
||||||
that:
|
that:
|
||||||
- argv_test.data == 'success'
|
- argv_test.data == 'success'
|
||||||
|
|
||||||
|
- name: call module with symbolic link tests
|
||||||
|
symbolic_link_test:
|
||||||
|
path: C:\ansible testing
|
||||||
|
register: symbolic_link
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- symbolic_link.data == 'success'
|
||||||
|
|
||||||
- name: remove testing folder
|
- name: remove testing folder
|
||||||
win_file:
|
win_file:
|
||||||
path: C:\ansible testing
|
path: C:\ansible testing
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue