Extending Directory()

Copyright (C) 1997, Paul Piko

Download source code: EXTDIR.MEF (8K)

Introduction

The CAVO20 forum on Compuserve is an excellent venue for discussing issues relating to CA-Visual Objects 2.0. Recently some people in the forum reported that the Directory() function was not returning all the names of the files that it should have. In response to that, I started investigating the possibility of using some of the Windows API functions to provide an alternative. This article explains the solution I came up with, and shows how to include additional information not normally provided by Directory().

Windows API Functions

There are two main functions in the Windows API that provide the basis for our replacement directory function: FindFirstFile() and FindNextFile().

 

The FindFirstFile() function accepts two parameters, a PSZ containing the required file specification (including wildcards, if necessary) and a pointer to a block of memory that will be filled with information about the first file matching the specification. FindFirstFile() returns a "search handle", which is a value that is passed to the companion FindNextFile() function. Again, FindNextFile takes a second parameter that is a pointer to a block of memory to be filled with details about the next file that is found.

In VO2, the structure of the memory containing the file details is called _WINWIN32_FIND_DATA, listed below:

STRUCT _WINWIN32_FIND_DATA

MEMBER dwFileAttributes AS DWORD

MEMBER ftCreationTime IS _WINFILETIME

MEMBER ftLastAccessTime IS _WINFILETIME

MEMBER ftLastWriteTime IS _WINFILETIME

MEMBER nFileSizeHigh AS DWORD

MEMBER nFileSizeLow AS DWORD

MEMBER dwReserved0 AS DWORD

MEMBER dwReserved1 AS DWORD

MEMBER DIM cFileName[ MAX_PATH ] AS BYTE

MEMBER DIM cAlternateFileName[ 14 ] AS BYTE

 

On examination of this structure, we find that we not only have the necessary information to derive the values normally returned by Directory(), but there are also additional members that we can make use of such as cFileName, which contains the long filename, ftCreationTime which contains details of when the file was created, and ftLastAccessTime, containing details of when the file was last accessed. And looking deeper into the dwFileAttributes member reveals there are additonal file attribute flags, such as COMPRESSED, OFFLINE and TEMPORARY.

Having found this information in the Windows API, we see that it is possible not only to match the functionality of Directory(), but to extend it as well.

ExtendedDirectory()

I decided to try to make our new function similar to Directory(), but not exactly the same. I have deliberately left out the disk volume information - in fact that might be a project for another day, since the Windows API function GetVolumeInformation() has additional information not normally provided by Directory().

The skeleton of our new function can be as follows:

function ExtendedDirectory(cFileSpec,uAttributes)

LOCAL pFindData IS _WINWIN32_FINDDATA

LOCAL hSearch AS PTR

LOCAL aFiles := {} AS ARRAY

 

hSearch := FindFirstFile( PSZ(cFileSpec), @pFindData )

 

IF ! hSearch == INVALID_HANDLE_VALUE

aThisFile := ConvertFindData( @pFindData )

aadd( aFiles, aThisFile )

 

DO WHILE FindNextFile( hSearch, @pFindData )

aThisFile := ConvertFindData( @pFindData )

aadd( aFiles, aThisFile )

ENDDO

ENDIF

 

FindClose( hSearch )

 

RETURN aFiles

 

This code simply sets up a block of memory to contain the file details (by declaring pFindData IS _WINWIN32_FIND_DATA), and attempts to find the first matching file by calling FindFirstFile(). If that is successful, it adds the information to an array by calling ConvertFindData() (more about that below), and then keeps looking for further files by looping and calling FindNextFile(). As long as further files are found the structure information is again analysed by ConvertFindData(), and the results added to the array.

Something more familiarů

The ConvertFindData() function takes the _WINWIN32_FIND_DATA structure and converts the information into a format that is more familiar and friendly to VO programmers. Since the Directory() function returns an array for each file found, I thought we would do the same, but extend it to include the additional information we have at hand.

Each piece of information in the structure needs some type of manipulation to get it into our VO style format. Let's start with the filename.

The structure contains both the long version of the filename and the short (8.3) version. The cFilename member contains the long filename, and cAlternateFilename contains the short filename. We can convert both of these into VO strings as shown below:

cLongName := PSZ2String( PSZ( _CAST, @pFindData.cFilename ) )

