views:

46

answers:

2

i want to to persist some filenames for the user (e.g. recent files).

Let's use six example files:

  • c:\Documents & Settings\Ian\My Documents\Budget.xls
  • c:\Documents & Settings\Ian\My Documents\My Pictures\Daughter's Winning Goal.jpg
  • c:\Documents & Settings\Ian\Application Data\uTorrent
  • c:\Documents & Settings\All Users\Application Data\Consonto\SpellcheckDictionary.dat
  • c:\Develop\readme.txt
  • c:\Program Files\Adobe\Reader\WhatsNew.txt

i'm now hard-coding path to special folders. If the user redirects their folders, roams to another computer, or upgrades their operating system, the paths will be broken:

i want to be a good developer, and convert these hard-coded absolute paths to relative paths from the appropriate special folders:

  • %CSIDL_Personal%\Budget.xls
  • %CSIDL_MyPictures%\Daughter's Winning Goal.jpg
  • %CSIDL_AppData%\uTorrent
  • %CSIDL_Common_AppData%\Consonto\SpellcheckDictionary.dat
  • c:\Develop\readme.txt
  • %CSIDL_Program_Files%\Adobe\Reader\WhatsNew.txt

The difficulty comes with the fact that there can be multiple representations for the same file, e.g.:

  • c:\Documents & Settings\Ian\My Documents\My Pictures\Daughter's Winning Goal.jpg
  • %CSIDL_Profile%\My Documents\My Pictures\Daughter's Winning Goal.jpg
  • %CSIDL_Personal%\My Pictures\Daughter's Winning Goal.jpg
  • %CSIDL_MyPictures%\Daughter's Winning Goal.jpg

Note also that in Windows XP My Pictures are stored in My Documents:

%CSIDL_Profile%\My Documents
%CSIDL_Profile%\My Documents\My Pictures

But on Vista/7 they are separate:

%CSIDL_Profile%\Documents
%CSIDL_Profile%\Pictures

Note: i realize the syntax %CSIDL_xxx%\filename.ext is not valid; that Windows will not expand those keywords like they are environment strings. i'm only using it as a way to ask this question. Internally i would obviously store the items some other way, perhaps as a CSIDL parent and the tail of the path, e.g.:

 CSIDL_Personal         \Budget.xls
 CSIDL_MyPictures       \Daughter's Winning Goal.jpg
 CSIDL_AppData          \uTorrent
 CSIDL_Common_AppData   \Consonto\SpellcheckDictionary.dat
 -1                     c:\Develop\readme.txt   (-1, since 0 is a valid csidl)
 CSIDL_Program_Files    \Adobe\Reader\WhatsNew.txt

The question becomes, how to use, as much as possible, paths relative to canonical special folders?


I'm thinking:

void CanonicalizeSpecialPath(String path, ref CSLID cslid, ref String relativePath)
{
   return "todo";
}

See also

+1  A: 

I suppose you could find out how the CSIDL map to paths (using something like SHGetKnownFolderPath), build a reverse dictionary of them, then check whether the beginning of the path you want to store matches any of the keys in the dictionary and then remove the beginning and store the CSIDL that matched.

Not overtly elegant, but it should get the work done.

dguaraglia
What was, of course, my first thought. But there's the problem of finding the "best" root CSIDL
Ian Boyd
You've probably spent more time thinking about this than I have, but my instinctive answer is "the most specific one", which effectively translates to the longest common prefix. You and I might think of "My Pictures" as being a subdirectory of "My Documents" (on XP, anyway) but the kind of user you're (commendably) going out of your way to help with this canonicalisation thinks of it as "the place I put my pictures". What if "My Pictures" gets redirected? A user looks in "My Pictures" in Explorer and sees their document. Your code looks in `%CSIDL_Personal%\My Pictures` and doesn't.
shambulator
This is what i ended up doing. The "longest" matching prefix is the one i use. Code posted in another answer.
Ian Boyd
+1  A: 
function CanonicalizeSpecialPath(const path: string): string;
var
    s: string;
    BestPrefix: string;
    BestCSIDL: Integer;
    i: Integer;
begin
    BestPrefix := ''; //Start with no csidl being the one
    BestCSIDL := 0;

    //Iterate over the csidls i know about today for Windows XP.    
    for i := Low(csidls) to High(csidls) do
    begin
       //Get the path of this csidl. If the OS doesn't understand it, it returns blank
       s := GetSpecialFolderPath(0, i, False);
       if s = '' then
          Continue;

       //Don't do a string search unless this candidate is larger than what we have
       if (BestPrefix='') or (Length(s) > Length(BestPrefix)) then
       begin
          //The special path must be at the start of our string
          if Pos(s, Path) = 1 then //1=start
          begin
             //This is the best csidl we have so far
             BestPrefix := s;
             BestCSIDL := i;
          end;
       end;
    end;

    //If we found nothing useful, then return the original string
    if BestPrefix = '' then
    begin
       Result := Path;
       Exit;
    end;

    {
       Return the canonicalized path as pseudo-environment string, e.g.:

           %CSIDL_PERSONAL%\4th quarter.xls
    }
    Result := '%'+CsidlToStr(BestCSIDL)+'%'+Copy(Path, Length(BestPrefix)+1, MaxInt);
end;

And then there's a function that "expands" the special environment keywords:

function ExpandSpecialPath(const path: string): string;
begin
   ...
end;

which expands:

%CSIDL_PERSONAL%\4th quarter.xls

into

\\RoamingProfileServ\Users\ian\My Documents\4th quarter.xls

It does it by looking for %xx% at the start of the string, and expanding it.

Ian Boyd