If you use Microsoft DFS, determining “real” location of a file can be tricky… at least in a quick and script-y way. You can always right-click, choose properties and click on the DFS tab if you’ve got the time/permissions etc. For a while and for valid-ish reasons, we had DFS links that pointed to other DFS links/namespaces and this made it more difficult to know where a file lived. For example:
\\domain\[DFSnamespace]Alpha\[link]Bravo -> \\domain\[DFSnamespace]Bravo\
So, consider the path\\domain\Alpha\Bravo\Charlie\Delta.txt. This would really be:
\\domain\[namespace]Alpha\[link]Bravo\[subfolder]Charlie\[file]Delta.txt
And that Bravo link would translate it to:
\\domain\[namespace]Bravo\[link]Charlie\[file]Delta.txt
Which in turn would translate to:
\\server\[share]Charlie\[file]Delta.txt
Clear as mud, right? Additionally, once you get to that final share, it’s not always clear where on the local file system Delta.txt might live – that’s another thing you have to look up. I’m not even going to deal with links with multiple targets.
So, I wrote a script that gives the “final destination” of a DFS path and optionally all the paths along the way and or the path on the local file system.
To begin, I needed a way to look up DFS link targets. I could have called dfsutil, the command line DFS administration tool, and parsed the text but that’s slightly yucky. Someone once said, “If you’re parsing strings in PowerShell you’re doing something wrong.” and that’s mostly true. Additionally, the error handling isn’t as nice calling old school command line apps. They’ll do silly things like output informational text to StdErr or not give you reliable return codes. And if they prompt for something, it doesn’t work if you’re using the ISE.
Luckily, I came across this blog not long before. It links to a script that uses the Platform Invoke service (P/Invoke) in a custom type defined in the script in C# . This is pretty powerful stuff. It lets you write a type that can call unmanaged things directly from .dlls. Powershell can now do pretty much anything via just a script… given enough time to figure it out. See pinvoke.net for shortcuts to figuring lots of things out. I wanted to use the NetDFSGetClientInfo function.
NetDFSGetClientInfo is a little tricker than the example in the blog because it uses pointers and buffers. For help marshaling the unmanaged stuff, I turned to coworker who is a .Net guru. He did in a few minutes what might have taken me several hours of pain.
Here’s the type definition he came up with with some minor tweaks:
$signature = @" using System; using System.Runtime.InteropServices; using System.Collections; public class NativeNetAPIMethods { [DllImport("Netapi32.dll", EntryPoint = "NetApiBufferFree")] public static extern uint NetApiBufferFree(IntPtr buffer); [DllImport("Netapi32.dll")] public static extern uint NetDfsGetClientInfo( [MarshalAs(UnmanagedType.LPWStr)] string DfsEntryPath, [MarshalAs(UnmanagedType.LPWStr)] string ServerName, [MarshalAs(UnmanagedType.LPWStr)] string ShareName, int Level, out IntPtr Buffer ); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DFS_INFO_3 { public string EntryPath; public string Comment; public int State; public int NumberOfStorages; public IntPtr Storage; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DFS_STORAGE_INFO { public ulong State; public string ServerName; public string ShareName; } public static uint DfsGetClientInfo(string DfsEntryPath, ref string ServerName, ref string ShareName) { uint Result; IntPtr Buf; int x; Result = NetDfsGetClientInfo(DfsEntryPath, null, null, 3, out Buf); if (Result == 0) { DFS_INFO_3 info = (DFS_INFO_3)Marshal.PtrToStructure(Buf, typeof(DFS_INFO_3)); // Console.WriteLine("{0}, {1}, {2} ", info.EntryPath, info.Comment, info.NumberOfStorages); // We only care about the first target in our environment // Otherwise use "x < info.NumberOfStorage" and make ServerName and ShareName arrays or something for (x = 0; x < 1; x++) { IntPtr pStorage = new IntPtr(info.Storage.ToInt64() + x * Marshal.SizeOf(typeof(DFS_STORAGE_INFO))); DFS_STORAGE_INFO storage = (DFS_STORAGE_INFO)Marshal.PtrToStructure(pStorage, typeof(DFS_STORAGE_INFO)); ServerName = storage.ServerName; ShareName = storage.ShareName; // Console.WriteLine(" {0}, {1}, {2} ", storage.ServerName, storage.ShareName, storage.State); } NetApiBufferFree(Buf); } return (Result); } } "@
The meat of this is in the DfsGetClientInfo method. It takes the string to the UNC path of the link and two references to strings to return the results – one for the server name and one for the share name and it returns the result of the native NetDfsGetClientInfo call. That’s the key for me because now I can do better error checking than relying on the console application.
To load the class we can use this bit of code:
# If the custom class isn't loaded, load it if (("NativeNetAPIMethods" -as [type]) -eq $null) { Add-Type -TypeDefinition $signature }
Note that when addding a type like this, you can’t remove it or update it by calling a cmdlet. You have to restart your PowerShell session. That means any time you change what’s in that type definition string, you have to quit PowerShell and open it again to see the change.
Once that’s loaded, we can call the native function like so:
$res = [NativeNetAPIMethods]::DfsGetClientInfo($pathPart,[ref] $server,[ref] $share)
Here, since we’re storing the server and share in variables passed as references, we need to prefix them with [ref] and they need to exist prior to calling this.
Using all of this, we can now write a recursive function that steps through each section of the path to find the link, then processes the target to make sure it doesn’t have links and so on. But, what if we want to store each of those intermediate paths? Well we could just write them to a global variable but it would really be better to store them a local variable and return the results when we’re done. Since the function is recursive, the problem we have there is each subsequent call, we’re in a new local scope. The following bit of code will determine how deep in the recursion we are and add the path to the variable in the first call.
# This finds our recursion depth so we can set the PathList variable used in -List # only in scope of the first function call. It also intitializes PathList as an array # or appends the path used in this call to PathList at the correct scope. if ($List) { $CallStack = Get-PSCallStack | where {$_.Command -eq $MyInvocation.MyCommand.Name} #If we're at the first call, $CallStack will be a stack object, not an array if ($CallStack.Count -eq $null) { $stackCount = 0 Set-Variable -Name PathList -Scope $stackCount -Value @($Path) } else { $stackCount = $CallStack.Count - 1 Set-Variable -Name PathList -Scope $stackCount -Value ((Get-Variable -name PathList).Value + ($Path) ) } }
And finally if we want to know the local path to a file based on the share path, the easiest thing is to use WMI.
Get-WmiObject -ComputerName Server -Class Win32_Share -Filter "Name = 'MyShare'"
will return an object with a property called Path that contains the local path. From that, we can write a function to parse a path for its server and share name and return the local path.
My mostly final version is here. One thing I’d like to do is get it handling Paths from the pipeline.