LongPathFunctions.ps1
######################################## # Private functions. ######################################## function ConvertFrom-LongFormPath { [CmdletBinding()] param([string]$Path) if ($Path) { if ($Path.StartsWith('\\?\UNC')) { # E.g. \\?\UNC\server\share -> \\server\share return $Path.Substring(1, '\?\UNC'.Length) } elseif ($Path.StartsWith('\\?\')) { # E.g. \\?\C:\directory -> C:\directory return $Path.Substring('\\?\'.Length) } } return $Path } function ConvertTo-LongFormPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path) [string]$longFormPath = Get-FullNormalizedPath -Path $Path if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { if ($longFormPath.StartsWith('\\')) { # E.g. \\server\share -> \\?\UNC\server\share return "\\?\UNC$($longFormPath.Substring(1))" } else { # E.g. C:\directory -> \\?\C:\directory return "\\?\$longFormPath" } } return $longFormPath } # TODO: ADD A SWITCH TO EXCLUDE FILES, A SWITCH TO EXCLUDE DIRECTORIES, AND A SWITCH NOT TO FOLLOW REPARSE POINTS. function Get-DirectoryChildItem { [CmdletBinding()] param( [string]$Path, [ValidateNotNullOrEmpty()] [Parameter()] [string]$Filter = "*", [switch]$Force, [VstsTaskSdk.FS.FindFlags]$Flags = [VstsTaskSdk.FS.FindFlags]::LargeFetch, [VstsTaskSdk.FS.FindInfoLevel]$InfoLevel = [VstsTaskSdk.FS.FindInfoLevel]::Basic, [switch]$Recurse) $stackOfDirectoryQueues = New-Object System.Collections.Stack while ($true) { $directoryQueue = New-Object System.Collections.Queue $fileQueue = New-Object System.Collections.Queue $findData = New-Object VstsTaskSdk.FS.FindData $longFormPath = (ConvertTo-LongFormPath $Path) $handle = $null try { $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( [System.IO.Path]::Combine($longFormPath, $Filter), $InfoLevel, $findData, [VstsTaskSdk.FS.FindSearchOps]::NameMatch, [System.IntPtr]::Zero, $Flags) if (!$handle.IsInvalid) { while ($true) { if ($findData.fileName -notin '.', '..') { $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes # If the item is hidden, check if $Force is specified. if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { # Create the item. $item = New-Object -TypeName psobject -Property @{ 'Attributes' = $attributes 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) 'Name' = $findData.fileName } # Output directories immediately. if ($item.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { $item # Append to the directory queue if recursive and default filter. if ($Recurse -and $Filter -eq '*') { $directoryQueue.Enqueue($item) } } else { # Hold the files until all directories have been output. $fileQueue.Enqueue($item) } } } if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } if ($handle.IsInvalid) { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path )) } } } } finally { if ($handle -ne $null) { $handle.Dispose() } } # If recursive and non-default filter, queue child directories. if ($Recurse -and $Filter -ne '*') { $findData = New-Object VstsTaskSdk.FS.FindData $handle = $null try { $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( [System.IO.Path]::Combine($longFormPath, '*'), [VstsTaskSdk.FS.FindInfoLevel]::Basic, $findData, [VstsTaskSdk.FS.FindSearchOps]::NameMatch, [System.IntPtr]::Zero, $Flags) if (!$handle.IsInvalid) { while ($true) { if ($findData.fileName -notin '.', '..') { $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes # If the item is hidden, check if $Force is specified. if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { # Collect directories only. if ($attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { # Create the item. $item = New-Object -TypeName psobject -Property @{ 'Attributes' = $attributes 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) 'Name' = $findData.fileName } $directoryQueue.Enqueue($item) } } } if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } if ($handle.IsInvalid) { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path )) } } } } finally { if ($handle -ne $null) { $handle.Dispose() } } } # Output the files. $fileQueue # Push the directory queue onto the stack if any directories were found. if ($directoryQueue.Count) { $stackOfDirectoryQueues.Push($directoryQueue) } # Break out of the loop if no more directory queues to process. if (!$stackOfDirectoryQueues.Count) { break } # Get the next path. $directoryQueue = $stackOfDirectoryQueues.Peek() $Path = $directoryQueue.Dequeue().FullName # Pop the directory queue if it's empty. if (!$directoryQueue.Count) { $null = $stackOfDirectoryQueues.Pop() } } } function Get-FullNormalizedPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path) [string]$outPath = $Path [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() if ($bufferSize -gt 0) { $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() if ($length -gt 0) { $outPath = $absolutePath.ToString() } else { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( $lastWin32Error Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path )) } } else { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( $lastWin32Error Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path )) } if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { $outPath = $outPath.TrimEnd('\') } $outPath } ######################################## # Types. ######################################## # If the type has already been loaded once, then it is not loaded again. Write-Verbose "Adding long path native methods." Add-Type -Debug:$false -TypeDefinition @' namespace VstsTaskSdk.FS { using System; using System.Runtime.InteropServices; public static class NativeMethods { private const string Kernel32Dll = "kernel32.dll"; [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool FindClose(IntPtr hFindFile); // HANDLE WINAPI FindFirstFile( // _In_ LPCTSTR lpFileName, // _Out_ LPWIN32_FIND_DATA lpFindFileData // ); [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] public static extern SafeFindHandle FindFirstFile( [MarshalAs(UnmanagedType.LPTStr)] string fileName, [In, Out] FindData findFileData ); //HANDLE WINAPI FindFirstFileEx( // _In_ LPCTSTR lpFileName, // _In_ FINDEX_INFO_LEVELS fInfoLevelId, // _Out_ LPVOID lpFindFileData, // _In_ FINDEX_SEARCH_OPS fSearchOp, // _Reserved_ LPVOID lpSearchFilter, // _In_ DWORD dwAdditionalFlags //); [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] public static extern SafeFindHandle FindFirstFileEx( [MarshalAs(UnmanagedType.LPTStr)] string fileName, [In] FindInfoLevel fInfoLevelId, [In, Out] FindData lpFindFileData, [In] FindSearchOps fSearchOp, IntPtr lpSearchFilter, [In] FindFlags dwAdditionalFlags ); [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool FindNextFile(SafeFindHandle hFindFile, [In, Out] FindData lpFindFileData); [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] public static extern int GetFileAttributes(string lpFileName); [DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)] public static extern uint GetFullPathName( [MarshalAs(UnmanagedType.LPTStr)] string lpFileName, uint nBufferLength, [Out] System.Text.StringBuilder lpBuffer, System.Text.StringBuilder lpFilePart ); } //for mapping to the WIN32_FIND_DATA native structure [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public sealed class FindData { // NOTE: // Although it may seem correct to Marshal the string members of this class as UnmanagedType.LPWStr, they // must explicitly remain UnmanagedType.ByValTStr with the size constraints noted. Otherwise we end up with // COM Interop exceptions while trying to marshal the data across the PInvoke boundaries. public int fileAttributes; public System.Runtime.InteropServices.ComTypes.FILETIME creationTime; public System.Runtime.InteropServices.ComTypes.FILETIME lastAccessTime; public System.Runtime.InteropServices.ComTypes.FILETIME lastWriteTime; public int nFileSizeHigh; public int nFileSizeLow; public int dwReserved0; public int dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string fileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string alternateFileName; } //A Win32 safe find handle in which a return value of -1 indicates it's invalid public sealed class SafeFindHandle : Microsoft.Win32.SafeHandles.SafeHandleMinusOneIsInvalid { public SafeFindHandle() : base(true) { return; } [System.Runtime.ConstrainedExecution.ReliabilityContract(System.Runtime.ConstrainedExecution.Consistency.WillNotCorruptState, System.Runtime.ConstrainedExecution.Cer.Success)] protected override bool ReleaseHandle() { return NativeMethods.FindClose(handle); } } // Refer https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx [Flags] public enum Attributes : uint { None = 0x00000000, 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, IntegrityStream = 0x00008000, Virtual = 0x00010000, NoScrubData = 0x00020000, Write_Through = 0x80000000, Overlapped = 0x40000000, NoBuffering = 0x20000000, RandomAccess = 0x10000000, SequentialScan = 0x08000000, DeleteOnClose = 0x04000000, BackupSemantics = 0x02000000, PosixSemantics = 0x01000000, OpenReparsePoint = 0x00200000, OpenNoRecall = 0x00100000, FirstPipeInstance = 0x00080000 } [Flags] public enum FindFlags { None = 0, CaseSensitive = 1, LargeFetch = 2, } public enum FindInfoLevel { Standard = 0, Basic = 1, } public enum FindSearchOps { NameMatch = 0, LimitToDirectories = 1, LimitToDevices = 2, } } '@ |