|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionAs programmers we often find ourselves having to deal with filenames and to perform various operations on the filenames. Some years ago I found myself repeatedly writing code that received a filename and needed to perform some simple transformation such as changing the extension. After about the fifteenth function that contained code such as the following
TCHAR tszDrive[_MAX_DRIVE],
tszPath[_MAX_PATH],
tszFilename[_MAX_FNAME],
tszExtension[_MAX_EXT];
CString csNewPath;
_tsplitpath(tszFile, tszDrive, tszPath, tszFilename, tszExtension);
.
.
.
csNewPath = tszDrive;
csNewPath += tszPath + tszFilename;
csNewPath += someotherextension;
I realised that much of the work could be encapsulated in a class.
BackgroundYou may recognise this class. It was first published on CodeGuru[^] but it's been extended since those days (August 1998) and a few bugs have been fixed. The class now understands Unicode. The class encapsulates the basic idea of a filename whilst catering for UNC formatted filenames. It can also convert a filename specified, using drive letters on the local machine, into UNC format. How it does this depends on whether the drive portion of the filename refers to a local drive or a mapped network drive (I discuss this later in this article). A filename has at least two forms. The first form is (for example) c:\seti\01\seti.exeYou can break this down into four pieces.
CFileSpec understands this format.
The other form that this class understands is a UNC filename of the form \\Rob\C\seti\01\seti.exewhere
The class interfaceThe class definition looks like this.
class CFileSpec
{
public:
enum FS_BUILTINS
{
FS_EMPTY, // Nothing
FS_APP, // Full application path and name
FS_APPDIR, // Application folder
FS_WINDIR, // Windows folder
FS_SYSDIR, // System folder
FS_TMPDIR, // Temporary folder
FS_DESKTOP, // Desktop folder
FS_FAVOURITES, // Favourites folder
FS_MEDIA, // Default media folder
FS_CURRDIR, // Current folder
FS_TEMPNAME // Create a temporary name
};
CFileSpec(FS_BUILTINS eSpec = FS_EMPTY);
CFileSpec(FS_BUILTINS eSpec, LPCTSTR szFileame);
CFileSpec(LPCTSTR szSpec, LPCTSTR szFilename);
CFileSpec(LPCTSTR szFilename);
// Operations
BOOL Exists() const;
BOOL IsUNCPath() const;
BOOL LoadArchive(CObject *pObj) const;
BOOL SaveArchive(CObject *pObj) const;
// Access functions
CString& Drive() { return m_csDrive; }
CString& Path() { return m_csPath; }
CString& FileName() { return m_csFilename; }
CString& Extension() { return m_csExtension; }
const CString FullPathNoExtension() const;
const CString GetFolder() const;
const CString GetFullSpec() const;
const CString GetFileName() const;
const CString ConvertToUNCPath() const;
void SetFullSpec(LPCTSTR szSpec);
void SetFullSpec(FS_BUILTINS eSpec = FS_EMPTY);
void SetFileName(LPCTSTR szSpec);
void Initialise(FS_BUILTINS eSpec);
private:
BOOL IsUNCPath(LPCTSTR szPath) const;
void WriteAble() const;
void ReadOnly() const;
void GetShellFolder(int iFolder);
CString m_csDrive,
m_csPath,
m_csFilename,
m_csExtension;
};
Let's ignore the FS_BUILTINS enum for now (and all constructors using that enum) and look at the constructors that take a string or two.
The simplest constructor takes a single string which is a filename and path. The constructor simply deconstructs the string into it's constituent parts via a call
to You can then use the member functions to retrieve the drive, the path, the filename, the extension, or the entire path. You can also change any of those constituent parts and expect the class to do reasonable things after the change. For example
CFileSpec fs(_T("c:\seti\01\seti.exe"));
fs.Extension() = _T("dat");
printf(_T("%s\n"), fs.GetFullSpec());
would print
c:\seti\01\seti.datshowing that the extension was changed. Note well that I said '(if present)' earlier. The class is perfectly capable of handing filenames that don't start at the top of the filename namespace and it doesn't care if it's a UNC path or not. (Though if it's a UNC path by definition it will be rooted at the top of the filename namespace). So now we've got a filename parsed into it's component parts. Big deal. The power of the class comes from what you can do once you've got an instance of the class.
Suppose your application has an initialisation file stored in the same directory as the exe? How to get at it? You can use
TCHAR tszDrive[_MAX_DRIVE],
tszPath[_MAX_PATH],
tszFile[_MAX_PATH];
CString csNewPath;
GetModuleFileName(NULL, tszFile, _MAX_PATH);
_tsplitpath(tszFile, tszDrive, tszPath, NULL, NULL);
csNewPath = tszDrive + tszPath;
csNewPath += _T("InitialisationFile.ext");
Or you could do this.
CFileSpec fs(CFileSpec::FS_APPDIR);
fs.SetFileNameEx(_T("InitialisationFile.ext");
LoadMyInitFile(fs);
or even this
CFileSpec fs(CFileSpec::FS_APPDIR, _T("InitialisationFile.ext"));
LoadMyInitFile(fs);
The same work is performed but you have to write rather less code.
Which leads into a discussion of the
If you examine the source code posted on CodeGuru 5 years ago you'll see that I used the Windows Registry to obtain the paths for the users desktop and favourites folders. Well it turns out that according to Raymond Chen[^], this is evil, so I changed the class to use the recommended method, So far it's a pretty uninteresting though possibly useful class. Where it becomes interesting is in two features. UNC ConversionElaine is working with an app that refers to data through driveF:. She doesn't know (and doesn't care) that her machine doesn't have a physical drive F:.
Somehow, through the magic of drive mapping, a folder on the server called \\Roger\CPData is mapped to drive F: on her machine.
Everything works as it should and, apart from network throughput issues, the world is a shining place. When Elaine closes her app it saves a reference to the drive
it was using.
Somewhat later, Ian logs on to the same machine. He opens the same application and it, using the saved path, attempts to access the same files that Elaine
was using. If Ian and Elaine have the same mapped drives it all works. But woe if Elaine mas mapped drive
You call the member function
const CString CFileSpec::ConvertToUNCPath() const
{
USES_CONVERSION;
CString csPath = GetFullSpec();
if (IsUNCPath(csPath))
return csPath;
if (csPath[1] == ':')
{
// Fully qualified pathname including a drive letter, check if it's a
// mapped drive
UINT uiDriveType = GetDriveType(m_csDrive);
if (uiDriveType & DRIVE_REMOTE)
{
// Yup - it's mapped so convert to a UNC path...
TCHAR tszTemp[_MAX_PATH];
UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp;
DWORD dwSize = _MAX_PATH;
DWORD dwRet = WNetGetUniversalName(m_csDrive,
REMOTE_NAME_INFO_LEVEL, uncName,
&dwSize);
CString csDBShare;
if (dwRet == NO_ERROR)
return uncName->lpUniversalName + m_csPath + m_csFilename
+ m_csExtension;
}
else
{
// It's a local drive so search for a share to it...
NET_API_STATUS res;
PSHARE_INFO_502 BufPtr,
p;
DWORD er = 0,
tr = 0,
resume = 0,
i;
int iBestMatch = 0;
CString csTemp,
csTempDrive,
csBestMatch;
do
{
res = NetShareEnum(NULL, 502, (LPBYTE *) &BufPtr, DWORD(-1), &er, &tr,
&resume);
//
// If the call succeeds,
//
if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
{
csTempDrive = GetFolder();
csTempDrive.MakeLower();
p = BufPtr;
//
// Loop through the entries;
//
for (i = 1; i <= er; i++)
{
if (p->shi502_type == STYPE_DISKTREE)
{
csTemp = W2A((LPWSTR) p->shi502_path);
csTemp.MakeLower();
if (csTempDrive.Find(csTemp) == 0)
{
// We found a match
if (iBestMatch < csTemp.GetLength())
{
iBestMatch = csTemp.GetLength();
csBestMatch = W2A((LPWSTR) p->shi502_netname);
}
}
}
p++;
}
//
// Free the allocated buffer.
//
NetApiBufferFree(BufPtr);
if (iBestMatch)
{
TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dwBufLen = countof(tszComputerName);
csTemp = GetFolder();
csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch + 1);
GetComputerName(tszComputerName, &dwBufLen);
csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName,
csBestMatch, csTemp,
m_csFilename, m_csExtension);
}
}
else
TRACE(_T("Error: %ld\n"), res);
// Continue to call NetShareEnum while
// there are more entries.
//
} while (res == ERROR_MORE_DATA); // end do
}
}
return csPath;
}
The function is const meaning that it doesn't modify the underlying CFileSpec object. It merely converts the path represented by that
object into a UNC path (if possible). Either way it returns a CString.
The function first checks if the object already contains a UNC path. If so it returns the path. If not, the function then checks if the object contains a path rooted to the file system namespace. Ie, does it contain a drive value. That's done by the line if (csPath[1] == ':')If this test fails we simply return the path the object represents (no conversion possible).
Assuming the test passes we then have two possibilities. The path may be on a share or it may be on local storage. The simpler case is when the path is on a share
on another machine (ie, it's on a mapped drive). We get the drive type using the
// Yup - it's mapped so convert to a UNC path...
TCHAR tszTemp[_MAX_PATH];
UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp;
DWORD dwSize = _MAX_PATH;
DWORD dwRet = WNetGetUniversalName(m_csDrive, REMOTE_NAME_INFO_LEVEL,
uncName, &dwSize);
CString csDBShare;
if (dwRet == NO_ERROR)
return uncName->lpUniversalName + m_csPath + m_csFilename + m_csExtension;
This is pretty simple. We call the WNetGetUniversalName() API passing the drive name (c:, d: etc) and, if the API succeeds, we get back a server/share
name to which we append the remainder of the path. If the API fails (for whatever reason), the UNC conversion method returns the untranslated path.
Where it gets interesting is when the path refers to a drive on the local machine. In this case we want to search for a share on this machine that will be reachable from another machine.
To do this we need to enumerate shares. Naturally it's not quite that simple. Shares can be print queues, disk drives or IPC channels. Our code needs to ignore any
share except drive shares. We do this by checking the share type for each share that's enumerated. Pretty simple. We set up the enum using the
Now things get complicated. There may be more than one share on your machine that can reach the path. For example, you may have a share for This is done in this code
if (iBestMatch)
{
TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dwBufLen = countof(tszComputerName);
csTemp = GetFolder();
csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch);
GetComputerName(tszComputerName, &dwBufLen);
csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName, csBestMatch, csTemp,
m_csFilename, m_csExtension);
}
We delete that part of the path that is contained in the share. Eg, if the share on machine Rob is called seti and represents
c:\seti on that machine, and the path is c:\seti\setisrv.exe then the returned UNC path will be \\Rob\seti\setisrv.exe.
A gotchaIf you've examined the code carefully you might be puzzled by this line.
csTemp = W2A((LPWSTR) p->shi502_path);
You'll have noticed that the function uses the USES_CONVERSION macro. This macro sets up some local variables to allow for Unicode to MBCS/ANSI conversion
within the function (and vice versa). If you know about USES_CONVERSION then you probably also know about the W2A macro. This macro does a
conversion from Wide to MBCS/ANSI, ie, from Unicode to MBCS/ANSI. The macro, if compiled within an ANSI/MBCS application, expects to see a Unicode input string
(LPWSTR). Hmmm, so the example project included isn't Unicode. Why then the cast to LPWSTR? Well it turns out that in the version of the PSDK I have on
this machine, the SHARE_INFO_502 structure defines it's member variables as LPSTR's if it's an ANSI/MBCS build and as LPWSTR's
if it's a Unicode build. Sounds good except for one thing. Regardless of how the build is done the OS returns Unicode strings! (Win2k SP4). If you've done an
MBCS/ANSI build the code will return the wrong path unless you do the cast and use the W2A macro. So even though the compiler imagines that in an
ANSI/MBCS build those members are ANSI/MBCS, they're really Unicode. But the W2A macro complains if you pass it a non LPWSTR argument.
Hence the casts. This cost me a few minutes of head scratching.
Serialisation supportLots of people don't like MFC's serialisation support. I've yet to see a convincing argument against it so this class contains support for it. This support wasn't a part of the original class. I just noticed that I was writing a lot of similar code so I added it and lost the need to write pretty much the same code for every new application I write.open a file create an archive object attach the file to the archive serialise the object through the archive detach the file from the archive close the archive close the fileIt seemed to me that adding MFC archive support to this class was a no brainer - and it was. You've seen the functions defined above in the class header. Here's the code.
BOOL CFilename::LoadArchive(CObject *pObj) const
{
CFile file;
BOOL bStatus = FALSE;
ASSERT(pObj);
ASSERT_VALID(pObj);
ASSERT_KINDOF(CObject, pObj);
ASSERT(pObj->IsSerializable());
if (Exists())
{
try
{
if (file.Open(GetFullSpec(), CFile::modeRead | CFile::typeBinary
| CFile::shareExclusive))
{
CArchive ar(&file, CArchive::load);
pObj->Serialize(ar);
ar.Close();
file.Close();
bStatus = TRUE;
}
}
catch(CException *e)
{
e->Delete();
}
}
return bStatus;
}
Simple code. Check if the file exists. If it does, open it, attach an archive object to the file object and then serialize it into the object pointer passed. Naturally
the object pointed to must support serialisation, hence the debug checks. It's almost exactly the same code for saving an archive.
The Demo ApplicationThe demo application does some simple conversions of paths to translated paths. It also shows the member variables of the underlyingCFileSpec object.
The examples shown here demonstrate how the class converts from a reference to my wifes machine (f:\seti\setispy.ini) where f: is mapped to \\Suzy\C.
AcknowlegementsThe sample project uses a class,CFileEditCtrl, written by P J Arends. An excellent class found here.
HistoryDecember 24 2003, Initial CodeProject version. December 24 2003, Fixed a bug noted by PJ Arends. December 27 2003, Fixed a bug noted by Mike Dunn. | ||||||||||||||||||||