Managing File Operations

Shell exploring is a fancy thing to do for a while, but it gets boring. In the end of the day you don't want to be a casual observer, you want to exercise your managerial skills. Managing the shell is all about moving things about, changing names, linking and all that. Time for those shell objects to Respect My Authorita! <g>

Topics: The Jack of all trades | Mind that moniker | Managing virtual reality

The Jack of all trades

As usual, managing regular filesystem files and folders is the easiest job, that even apprentice managers can tackle <g>. A single API SHFileOperation will copy, move, rename and delete, both files and folders, and even provide an animated mini-spectacle for your amusement while it is at it. Life can't get sweeter than that. Let's copy a couple of files then:
#include <shellapi.h>

SHFILEOPSTRUCT fop;

// initialize all data required for the copy
fop.hwnd = NULL; 
fop.wFunc = FO_COPY; 
fop.pFrom = "C:\\autoexec.bat\0C:\\config.sys\0"; 
fop.pTo = "A:\\";
fop.fFlags = FOF_ALLOWUNDO;
// remaining entries are output variables, no need to initialize

SHFileOperation(&fop); 

SHFileOperation takes only one argument, which contains all the relevant information in a SHFILEOPSTRUCT. The only thing in this code snippet that can be considered as remotely "tricky" is the construction of the pFrom string with the files to copy from. First note that full paths must be specified for all files. The pitfall with full paths strings in C++ that catches everybody off guard is the humble backslash. You need two of them ("\\") in string constants, else they are misinterpreted as escape sequences.

The second issue is multiple source files and how to separate them. The architects of SHFileOperation came up with a simple design: a large buffer that contains consecutive NULL-terminating strings to the individual files. The last file is identified by an extra zero byte; this way we don't need a separate argument for the number of files in the pFrom buffer. In the code above the \0 escape sequence is used to add extra zero bytes in the string. Note that even if you just want to copy a single file you'd still need to use an extra zero to terminate the string, e.g. "c:\\file.txt\0".

Other than that, managing files with SHFileOperation is pretty straightforward. You select what to do by setting the wFunc member. You can fine tune operations using various options for the fFlags member. All FOF_xxx constants have self-documenting names. I don't have much to say on the subject that the online docs won't cover. Some issues that perhaps need clarification follow:

Space Oddity: Connected HTML files
This is a new feature introduced in win2000, and not many people are aware of it. It allows HTML files to be connected to related files such as GIF images contained or style sheets. When you move or copy the HTML file, all of the files in the folder will be moved or copied as well. Conversely, if you move the folder with the related files, the HTML file is also moved. You create the connection to the related files by placing them in a subfolder of the folder containing the HTML file. The folder must have the name of the HTML file followed by " files". For example, if the HTML file is MyFile.htm, the folder should be named "MyFile files". Any files you place in the "MyFile files" subfolder will be connected to MyFile.htm. Personally I can't see this feature catching on. Any decent webpage has many HTML files which share GIF files, style sheets etc. It would be a sheer waste and administerial nightmare to create connected subfolders for each one of them. Rather half-baked this.

Reading through COM documentation is a real experience. Never mind the complication of the subject itself, the documenters have come up with various weird and wacky names that add to the intimidating experience. There's talk about marshalling, reconcilliators, and of course monikers. Funny name, innit? When I first came across IMoniker I thought that it was some horrible contagious venereal disease <g>. But of course, the truth turns out to be much more benign: monikers are a hype name for the familiar links to COM objects. It's all a plot to confuse the shareholders into believing we're doing top work for their money, see? <g>

Within the context of the shell namespace, monikers are pointers to other shell objects, known as links or shortcuts. Usually this takes the form of a small file with a .lnk extension, which contains the "address" (either full path or full PIDL) of the target object. Windows links are not as smart as UNIX links, but there is some form of object-orientedness in them. If you move the file pointed to by a link, the OS will try to relocate it, and update the link information if successful.

Shell links are managed with IShellLink interface. From a client perspective, many times you'd want to know whether an item is a link, and if so find out about it's target. The first part is easy: either test for a .lnk extension, or for increased robustness obtain the item's properties via GetAttributesOf and test for the SFGAO_LINK bit. If it tests positive for conjoined moniker-isitis syndrome <g> here's how you find out about its target object:

void ResolveLink(LPCTSTR szLinkFullPath)
{
   IShellLink* psl;
   // create a link manager object and request its interface
   HRESULT hr = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
                                   IID_IShellLink, (void**)&psl);

   // associate the manager object with the link file in hand
   IPersistFile* ppf; 
   // Get a pointer to the IPersistFile interface. 
   hr = psl->QueryInterface(IID_IPersistFile, (void**)&ppf);

   // full path string must be in Unicode.
   OLECHAR wsz[MAX_PATH];
   ::MultiByteToWideChar(CP_ACP, 0, szLinkFullPath, -1, wsz, MAX_PATH);

   // "load" the name and resove the link
   hr = ppf->Load(wsz, STGM_READ);
   hr = psl->Resolve(NULL, SLR_UPDATE);

   // Get the path to the link target.    
   WIN32_FIND_DATA ffd; // we get those free of charge
   TCHAR buf[MAX_PATH]; // could have simply reused 'wsz'...
   hr = psl->GetPath(buf, MAX_PATH, &ffd, 0);

   // not much to do, so just show some information
   cout << "Pointed item: " << buf << endl;
   cout << "Type: " << ( (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ?
                        "folder\n" : "file\n");

   // Release all interface pointers (both belong to the same object)
   ppf->Release();
   psl->Release(); 
}

