33

In .NET, we can retrieve the paths to 'special folders', like Documents / Desktop etc. Today I tried to find a way to get the path to the 'Downloads' folder, but it's not special enough it seems.

I know I can just do 'C:\Users\Username\Downloads', but that seems an ugly solution. So how can I retrieve the path using .NET?

Ray
  • 7,940
  • 7
  • 58
  • 90
Maestro
  • 9,046
  • 15
  • 83
  • 116

11 Answers11

32

Yes it is special, discovering the name of this folder didn't become possible until Vista. .NET still needs to support prior operating systems. You can pinvoke SHGetKnownFolderPath() to bypass this limitation, like this:

using System.Runtime.InteropServices;
...

public static string GetDownloadsPath() {
    if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException();
    IntPtr pathPtr = IntPtr.Zero;
    try {
        SHGetKnownFolderPath(ref FolderDownloads, 0, IntPtr.Zero, out pathPtr);
        return Marshal.PtrToStringUni(pathPtr);
    }
    finally {
        Marshal.FreeCoTaskMem(pathPtr);
    }
}

private static Guid FolderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern int SHGetKnownFolderPath(ref Guid id, int flags, IntPtr token, out IntPtr path);
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Why use `SHGetKnownFolderPath` and not `Environment.SpecialFolder` ? – Kiquenet Mar 14 '17 at 11:30
  • Using ***Impersonator with LOGON32_LOGON_INTERACTIVE*** not working `Environment.GetFolderPath(Environment.SpecialFolder.Personal)` neither `SHGetKnownFolderPath` ? – Kiquenet Mar 14 '17 at 13:57
  • 4
    @Kiquenet: `Environment.SpecialFolder` doesn't cover _all_ known folders (see https://learn.microsoft.com/en-us/dotnet/api/system.environment.specialfolder?view=netframework-4.8), and the _Downloads_ folder is a notable omission. – mklement0 Sep 16 '19 at 04:15
  • 1
    Why is `CharSet.Auto` specified here? There is no string conversion taking place given the `PWSTR` out parameter is an `IntPtr`. In fact, automatic `string` conversion _is_ possible here, removing the need for manually calling `PtrToStringUni` and `FreeCoTaskMem`, but requires `CharSet.Unicode` since it's a `PWSTR`, and there are no `A` or `W` versions (I've provided suggested edits in my answer below). – Ray Apr 21 '22 at 12:40
4

The problem of your first answer is it would give you WRONG result if the default Downloads Dir has been changed to [Download1]! The proper way to do it covering all possibilities is

using System;
using System.Runtime.InteropServices;

static class cGetEnvVars_WinExp    {
    [DllImport("Shell32.dll")] private static extern int SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)]Guid rfid, uint dwFlags, IntPtr hToken,
        out IntPtr ppszPath);

    [Flags] public enum KnownFolderFlags : uint { SimpleIDList = 0x00000100
        , NotParentRelative = 0x00000200, DefaultPath = 0x00000400, Init = 0x00000800
        , NoAlias = 0x00001000, DontUnexpand = 0x00002000, DontVerify = 0x00004000
        , Create = 0x00008000,NoAppcontainerRedirection = 0x00010000, AliasOnly = 0x80000000
    }
    public static string GetPath(string RegStrName, KnownFolderFlags flags, bool defaultUser) {
        IntPtr outPath;
        int result = 
            SHGetKnownFolderPath (
                new Guid(RegStrName), (uint)flags, new IntPtr(defaultUser ? -1 : 0), out outPath
            );
        if (result >= 0)            {
            return Marshal.PtrToStringUni(outPath);
        } else {
            throw new ExternalException("Unable to retrieve the known folder path. It may not "
                + "be available on this system.", result);
        }
    }

}   

To test it, if you specifically desire your personal download dir, you flag default to false -->

using System.IO;

