28

On my machine, it's here:

string downloadsPath = Path.Combine(
   Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
   "Downloads");

But on a colleagues machine, this folder doesnt exist, and his Downloads folder is in his 'My Documents' folder. We are both on Windows 7*.

*Edit: in fact, it turns out he was not running the app on his own machine but a Windows Server 2003 machine.

mackenir
  • 10,801
  • 16
  • 68
  • 100
  • 4
    the "Downloads" folder is localized. for non-english systems, it is not called "Downloads"... – Adrien Plisson Oct 06 '11 at 10:04
  • 1
    Does this answer your question? [How to programmatically derive Windows Downloads folder "%USERPROFILE%/Downloads"?](https://stackoverflow.com/questions/3795023/how-to-programmatically-derive-windows-downloads-folder-userprofile-downloads) – Ray Apr 19 '22 at 20:04

3 Answers3

29

Windows does not define a CSIDL for the Downloads folder and it is not available through the Environment.SpecialFolder enumeration.

However, the new Vista Known Folder API does define it with the ID of FOLDERID_Downloads. Probably the easiest way to obtain the actual value is to P/invoke SHGetKnownFolderPath.

public static class KnownFolder
{
    public static readonly Guid Downloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
}

[DllImport("shell32.dll", CharSet=CharSet.Unicode)]
static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out string pszPath);

static void Main(string[] args)
{
    string downloads;
    SHGetKnownFolderPath(KnownFolder.Downloads, 0, IntPtr.Zero, out downloads);
    Console.WriteLine(downloads);
}

Note that the P/invoke given on pinvoke.net is incorrect since it fails to use Unicode character set. Also I have taken advantage of the fact that this API returns memory allocated by the COM allocator. The default marshalling of the P/invoke above is to free the returned memory with CoTaskMemFree which is perfect for our needs.

Be careful that this is a Vista and up API and do not attempt to call it on XP/2003 or lower.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • under windows 7 the Downloads folder is a special folder, in that way that I can move it to any specific location that I want. The same way that you can move the My Documents folder etc. – Daan Timmer Oct 06 '11 at 10:11
  • @Daan There's no [CSIDL](http://msdn.microsoft.com/en-us/library/bb762494(VS.85).aspx) for it so it's not special in that way. – David Heffernan Oct 06 '11 at 10:16
  • @Daan But it is defined as a known folder. I was looking in the old CSIDL list. – David Heffernan Oct 06 '11 at 10:21
  • seems true, but it is however known as KNOWNFOLDERID: http://msdn.microsoft.com/en-us/library/dd378457(v=VS.85).aspx FOLDERID_Downloads [edit] seems like you already found/answered that part. – Daan Timmer Oct 06 '11 at 11:00
  • @Daan Yeah, known folders replaced CSIDL which is now deprecated as I understand. – David Heffernan Oct 06 '11 at 11:03
  • If you want and are okay with exceptions, you can further take advantage of P/Invoke here with `PreserveSig = false`, making it convert failure `HRESULT`s to appropriate exceptions, and turn the last `out` parameter into the return value. – Ray Apr 21 '22 at 12:24
13

You can use the Windows API Code Pack for Microsoft .NET Framework.

Reference: Microsoft.WindowsAPICodePack.Shell.dll

Need the following namespace:

using Microsoft.WindowsAPICodePack.Shell;

Simple usage:

string downloadsPath = KnownFolders.Downloads.Path;
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
0

The VB.Net function that I use is following

<DllImport("shell32.dll")>
Private Function SHGetKnownFolderPath _
    (<MarshalAs(UnmanagedType.LPStruct)> ByVal rfid As Guid _
    , ByVal dwFlags As UInt32 _
    , ByVal hToken As IntPtr _
    , ByRef pszPath As IntPtr
    ) As Int32
End Function

Public Function GetDownloadsFolder() As String

    Dim Result As String = ""
    Dim ppszPath As IntPtr
    Dim gGuid As Guid = New Guid("{374DE290-123F-4565-9164-39C4925E467B}")

    If SHGetKnownFolderPath(gGuid, 0, 0, ppszPath) = 0 Then
        Result = Marshal.PtrToStringUni(ppszPath)
        Marshal.FreeCoTaskMem(ppszPath)
    End If
    
   'as recommended by Ray (see comments below) 
    Marshal.FreeCoTaskMem(ppszPath)

    Return Result
End Function

In my program, I call it to move some CSV files in another folder.

    Dim sDownloadFolder = GetDownloadsFolder()
    Dim di = New DirectoryInfo(sDownloadFolder)

    'Move all CSV files that begin with BE in specific folder
    'that has been defined in a CONFIG file (variable: sExtractPath

    For Each fi As FileInfo In di.GetFiles("BE*.csv")
        Dim sFilename = sExtractPath & "\" & fi.Name
        File.Move(fi.FullName, sFilename)
    Next
schlebe
  • 3,387
  • 5
  • 37
  • 50
  • 1
    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:05
  • 1
    Does this mean that `Marshal.FreeCoTaskMem(ppszPath)` should be called outside the `If` block ? – schlebe Apr 20 '22 at 07:32
  • Yes, correct! Even if I've never seen the method return anything else than a null pointer / no memory on failure, I wouldn't rely on that, as it is an implementation detail MS may change any time. It is safe to call `Marshal.FreeCoTaskMem` on a null pointer anyway, so you can call it without thinking about it too much. Alternatively, you can P/Invoke to a `String` directly as mentioned to let the marshaller do this work, given you specify the `CharSet.Unicode` (and `ExactSpelling = True` to prevent lookup of a non-existent `W` method). – Ray Apr 20 '22 at 07:56
  • I've now posted further simplifications of the import [here](https://stackoverflow.com/a/71954266/777985), if you're interested. I think this is also supported in VB.NET. – Ray Apr 21 '22 at 12:25