How to determine if a directory path was SUBST'd - c#

How can I figure out if a file is in a folder that has been SUBST'ed or is located in a user folder using C#?

I think you need to P/Invoke QueryDosDevice() for the drive letter. Subst drives will return a symbolic link, similar to \??\C:\blah. The \??\ prefix indicates it is substituted, the rest gives you the drive+directory.

This is the code I use to get the information if a path is substed:
(Some parts come from pinvoke)
using System.Runtime.InteropServices;
[DllImport("kernel32.dll", SetLastError=true)]
static extern uint QueryDosDevice(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax);
public static bool IsSubstedPath(string path, out string realPath)
{
StringBuilder pathInformation = new StringBuilder(250);
string driveLetter = null;
uint winApiResult = 0;
realPath = null;
try
{
// Get the drive letter of the path
driveLetter = Path.GetPathRoot(path).Replace("\\", "");
}
catch (ArgumentException)
{
return false;
//<------------------
}
winApiResult = QueryDosDevice(driveLetter, pathInformation, 250);
if(winApiResult == 0)
{
int lastWinError = Marshal.GetLastWin32Error(); // here is the reason why it fails - not used at the moment!
return false;
//<-----------------
}
// If drive is substed, the result will be in the format of "\??\C:\RealPath\".
if (pathInformation.ToString().StartsWith("\\??\\"))
{
// Strip the \??\ prefix.
string realRoot = pathInformation.ToString().Remove(0, 4);
// add backshlash if not present
realRoot += pathInformation.ToString().EndsWith(#"\") ? "" : #"\";
//Combine the paths.
realPath = Path.Combine(realRoot, path.Replace(Path.GetPathRoot(path), ""));
return true;
//<--------------
}
realPath = path;
return false;
}

I think you have a few choices --
Via System.Management classes:
http://briancaos.wordpress.com/2009/03/05/get-local-path-from-unc-path/
Or
Via P/Invoking this MAPI function:
ScUNCFromLocalPath
http://msdn.microsoft.com/en-us/library/cc842520.aspx

If SUBST is run without parameters it produces a listing of all current substitutions. Get the list, and check your directory against the list.
There is also the issue of mapping a volume to a directory. I have never attempted to detect these, but the mount point directories do show up differently than regular directories, so they must have a different attribute of some kind, and that could be detected.

Related

Find Path to any given exe file name

I know similar questions have been asked before but all the answers have been partial, for example suggesting I use AssocQueryString which I am doing. However I'm still failing to find some exe. For example the code below fails to find outlook.exe or firefox.exe - Typing outlook.exe into Windows Explorer address bar finds these almost instantley.
In the code below the exe fileName can be any local location on the users machine, it may or may not be on the users search path.
How can I improve this to find the files? (this is called from a 32 bit exe)
// for example, get actual full path to outlook.exe
string fullPath = FindPath("outlook.exe");
public static string FindPath(string fileName)
{
uint length = UnsafeMethods.MAX_PATH;
StringBuilder path = new StringBuilder((int)length);
if (Path.GetDirectoryName(fileName) == string.Empty)
{
if (UnsafeMethods.AssocQueryString(UnsafeMethods.AssocF.OpenByExeName, UnsafeMethods.AssocStr.Executable, fileName, null, path, ref length) != 0)
{
IntPtr filePart = IntPtr.Zero;
IntPtr wow64Value = IntPtr.Zero;
// try 64 bit path
UnsafeMethods.Wow64DisableWow64FsRedirection(ref wow64Value);
bool success = UnsafeMethods.SearchPath(null, fileName, null, path.Capacity, path, ref filePart) > 0;
UnsafeMethods.Wow64RevertWow64FsRedirection(wow64Value);
if (!success)
{
// try 32 bit path
success = UnsafeMethods.SearchPath(null, fileName, null, path.Capacity, path, ref filePart) > 0;
}
}
return path.ToString();
}
else
{
return fileName;
}
}
It seems there are many places to look to find the path to an exe. While the original code above works, it is not an exhaustive search and you also need to look in the registry key SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ and also check the SpecialFolders Windows, System and SystemX86 (https://learn.microsoft.com/en-us/dotnet/api/system.environment.specialfolder?view=netframework-4.7.2)

Getting Downloads Folder in C#? [duplicate]

This question already has answers here:
How to programmatically derive Windows Downloads folder "%USERPROFILE%/Downloads"?
(11 answers)
Closed 5 years ago.
I have made some code that will search directories and display files in a listbox.
DirectoryInfo dinfo2 = new DirectoryInfo(#"C:\Users\Hunter\Downloads");
FileInfo[] Files2 = dinfo2.GetFiles("*.sto");
foreach (FileInfo file2 in Files2)
{
listBox1.Items.Add(file2.Name);
}
However, where it says Users\Hunter - well, when people get my software, their name is not Hunter. So how can I automatically detect the user's Downloads folder?
I have tried this:
string path = Environment.SpecialFolder.UserProfile + #"\Downloads";
DirectoryInfo dinfo2 = new DirectoryInfo(Environment.SpecialFolder.UserProfile + path);
FileInfo[] Files2 = dinfo2.GetFiles("*.sto");
foreach (FileInfo file2 in Files2)
{
listBox1.Items.Add(file2.Name);
}
I get an error though.
The Downloads folder is a so called "known" folder, together with Documents, Videos, and others.
Do NOT:
combine hardcoded path segments to retrieve known folder paths
assume known folders are children of the user folder
abuse a long deprecated registry key storing outdated paths
Known folders can be redirected anywhere in their property sheets. I've gone into more detail on this several years ago in my CodeProject article.
Do:
use the WinAPI method SHGetKnownFolderPath as it is the intended and only correct method to retrieve those paths.
You can p/invoke it as follows (I've provided only a few GUIDs which cover the new user folders):
public enum KnownFolder
{
Contacts,
Downloads,
Favorites,
Links,
SavedGames,
SavedSearches
}
public static class KnownFolders
{
private static readonly Dictionary<KnownFolder, Guid> _guids = new()
{
[KnownFolder.Contacts] = new("56784854-C6CB-462B-8169-88E350ACB882"),
[KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
[KnownFolder.Favorites] = new("1777F761-68AD-4D8A-87BD-30B759FA33DD"),
[KnownFolder.Links] = new("BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968"),
[KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
[KnownFolder.SavedSearches] = new("7D1D3A04-DEBB-4115-95CF-2F29DA2920DA")
};
public static string GetPath(KnownFolder knownFolder)
{
return SHGetKnownFolderPath(_guids[knownFolder], 0);
}
[DllImport("shell32",
CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
nint hToken = 0);
}
Here's an example of retrieving the path of the Downloads folder:
string downloadsPath = KnownFolders.GetPath(KnownFolder.Downloads);
Console.WriteLine($"Downloads folder path: {downloadsPath}");
NuGet Package
If you don't want to p/invoke yourself, have a look at my NuGet package (note that the usage is different, please check its README).
The easiest way is:
Process.Start("shell:Downloads");
If you only need to get the current user's download folder path, you can use this:
I extracted it from #PacMani 's code.
// using Microsoft.Win32;
string GetDownloadFolderPath()
{
return Registry.GetValue(#"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "{374DE290-123F-4565-9164-39C4925E467B}", String.Empty).ToString();
}
Note:
SHGetKnownFolderPath will return the WRONG value if you changed the download-folder.
The only thing that will ever return you the correct value is reading the shell-folders registry-key 374DE290-123F-4565-9164-39C4925E467B on Windows.
Now you can either use the "!Do not use this registry key", or you can get the wrong value.
You decide which is better for you.
Cross-Platform version:
public static string GetHomePath()
{
// Not in .NET 2.0
// System.Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (System.Environment.OSVersion.Platform == System.PlatformID.Unix)
return System.Environment.GetEnvironmentVariable("HOME");
return System.Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%");
}
public static string GetDownloadFolderPath()
{
if (System.Environment.OSVersion.Platform == System.PlatformID.Unix)
{
string pathDownload = System.IO.Path.Combine(GetHomePath(), "Downloads");
return pathDownload;
}
return System.Convert.ToString(
Microsoft.Win32.Registry.GetValue(
#"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
,"{374DE290-123F-4565-9164-39C4925E467B}"
,String.Empty
)
);
}
string download = Environment.GetEnvironmentVariable("USERPROFILE")+#"\"+"Downloads";
http://msdn.microsoft.com/en-us//library/system.environment.specialfolder.aspx
There are the variables with the path to some special folders.
typically, your software shall have a configurable variable that stores the user's download folder, which can be assigned by the user, and provide a default value when not set. You can store the value in app config file or the registry.
Then in your code read the value from where it's stored.

Check if a file is real or a symbolic link

Is there a way to tell using C# if a file is real or a symbolic link?
I've dug through the MSDN W32 docs, and can't find anything for checking this. I'm using CreateSymbolicLink from here, and it's working fine.
private bool IsSymbolic(string path)
{
FileInfo pathInfo = new FileInfo(path);
return pathInfo.Attributes.HasFlag(FileAttributes.ReparsePoint);
}
I have some source code for symlinks posted on my blog that will allow you to:
create symlinks
check whether a path is a symlink
retrieve the target of a symlink
It also contains NUnit test cases, that you may wish to extend.
The meaty bit is:
private static SafeFileHandle getFileHandle(string path)
{
return CreateFile(path, genericReadAccess, shareModeAll, IntPtr.Zero, openExisting,
fileFlagsForOpenReparsePointAndBackupSemantics, IntPtr.Zero);
}
public static string GetTarget(string path)
{
SymbolicLinkReparseData reparseDataBuffer;
using (SafeFileHandle fileHandle = getFileHandle(path))
{
if (fileHandle.IsInvalid)
{
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
int outBufferSize = Marshal.SizeOf(typeof(SymbolicLinkReparseData));
IntPtr outBuffer = IntPtr.Zero;
try
{
outBuffer = Marshal.AllocHGlobal(outBufferSize);
int bytesReturned;
bool success = DeviceIoControl(
fileHandle.DangerousGetHandle(), ioctlCommandGetReparsePoint, IntPtr.Zero, 0,
outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);
fileHandle.Close();
if (!success)
{
if (((uint)Marshal.GetHRForLastWin32Error()) == pathNotAReparsePointError)
{
return null;
}
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
reparseDataBuffer = (SymbolicLinkReparseData)Marshal.PtrToStructure(
outBuffer, typeof(SymbolicLinkReparseData));
}
finally
{
Marshal.FreeHGlobal(outBuffer);
}
}
if (reparseDataBuffer.ReparseTag != symLinkTag)
{
return null;
}
string target = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
reparseDataBuffer.PrintNameOffset, reparseDataBuffer.PrintNameLength);
return target;
}
That is:
Open the file with CreateFile()
Call DeviceIoControl() to get the reparse point data (NOTE: it could be a junction point!)
Check out the returned data structure to inspect. The reparse tag will tell you if it is a junction point or symbolic link. This may be all you want to do.
Here is an example of differentiating files and directories from links to files and links to directories.
Links to either files or directories maintain their own attributes (creation date, permissions) separate from their targets.
File links can be deleted (e.g. using "del") without affecting the target file.
Directory links can be removed (e.g. "rmdir") without affecting the target directory. Take care when using "rd /s". This WILL remove the directory link target.
The key FileAttributes flag to check in both FileInfo and DirectoryInfo is FileAttributes.ReparsePoint.
static void Main( string[] args ) {
FileInfo file_info = new FileInfo(args[0]);
DirectoryInfo directory_info = new DirectoryInfo(args[0]);
bool is_file = file_info.Exists;
bool is_directory = directory_info.Exists;
if (is_file) {
Console.WriteLine(file_info.ToString() + " is a file");
if ( file_info.Attributes.HasFlag(FileAttributes.ReparsePoint) )
Console.WriteLine(args[0] + " is a Windows file link");
}
else if (is_directory) {
Console.WriteLine(directory_info.ToString() + " is a directory");
if ( directory_info.Attributes.HasFlag(FileAttributes.ReparsePoint) )
Console.WriteLine(args[0] + " is a Windows directory link");
}
It proves the above answers are not reliable.
Finally I got the right solution from MSDN:
To determine if a specified directory is a mounted folder, first call the GetFileAttributes function and inspect the FILE_ATTRIBUTE_REPARSE_POINT flag in the return value to see if the directory has an associated reparse point. If it does, use the FindFirstFile and FindNextFile functions to obtain the reparse tag in the dwReserved0 member of the WIN32_FIND_DATA structure. To determine if the reparse point is a mounted folder (and not some other form of reparse point), test whether the tag value equals the value IO_REPARSE_TAG_MOUNT_POINT. For more information, see Reparse Points.
Starting with .NET 6 you can use:
FileSystemInfo.LinkTarget Property
Property description:
Gets the target path of the link located in FullName, or null if this FileSystemInfo instance doesn't represent a link.
For example:
static bool IsSymbolicLink(string path)
{
FileInfo file = new FileInfo(path);
return file.LinkTarget != null;
}
GetFileInformationByHandle fills a BY_HANDLE_FILE_INFORMATION structure which has a field dwFileAttributes where bits are set with info about the file's attributes (details here). In particular, look at the bit at mask...:
FILE_ATTRIBUTE_REPARSE_POINT 1024 0x0400
A file or directory that has an
associated reparse point, or a file
that is a symbolic link.
According to this answer to Stack Overflow question Find out whether a file is a symbolic link in PowerShell, getting the System.IO.FileAttributes for the file (via File.GetAttributes), and testing for the ReparsePoint bit, works. If the bit is set, it is a symlink or a junction point. If not, it is a regular file (or hardlink).
The library MonoPosix provides API to check if a file is a symbolic link:
public bool IsSymlink(string filePath)
=> UnixFileSystemInfo.GetFileSystemEntry(filePath).IsSymbolicLink;
I know I am late to the party but found this discussion when researching same question
I found the below worked for me so thought I would post in case of use to anyone else
It works like this:-
var provider = ReparsePointFactory.Provider;
var link = provider.GetLink(#"c:\program files (x86)\common files\microsoft shared\vgx\vgx.dll");
MsgBox("Link Type: " + link.Type.ToString + " Link Target: " + link.Target + " Link Attributes: " + link.Attributes.ToString);
https://github.com/NCodeGroup/NCode.ReparsePoints https://www.nuget.org/packages/NCode.ReparsePoints/

How to convert a relative path to an absolute path in a Windows application?

How do I convert a relative path to an absolute path in a Windows application?
I know we can use server.MapPath() in ASP.NET. But what can we do in a Windows application?
I mean, if there is a .NET built-in function that can handle that...
Have you tried:
string absolute = Path.GetFullPath(relative);
? Note that that will use the current working directory of the process, not the directory containing the executable. If that doesn't help, please clarify your question.
If you want to get the path relative to your .exe then use
string absolute = Path.Combine(Application.ExecutablePath, relative);
This one works for paths on different drives, for drive-relative paths and for actual relative paths. Heck, it even works if the basePath isn't actually absolute; it always uses the current working directory as final fallback.
public static String GetAbsolutePath(String path)
{
return GetAbsolutePath(null, path);
}
public static String GetAbsolutePath(String basePath, String path)
{
if (path == null)
return null;
if (basePath == null)
basePath = Path.GetFullPath("."); // quick way of getting current working directory
else
basePath = GetAbsolutePath(null, basePath); // to be REALLY sure ;)
String finalPath;
// specific for windows paths starting on \ - they need the drive added to them.
// I constructed this piece like this for possible Mono support.
if (!Path.IsPathRooted(path) || "\\".Equals(Path.GetPathRoot(path)))
{
if (path.StartsWith(Path.DirectorySeparatorChar.ToString()))
finalPath = Path.Combine(Path.GetPathRoot(basePath), path.TrimStart(Path.DirectorySeparatorChar));
else
finalPath = Path.Combine(basePath, path);
}
else
finalPath = path;
// resolves any internal "..\" to get the true full path.
return Path.GetFullPath(finalPath);
}
It's a bit older topic, but it might be useful for someone.
I have solved a similar problem, but in my case, the path was not at the beginning of the text.
So here is my solution:
public static class StringExtension
{
private const string parentSymbol = "..\\";
private const string absoluteSymbol = ".\\";
public static String AbsolutePath(this string relativePath)
{
string replacePath = AppDomain.CurrentDomain.BaseDirectory;
int parentStart = relativePath.IndexOf(parentSymbol);
int absoluteStart = relativePath.IndexOf(absoluteSymbol);
if (parentStart >= 0)
{
int parentLength = 0;
while (relativePath.Substring(parentStart + parentLength).Contains(parentSymbol))
{
replacePath = new DirectoryInfo(replacePath).Parent.FullName;
parentLength = parentLength + parentSymbol.Length;
};
relativePath = relativePath.Replace(relativePath.Substring(parentStart, parentLength), string.Format("{0}\\", replacePath));
}
else if (absoluteStart >= 0)
{
relativePath = relativePath.Replace(".\\", replacePath);
}
return relativePath;
}
}
Example:
Data Source=.\Data\Data.sdf;Persist Security Info=False;
Data Source=..\..\bin\Debug\Data\Data.sdf;Persist Security Info=False;

Compress a folder using NTFS compression in .NET

I want to compress a folder using NTFS compression in .NET. I found this post, but it does not work. It throws an exception ("Invalid Parameter").
DirectoryInfo directoryInfo = new DirectoryInfo( destinationDir );
if( ( directoryInfo.Attributes & FileAttributes.Compressed ) != FileAttributes.Compressed )
{
string objPath = "Win32_Directory.Name=" + "\"" + destinationDir + "\"";
using( ManagementObject dir = new ManagementObject( objPath ) )
{
ManagementBaseObject outParams = dir.InvokeMethod( "Compress", null, null );
uint ret = (uint)( outParams.Properties["ReturnValue"].Value );
}
}
Anybody knows how to enable NTFS compression on a folder?
Using P/Invoke is, in my experience, usually easier than WMI. I believe the following should work:
private const int FSCTL_SET_COMPRESSION = 0x9C040;
private const short COMPRESSION_FORMAT_DEFAULT = 1;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int DeviceIoControl(
SafeFileHandle hDevice,
int dwIoControlCode,
ref short lpInBuffer,
int nInBufferSize,
IntPtr lpOutBuffer,
int nOutBufferSize,
ref int lpBytesReturned,
IntPtr lpOverlapped);
public static bool EnableCompression(SafeFileHandle handle)
{
int lpBytesReturned = 0;
short lpInBuffer = COMPRESSION_FORMAT_DEFAULT;
return DeviceIoControl(handle, FSCTL_SET_COMPRESSION,
ref lpInBuffer, sizeof(short), IntPtr.Zero, 0,
ref lpBytesReturned, IntPtr.Zero) != 0;
}
Since you're trying to set this on a directory, you will probably need to use P/Invoke to call CreateFile using FILE_FLAG_BACKUP_SEMANTICS to get the SafeFileHandle on the directory.
Also, note that setting compression on a directory in NTFS does not compress all the contents, it only makes new files show up as compressed (the same is true for encryption). If you want to compress the entire directory, you'll need to walk the entire directory and call DeviceIoControl on each file/folder.
I have tested the code and it
(source: typepad.com)
!
Make sure it works for you with the gui. Maybe the allocation unit size is too big for compression. Or you don't have sufficient permissions.
For your destination use format like so: "c:/temp/testcomp" with forward slashes.
Full code:
using System.IO;
using System.Management;
class Program
{
static void Main(string[] args)
{
string destinationDir = "c:/temp/testcomp";
DirectoryInfo directoryInfo = new DirectoryInfo(destinationDir);
if ((directoryInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed)
{
string objPath = "Win32_Directory.Name=" + "'" + directoryInfo.FullName.Replace("\\", #"\\").TrimEnd('\\') + "'";
using (ManagementObject dir = new ManagementObject(objPath))
{
ManagementBaseObject outParams = dir.InvokeMethod("Compress", null, null);
uint ret = (uint)(outParams.Properties["ReturnValue"].Value);
}
}
}
}
There is a much simpler way, which I am using in Windows 8 64-bit, rewritten for VB.NET. Enjoy.
Dim Path as string = "c:\test"
Dim strComputer As String = "."
Dim objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
Dim colFolders = objWMIService.ExecQuery("Select * from Win32_Directory where name = '" & Replace(path, "\", "\\") & "'")
For Each objFolder In colFolders
objFolder.Compress()
Next
works great for me. Chagne .\root to \pcname\root if you need to do it on another computer. Use with care.
When creating the Win32_Directory.Name=... string you need to double the backslashes, so for example the path C:\Foo\Bar would be built up as:
Win32_Directory.Name="C:\\Foo\\Bar",
or using your example code:
string objPath = "Win32_Directory.Name=\"C:\\\\Foo\\\\Bar\"";
Apparently the string is fed to some process that expects an escaped form of the path string.
This is a slight adaption of Igal Serban answer. I ran into a subtle issue with the Name having to be in a very specific format. So I added some Replace("\\", #"\\").TrimEnd('\\') magic to normalize the path first, I also cleaned up the code a bit.
var dir = new DirectoryInfo(_outputFolder);
if (!dir.Exists)
{
dir.Create();
}
if ((dir.Attributes & FileAttributes.Compressed) == 0)
{
try
{
// Enable compression for the output folder
// (this will save a ton of disk space)
string objPath = "Win32_Directory.Name=" + "'" + dir.FullName.Replace("\\", #"\\").TrimEnd('\\') + "'";
using (ManagementObject obj = new ManagementObject(objPath))
{
using (obj.InvokeMethod("Compress", null, null))
{
// I don't really care about the return value,
// if we enabled it great but it can also be done manually
// if really needed
}
}
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine("Cannot enable compression for folder '" + dir.FullName + "': " + ex.Message, "WMI");
}
}
I don't believe there is a way to set folder compression in the .NET framework as the docs (remarks section) claim it cannot be done through File.SetAttributes. This seems to be only available in the Win32 API using the DeviceIoControl function. One can still do this through .NET by using PInvoke.
Once familiar with PInvoke in general, check out the reference at pinvoke.net that discusses what the signature needs to look like in order to make this happen.

Categories