class Program    {
    [STAThread]
    static void Main(string[] args)        {
        string path2Downloads = string.Empty;
        path2Downloads = 
            cGetEnvVars_WinExp.GetPath("{374DE290-123F-4565-9164-39C4925E467B}", cGetEnvVars_WinExp.KnownFolderFlags.DontVerify, false);
        string[] files = { "" };
        if (Directory.Exists(path2Downloads)) {
            files = Directory.GetFiles(path2Downloads);
        }
    }
Ray
  • 7,940
  • 7
  • 58
  • 90
Jenna Leaf
  • 2,255
  • 21
  • 29
  • 3
    Your first example using `SHGetKnownFolderPath` will leak memory. The documentation for `SHGetKnownFolderPath` says the caller must free the `` ppszPath` value with `CoTaskMemFree. `, while the documentation for `Marshal.PtrToStringUni` says it does not free the string passed to it. – Dai May 18 '18 at 04:14
  • This tries to read a protected memory and instantly puts my application on break mode on the server. I have debugged it and this happens on `result` variable in the `GetPath` function any solution? – BlackMarker Aug 13 '18 at 13:45
  • I've checked the history of your post, and you added an incorrect answer to expand `%USERPROFILE%\Downloads`, which yields wrong results if the Downloads directory does not reside under `%USERPROFILE%` (an issue many answers on SE oversee), so I rolled back to the previous revision of the post, as the initial answer is correct. Please let me know if you have questions about this. – Ray Nov 07 '20 at 23:50
  • 1
    Please note that this answer still leaks memory due to not calling `Marshal.FreeCoTaskMem` on `outPath`. For a proper solution, s. Hans Passant's answer. – Ray Nov 08 '20 at 00:00
3

This is a refactor of the accepted answer as IMO it could be implemented a little nicer:

public static class KnownFolders
{
    public static Guid Contacts = new Guid("{56784854-C6CB-462B-8169-88E350ACB882}");
    public static Guid Desktop = new Guid("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}");
    public static Guid Documents = new Guid("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}");
    public static Guid Downloads = new Guid("{374DE290-123F-4565-9164-39C4925E467B}");
    public static Guid Favorites = new Guid("{1777F761-68AD-4D8A-87BD-30B759FA33DD}");
    public static Guid Links = new Guid("{BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968}");
    public static Guid Music = new Guid("{4BD8D571-6D19-48D3-BE97-422220080E43}");
    public static Guid Pictures = new Guid("{33E28130-4E1E-4676-835A-98395C3BC3BB}");
    public static Guid SavedGames = new Guid("{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}");
    public static Guid SavedSearches = new Guid("{7D1D3A04-DEBB-4115-95CF-2F29DA2920DA}");
    public static Guid Videos = new Guid("{18989B1D-99B5-455B-841C-AB7C74E4DDFC}");

    static Dictionary<string, Guid> Map { get; } = new Dictionary<string, Guid> {
        { nameof(Contacts), Contacts },
        { nameof(Desktop), Desktop },
        { nameof(Documents), Documents },
        { nameof(Downloads), Downloads },
        { nameof(Favorites), Favorites },
        { nameof(Links), Links },
        { nameof(Music), Music },
        { nameof(Pictures), Pictures },
        { nameof(SavedGames), SavedGames },
        { nameof(SavedSearches), SavedSearches },
        { nameof(Videos), Videos },
    };

    public static string GetPath(string knownFolder,
        KnownFolderFlags flags = KnownFolderFlags.DontVerify, bool defaultUser = false) =>
        Map.TryGetValue(knownFolder, out var knownFolderId)
            ? GetPath(knownFolderId, flags, defaultUser)
            : ThrowUnknownFolder();

    public static string GetPath(Guid knownFolderId, 
        KnownFolderFlags flags=KnownFolderFlags.DontVerify, bool defaultUser=false)
    {
        if (SHGetKnownFolderPath(knownFolderId, (uint)flags, new IntPtr(defaultUser ? -1 : 0), out var outPath) >= 0)
        {
            string path = Marshal.PtrToStringUni(outPath);
            Marshal.FreeCoTaskMem(outPath);
            return path;
        }
        return ThrowUnknownFolder();
    }

    //[DoesNotReturn]
    static string ThrowUnknownFolder() => 
        throw new NotSupportedException("Unable to retrieve the path for known folder. It may not be available on this system.");

    [DllImport("Shell32.dll")]
    private static extern int SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)]Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr ppszPath);
}

Can be called with:

var downloadPath = KnownFolders.GetPath(KnownFolders.Downloads);

Or sometimes it's more convenient to fetch it using a string:

var downloadPath = KnownFolders.GetPath(nameof(KnownFolders.Downloads));
mythz
  • 141,670
  • 29
  • 246
  • 390
  • If i could suggest something, use a dictionary like this: private static Dictionary _knownFolderGuids. I think you can remove a lot of thing by using a such dictionary. – Vonkel. Jan 05 '21 at 17:40
  • Microsoft states to free the memory pointed to by ppszPath also in case of the function returning failure. You may want to P/Invoke to a `string` parameter directly and decorate the `DllImport` with `CharSet = CharSet.Unicode` to handle this automatically. You can also use `PreserveSig = false` to automatically throw an appropriate exception in case of the `HRESULT` indicating failure (and modify the imported call to return `void`). – Ray Apr 19 '22 at 20:02
2

Hans Passant's answer above about using SHGetKnownFolderPath is (as usual) absolutely correct. But if you like, you can channel some more P/Invoke functionality to simplify the import and make its signature more ".NET-esque":

  • We specify CharSet = CharSet.Unicode since the returned string is always a Unicode PWSTR. There are no A and W overloads, thus we also set ExactSpelling = true to prevent runtime searching for such. This now allows us to replace out IntPtr path with out string path which makes the marshaller convert the string automatically, including freeing the memory1. That removes the burden put on us to manually call PtrToStringUni and FreeCoTaskMem:

    public static string GetDownloadsPath() {
        if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException();
        string path;
        SHGetKnownFolderPath(ref FolderDownloads, 0, IntPtr.Zero, out path);
        return path;
    }
    
    private static Guid FolderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
    private static extern int SHGetKnownFolderPath(ref Guid id, int flags,
        IntPtr token, out string path);
    
  • We can specify PreserveSig = false to convert failure HRESULTs returned by the method into appropriate thrown exceptions, and replace the "return" value with the last out parameter, e.g. out string path:

    public static string GetDownloadsPath() {
        if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException();
        return SHGetKnownFolderPath(ref FolderDownloads, 0, IntPtr.Zero);
    }
    
    private static Guid FolderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true,
        PreserveSig = false)]
    private static extern string SHGetKnownFolderPath(ref Guid id, int flags,
        IntPtr token);
    
  • To further simplify passing parameters to the method, we can tell the marshaller to automatically pass the Guid as a reference with MarshalAs(UnmanagedType.LPStruct):

    public static string GetDownloadsPath() {
        if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException();
        return SHGetKnownFolderPath(FolderDownloads, 0, IntPtr.Zero);
    }
    
    private static Guid FolderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true,
        PreserveSig = false)]
    private static extern string SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token);
    
  • To simulate the token parameter being declared optional in the WinAPI method, we can specify = 0 or = default, and use C# 9's new nint instead of an IntPtr:

    public static string GetDownloadsPath() {
        if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException();
        return SHGetKnownFolderPath(FolderDownloads, 0);
    }
    
    private static Guid FolderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true,
        PreserveSig = false)]
    private static extern string SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, nint token = 0);
    

I've provided a complete example retrieving more than just the Downloads folder in my other answer, and go into further detail on it on my CodeProject article.


1 Automatic string conversion only works in this specific case. The marshaller assumes that 1) the buffer pointed to by the IntPtr was allocated with CoTaskMemAlloc before, and 2) the caller must free it. Thus it always calls CoTaskMemFree on it after conversion, which crashes if the conditions weren't met. It also requires an appropriate CharSet in the DllImport attribute, or converts strings with wrong encoding.
Ray
  • 7,940
  • 7
  • 58
  • 90
1

Try this:

string path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)+ @"\Downloads";
Shaahin
  • 1,195
  • 3
  • 14
  • 22
  • 2
    Incorrect, Downloads directory (and any other user directory) may not reside inside UserProfile. – Ray Nov 07 '20 at 23:44
  • But this is standard, I didnt move or manipulated structure of Windows, and where its standard and works, and some people tried and voted, I think there is no reason to complain it. If you have download folder in other places, so maybe its your case is different. – Shaahin Nov 12 '20 at 06:03
  • 3
    No. It is a Windows _built-in feature_ to change the user folder locations. Simply right-click the folders, go to the Locations tab, and enter a new path. And then the results of your code are completely wrong. Code that just works "most of the time" is not a solution, it's a way of creating bugs. Besides, there are numerous actually correct answers here and on similar questions (you _must_ use `SHGetKnownFolderPath` for this), and other incorrect answers exactly like yours, so I don't know why you posted this to begin with. – Ray Nov 12 '20 at 14:15
0

Hans's answer works perfectly! And I appreciate it's a very old question, but seeing as .Net (for whatever reason) still hasn't plugged this functionality hole, I figured I'd post the below refactoring of Han's answer in case someone finds it useful.

  • added some missing error handling
  • access folder via a property instead of a method, i.e. consistent with other .Net Environment usage (e.g Environment.CurrentDirectory)

copy/paste snippet..

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace Utils
{
    public static class SpecialFolder
    {
        public static string Downloads => _downloads ??= GetDownloads();

        // workaround for missing .net feature SpecialFolder.Downloads
        // - https://stackoverflow.com/a/3795159/227110
        // - https://stackoverflow.com/questions/10667012/getting-downloads-folder-in-c
        private static string GetDownloads()
        {
            if (Environment.OSVersion.Version.Major < 6)
                throw new NotSupportedException();

            var pathPtr = IntPtr.Zero;
            try
            {
                if (SHGetKnownFolderPath(ref _folderDownloads, 0, IntPtr.Zero, out pathPtr) != 0)
                    throw new DirectoryNotFoundException();
                return Marshal.PtrToStringUni(pathPtr);
            }
            finally
            {
                Marshal.FreeCoTaskMem(pathPtr);
            }
        }

        [DllImport("shell32.dll", CharSet = CharSet.Auto)]
        private static extern int SHGetKnownFolderPath(ref Guid id, int flags, IntPtr token, out IntPtr path);

        private static Guid _folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
        private static string _downloads;
    }
}

usage

var downloadFolder = SpecialFolder.Downloads;
stoj
  • 1,116
  • 1
  • 14
  • 25
0

This is my simplest possible solution for getting the SavedGames path. For getting the path of other folders, you must change the GUID accordingly.

public static string GetSavedGamesPath()
{
    SHGetKnownFolderPath(new Guid("{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}"), 0x00004000, new IntPtr(0), out var PathPointer);
    var Result = Marshal.PtrToStringUni(PathPointer);
    Marshal.FreeCoTaskMem(PathPointer);

    return Result;
}

[DllImport("Shell32.dll")]
static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr ppszPath);
Xtro
  • 301
  • 2
  • 12
-1

I used the below code and it works for .net 4.6 with Windows 7 and above. The below code gives the user profile folder path -> "C:\Users\<username>"

string userProfileFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

Next to access the downloads folder just combine additional path strings as below:

string DownloadsFolder = userProfileFolder + "\\Downloads\\";

Now, the final result will be

"C:\Users\<username>\Downloads\"

Hope it saves time for someone in the future.

Jabez
  • 795
  • 9
  • 18
  • 3
    As stated in the comments to the other answers doing the same thing, this yields wrong results if the folder has been moved out of the user profile folder. – Ray Nov 25 '18 at 17:52
-1

It's not that hard. If you want to get the downloads folder directory in vb.net, Just follow these steps:

1.Add a label named Special_Direcories.

2.Add this code when the form loaded:

Special_directories.Text = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\downloads\"

This will set the label text as the user folder path and "\downloads\". This is how it will look like:

C:\users\USERFOLDER\downloads\

Lorenzo Isidori
  • 1,809
  • 2
  • 20
  • 31
  • 4
    Incorrect, Downloads directory (and any other user directory) may not reside inside UserProfile. – Ray Nov 07 '20 at 23:44
-3

try:

Dim Dd As String = Environment.GetFolderPath(Environment.SpecialFolder.Favorites)
Dim downloD As String = Dd.Replace("Favorites", "Downloads")
txt1.text = downLoD

its just a trick , not solution .

  • Posting an answer to a question posted and answered 6 years ago - Please explain why your solution is better to use than the existing one – Gilad Green Oct 02 '16 at 08:04
  • ***Please explain why your solution is better to use than the existing one***? Better not use `SHGetKnownFolderPath` ? – Kiquenet Mar 14 '17 at 13:41
  • 4
    **Not god solution:** `The user might have moved the folder to another location (which is quite easy in 8, 8.1 and 10).` [Getting All "Special Folders" in .NET](https://www.codeproject.com/articles/878605/getting-all-special-folders-in-net) – Kiquenet Mar 16 '17 at 11:54
  • 1
    Sorry I have to laugh! This is a cute trick, but it is still called trick (not a solution - it will break one of these days!): replace "favorites" with "downloads", haha! Wish life were that easy! (what if i change my downloads location! ) – Jenna Leaf Oct 26 '17 at 18:50
-5

For VB, try...

Dim strNewPath As String = IO.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)) + "\Downloads\"
H--
  • 95
  • 3
  • 14
  • 1
    This is wrong, s. my remarks here: http://www.codeproject.com/Articles/878605/Getting-all-Special-Folders-in-NET – Ray Jul 11 '15 at 12:59
  • 2
    Don't hard code paths here. The user might have moved the folder to another location (which is quite easy in 8, 8.1 and 10)... – Simon Mattes Mar 29 '16 at 11:30