cShortName := PSZ2String( PSZ( _CAST, @pFindData.cAlternateFilename ) )

 

The size of the file can be calculated from the nFileSizeHigh and nFileSizeLow members:

nSize := (pFindData.nFileSizeHigh * MAXDWORD ) + pFindData.nFileSizeLow

 

The time related members, ftLastWriteTime, ftCreationTime, and ftLastAccessTime are actually formatted in Coordinated Universal Time (UTC) format. Windows provides the FileTimeToLocalFileTime() function to convert these into local time, and then FileTimeToSystemTime() to break the time into year, month, day, hour, minute, second, etc. To simplify the process of converting these values, I have come up with the UTC2DateTime() function, which will take a UTC value and return an array containing the corresponding date and time. The code for this function is included below:

 

FUNCTION UTC2DateTime(pFileTime AS _WINFILETIME) AS ARRAY

LOCAL pLocalFileTime IS _WINFILETIME

LOCAL pSystemTime IS _WINSYSTEMTIME

LOCAL dSystemDate AS DATE

LOCAL cSystemTime AS STRING

FileTimeToLocalFileTime( pFileTime, @pLocalFileTime )

FileTimeToSystemTime( @pLocalFileTime, @pSystemTime )

dSystemDate := SToD(PSZ( PadL(pSystemTime.wYear,4,"0") + ;

PadL(pSystemTime.wMonth,2,"0") + ;

PadL(pSystemTime.wDay,2,"0") ) )

 

cSystemTime := TString( pSystemTime.wHour*60*60 + ;

pSystemTime.wMinute*60 + ;

pSystemTime.wSecond + ;

pSystemTime.wMilliseconds / 1000 )

 

RETURN { dSystemDate, cSystemTime }

 

 

With this function, we can easily convert each of the time related members by following the technique listed in these few lines of code:

 

aDateTime := UTC2DateTime(@pFindData.ftLastWriteTime)

dWriteDate] := aDateTime[1]

cWriteTime := aDateTime[2]

 

All that remains is to convert the file attributes, which are stored in a DWORD, into a character string like that provided by directory. This can be achieved by using the following function:

 

FUNCTION dwAttribute2String(dwAttrib AS DWORD) AS STRING

LOCAL cAttributes AS STRING

IF _and(dwAttrib,FILE_ATTRIBUTE_COMPRESSED) > 0

cAttributes += "C"

ENDIF

IF _and(dwAttrib,FILE_ATTRIBUTE_OFFLINE) > 0

cAttributes += "O"

ENDIF

IF _and(dwAttrib,FILE_ATTRIBUTE_TEMPORARY) > 0

cAttributes += "T"

ENDIF

 

IF _and(dwAttrib,FILE_ATTRIBUTE_ARCHIVE) > 0

cAttributes += "A"

ENDIF

IF _and(dwAttrib,FILE_ATTRIBUTE_DIRECTORY) > 0

cAttributes += "D"

ENDIF

IF _and(dwAttrib,FILE_ATTRIBUTE_SYSTEM) > 0

cAttributes += "S"

ENDIF

 

IF _and(dwAttrib,FILE_ATTRIBUTE_HIDDEN) > 0

cAttributes += "H"

ENDIF

 

IF _and(dwAttrib,FILE_ATTRIBUTE_READONLY) > 0

cAttributes += "R"

ENDIF

 

RETURN cAttributes

 

Using all these components, the whole ConvertFindData() function can be written like this:

 

FUNCTION ConvertFindData(pFindData AS _WINWIN32_FIND_DATA) AS ARRAY

LOCAL aFile AS ARRAY

LOCAL aDateTime AS ARRAY

aFile := ArrayCreate(F_MAX)

aFile[F_NAME] := Psz2String(PSZ(_CAST,@pFindData.cFilename))

aFile[F_ATTR] := dwAttribute2String(pFindData.dwFileAttributes)

aFile[F_SIZE] := (pFindData.nFileSizeHigh * MAXDWORD) + ;

pFindData.nFileSizeLow

aFile[F_ATTRVALUE] := pFindData.dwFileAttributes

 

aDateTime := UTC2DateTime(@pFindData.ftLastWriteTime)

aFile[F_DATE] := aDateTime[1]

aFile[F_TIME] := aDateTime[2]

aDateTime := UTC2DateTime(@pFindData.ftCreationTime)

