Finding a Windows user's "true" application data folder?
Asked Answered
D

3

10

I have a Delphi 6 application that, like most Windows applications, reads/writes data to the user's "local application data" folder. I use the code below to determine that folder. Up until now, that code worked for most of my users. I have encountered a user whose local application data is not in the expected folder:

C:\Users\Bob\AppData\Roaming\

Usually the local app data folder resolves to:

C:\Documents and Settings\Bob\Application Data\

What is odd about this user's particular situation is that several registry keys normally found in HKEY_LOCAL_MACHINE are actually located in HKEY_CURRENT_USER. They are running on Windows 7.

For lack of a better word, is there a way to get the "true" application data for a user so I can navigate this situation better? If it's a matter of intelligently choosing between the CSIDL_APPDATA, CSIDL_COMMON_APPDATA and CSIDL_LOCAL_APPDATA special folders, what is the logic for doing so? As you can tell I'm looking for an all-purpose function that can root out the correct application data folder regardless of the version of Windows the user is running or their specific PC configuration.

I found this Stack Overflow post that seems to have the answer but it is using function from the .NET library and I am using Delphi 6. If this solution answers my question, can someone tell me a quick way to replicate it in Delphi:

How can i get the path of the current user's "Application Data" folder?

// Function to get the app data special folder.
function GetAppdataFolder: string;
begin
   Result := GetSpecialFolderLocation(CSIDL_APPDATA);
end;
Dorian answered 7/10, 2012 at 18:18 Comment(2)
@SertacAkyuz - it turned out that the real cause of the problem was that the user needed to install my program with admin rights, something that does not happen on most user's systems. To be fair to those who had already answered the original post, I reverted the post to the original form and created a new post for the new question: #12773115Dorian
Where is GetSpecialFolderLocation ? (Which unit should I add to my uses clause?)Buonarroti
L
8

The .net code you link to uses Environment.SpecialFolder.ApplicationData which is exactly the same as CSIDL_APPDATA. So your code is already equivalent to the .net code to which you link. And these both refer to the same location as FOLDERID_RoamingAppData.

Take a look at the documentation for FOLDERID_RoamingAppData. It says:

Default Path        %APPDATA% (%USERPROFILE%\AppData\Roaming)
Legacy Default Path %APPDATA% (%USERPROFILE%\Application Data) 

The "Default Path" is what you will see on Vista or later. The "Legacy Path" is what you see on XP.

The different behaviour that you have observed is nothing more than the expected difference between XP and Vista/7/8.

On my Windows machine,

Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)

evaluates to

C:\Users\heff\AppData\Roaming

In other words, your code is already doing the right thing. You do not need to make any changes to it at all. Carry on using GetSpecialFolderLocation(CSIDL_APPDATA).


What is odd about this user's particular situation is that several registry keys normally found in HKEY_LOCAL_MACHINE are actually located in HKEY_CURRENT_USER.

That's not uncommon. Quite often applications configure default settings in HKLM and then copy them to HKCU when the application is first run. Without knowing more details of the settings in question it's hard to comment on that aspect of your question.

Leery answered 7/10, 2012 at 18:42 Comment(6)
I answered the question you asked. Your update is perhaps related to UAC which is not present on XP. I think it's not fair to completely change the question like this. I put a lot of effort into this answer. I think you should revert the question, accept the answer and ask a new question. With lots of details that are not here.Leery
I reverted my post to the original form before I introduced the admin rights issue as you requested.Dorian
That was just a simple mistake. I meant to accept your answer originally. It's fixed.Dorian
@SertacAkyuz - I see. I inadvertently clipped out the code snippet when I clipped the text I used to start a new thread. I can see how that is confusing. Sorry about that. It's restored.Dorian
@Robert - So it was a mistake.. I'm really sorry for taking it on conscious. I'm taking back my downvote and will delete all my comments in a while.Mithridatism
@David: For the record: we do receive crash reports from Vista/7 computers where "ShGetFolderPath(0, CSIDL_APPDATA, 0, shgfp_Type_Current, Path)" actually does return "C:\Users\heff\Application Data" instead of "C:\Users\heff\AppData\Roaming". We can't write to that folder and our program crashes because of that.Package
U
5

You can use this (a wrapper). You'll need to add ShlApi to your uses clause. Pass it CSIDL_APPDATA just like your sample above does. For a list of the various CSIDL_ values, see the MSDN page here

function GetShellFolder(CSIDLFolder : integer) : string;
begin
  SetLength(Result, MAX_PATH);
  SHGetSpecialFolderPath(0, PChar(Result), CSIDLFolder, false);
  SetLength(Result, StrLen(PChar(Result)));
  if (Result <> '') then
    Result  := IncludeTrailingBackslash(Result);
end;

If you're supporting earlier of Windows (XP and below), which your text appears is the case, you can use SHGetFolderPath instead:

function GetFolderPath(Wnd: HWnd; CSIDLFolder: Integer): string;
begin
  SetLength(Result, MAX_PATH);
  Result := SHGetFolderPath(Wnd, CSIDLFolder, nil, 0, PChar(Result);
  SetLength(Result, StrLen(PChar(Result)));
end;

If you're only supporting Vista and higher, you should use SHGetKnownFolderPath instead, and pass it a KNOWNFOLDERID.

As far as the registry issue, Windows Vista and 7 are much more restrictive about the places a non-Admin user can write to, and one of the places that occurs is in HKLM and HKCR. Many of the items that used to be in those hives are now in HKCU, or are mirrored there.

Universal answered 7/10, 2012 at 18:41 Comment(4)
Isn't the question about which CSIDL to use rather than how to convert that to a path? It looks to me as though Robert already knows how to convert a CSIDL into a path. Or am I reading it incorrectly?Leery
I think you're reading it incorrectly. His text seems to indicate that he's not reading the path (he's presuming the location), and therefore is having trouble with a user on Win7 where the data is in the wrong place. He says he's not able to use the solution he found because it's from .NET and he's using Delphi.Universal
I read the code in the question as being the code that is currently being used. And then there's the references to CSIDL_COMMON_APPDATA and CSIDL_LOCAL_APPDATA. Robert is asking which to use. At least that's how I read it. I think Robert is using GetSpecialFolderLocation from the JEDI library. I think Robert is using an XP machine and doesn't recognise the re-organisation of profile folders that MS made in Vista. He thinks that the different paths indicates something more than that.Leery
@KenWhite. Thank you. Please see my recent update in the original post.Dorian
U
5

If it's a matter of intelligently choosing between the CSIDL_APPDATA, CSIDL_COMMON_APPDATA and CSIDL_LOCAL_APPDATA special folders, what is the logic for doing so?

Yes, it is just a matter of that. Your code is already working as expected.

CSIDL_APPDATA (FOLDERID_RoamingAppData) is for data that is accessible to the calling thread's current user account (which can be impersonated) on multiple machines (hense "roaming" data).

CSIDL_LOCAL_APPDATA (FOLDERID_LocalAppData) is for data that is accessible to the calling thread's current user account on the local machine only (hense "local" data).

CSIDL_COMMON_APPDATA (FOLDERID_ProgramData) is for data that is accessible to any user account on the local machine only (not "roaming" data).

Unparliamentary answered 8/10, 2012 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.