Rather unintuitive, isn't it? You get the shortcut COM object by creating it directly using CoCreateInstance. Then instead of using some sensible method exported by IShellLink to specify the shortcut path name (like, say, SetShortcutPath or something), you need to query for the object's IPersistFile interface, and use it's Load method. That's poor object design, serving nowt else but confusion. It's not like as if some people would just be loading names to shortcut objects without using the main IShellLink functionality.

But never mind that, c'est la vie, non? <g>. After the object knows the whereabouts of the .lnk file, it attempts to Resolve it, i.e. figure out where the real shell item pointed to by the shortcut is located. After this step, you can use pretty much any other method exported by IShellLink to manage the shortcut. In the code above I used GetPath to find out the location of the target, which incidentally filled in a WIN32_FIND_DATA struct, like the one returned by FindFirstFile.

Note that if the shortcut pointed to some virtual shell item, as for instance some applet in the Control Panel folder, then GetPath wouldn't be of much use. An interesting detail is that the return code in such a doomed attempt would be the odd S_FALSE. Think about it folks: that means "success and false". Talking about contradiction in terms! This is only surpassed by the proverbial oxymoron "I am a liar" <g>. The morale here is that you should always be careful interpreting the SUCCEEDED macro. The appropriate action in this case would be to use GetIDList which would return the PIDL to the control panel applet. All shell items have a PIDL whereas paths are limited to pure filesystem objects.

IShellLink has various Getxxx and Setxxx methods that control pretty much anything you'd see in a shortcut's properties page. Having said that, there's one exception: shortcut descriptions. There is this method SetDescription that could be used to set an infotip or comment for a link file, but there's no standard way for shell users to set this information. Which is all rather silly. I'd put an extra field in the properties page to make this useful concept easy to manage.

ADVANCED
Shell shortcuts go a long way towards the object-oriented filesystem paradigm. Much of the information associated with the link is actually taken from the target file. Hence, if you request an IDropTarget pointer for a link (assuming it has one, e.g. if it is a link to some folder), then Droping stuff in it will result in them being copied in the correct target directory. We'll be looking at drag/drop operations in more detail later.

ADVANCED: Managing virtual reality

There's no generic way to do file operations in non-filesystem folders. SHFileOperation and traditional copying/moving/etc go out of the window. The only straightforward thing one could possibly do is use SetNameOf to rename items in the virtual folder, and that only if they advertise the SFGAO_CANRENAME attribute.

A couple of indirect ways to do some sort of "operations" are via the context menu, and the drag/drop interfaces. For instance, you could obtain an IContextMenu pointer for a bunch of items, if available, and execute standard verbs on them like "copy", "delete", etc. You can even obtain the folder's IContextMenu and try invoking the "paste" verb on it, after filling the clipboard with shell object data. More information on working with context menus will be given in a following section.

The problem with such an approach is that the folder may not contain any data that can be regarded in the traditional "file" sense. However, there's one interesting case: archive files, like CAB, ZIP, etc. There are namespace extensions that will allow such archives to be browsed as folders, offering access to the file information packed within them. If the extension implements IDataObject and includes a CFSTR_FILECONTENTS format for transferring it's objects, then it's pretty much like working with regular files. Just compare the contained FILEDESCRIPTOR with a regular WIN32_FIND_DATA structure to see what I mean. The only difference is how you access the "file", which obviously cannot happen the filesystem way — it's packed somewhere in the archive. Still once you have a data object exposing CFSTR_FILECONTENTS, you can usually access the data in a file-like way via IStream, the COM equivalent of a file. That's how you can extract real files out of them.

Likewise, if the archive namespace extension exports IDropTarget and can accept any of the standard shell clipboard formats, you can copy files into such a folder. I won't get into any details here, because shell data objects and drag/drop will be discussed properly in a following section. This is just an appetizer, but just think about the possible applications folks. You can do everything you could do with regular filesystem folders — and I mean everything. If you need inspirations, just watch out for a future 2xExplorer in a screen near you <g>

Additional information

Q182820 - DOC: Using Wildcard Extension with SHFileOperation
Q186152 - PRB: File Names Truncated When Using SHFileOperation
Q235586 - BUG: "Empty Recycle Bin" Unavailable After Deletion by SHFileOperation




Namespace exploring Shell information Contents