aFile[F_CREATEDATE] := aDateTime[1]

aFile[F_CREATETIME] := aDateTime[2]

aDateTime := UTC2DateTime(@pFindData.ftLastAccessTime)

aFile[F_ACCESSDATE] := aDateTime[1]

aFile[F_ACCESSTIME] := aDateTime[2]

aFile[F_SHORTNAME] := Psz2String(;

PSZ(_CAST,@pFindData.cAlternateFilename))

IF Empty(aFile[F_SHORTNAME])

aFile[F_SHORTNAME] := aFile[F_NAME]

ENDIF

RETURN aFile

 

We just need to have a few extra DEFINEs to extend the array that Directory normally provides:

DEFINE F_ACCESSDATE := 6

DEFINE F_ACCESSTIME := 7

DEFINE F_CREATEDATE := 8

DEFINE F_CREATETIME := 9

DEFINE F_SHORTNAME := 10

DEFINE F_ATTRVALUE := 11

DEFINE F_MAX := 11

 

Selecting which files to show

As written so far, ExtendedDirectory() returns all directory entries. However, Directory() normally does not include System or Hidden files, or Directories. The final thing we need to do is simulate this behaviour.

Directory() allows you to pass a seconds parameter containing the attributes of files to be included in the returned array. This parameter can be a numeric (e.g. FA_DIRECTORY) or a string (e.g. "D")

We can also do this with ExtendedDirectory(). The code below shows how we can convert a string into the corresponding DWORD in ExtendedDirectory()

 

DO CASE

CASE IsString(uAttributes)

FOR i := 1 TO SLen(uAttributes)

DO CASE

CASE Upper( SubStr3(uAttributes,i,1) ) == "D"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_DIRECTORY)

CASE Upper( SubStr3(uAttributes,i,1) ) == "S"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_SYSTEM)

CASE Upper( SubStr3(uAttributes,i,1) ) == "H"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_HIDDEN)

CASE Upper( SubStr3(uAttributes,i,1) ) == "C"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_COMPRESSED)

CASE Upper( SubStr3(uAttributes,i,1) ) == "O"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_OFFLINE)

CASE Upper( SubStr3(uAttributes,i,1) ) == "T"

dwAttributes := _or(dwAttributes,FILE_ATTRIBUTE_TEMPORARY)

ENDCASE

NEXT

CASE IsNumeric(uAttributes)

dwAttributes := uAttributes

ENDCASE

 

We can then use this requested attributes to aid in determining if the retrieved file's details should be added to the array by making a change:

 

IF ! hSearch == INVALID_HANDLE_VALUE

aThisFile := ConvertFindData( @pFindData )

IF FileAttributesOK( dwAttributes, aThisFile[F_ATTRVALUE] )

aadd( aFiles, aThisFile )

ENDIF

 

DO WHILE FindNextFile( hSearch, @pFindData )

aThisFile := ConvertFindData( @pFindData )

IF FileAttributesOK( dwAttributes, aThisFile[F_ATTRVALUE] )

aadd( aFiles, aThisFile )

ENDIF

ENDDO

ENDIF

 

 

And the FileAttributesOK() function can decide if the attributes of the file are suitable:

FUNCTION FileAttributeOK(dwDesired AS DWORD,dwValue AS DWORD) AS LOGIC

LOCAL lFileOK AS LOGIC

lFileOk := TRUE

DO CASE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_DIRECTORY) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_DIRECTORY) > 0

lFileOK := FALSE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_SYSTEM) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_SYSTEM) > 0

lFileOK := FALSE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_HIDDEN) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_HIDDEN) > 0

lFileOK := FALSE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_COMPRESSED) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_COMPRESSED) > 0

lFileOK := FALSE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_OFFLINE) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_OFFLINE) > 0

lFileOK := FALSE

CASE ! _and(dwDesired,FILE_ATTRIBUTE_TEMPORARY) > 0 .and. ;

_and(dwValue,FILE_ATTRIBUTE_TEMPORARY) > 0

lFileOK := FALSE

ENDCASE

 

RETURN lFileOK

 

Conclusion

In this article we have been able to make use of the Windows API to give us a more powerful Directory() function. It highlights the fact that there is additional information in the Windows environment, just waiting to be tapped. And accessing that information from Visual Objects is definitely possible.