visual-foxpro

Creating XML file of folder directory hierarchy


I am trying to create an XML file that represents the hierarchical directory structure with an unknown amount of nested directories and files. This question relates to a previous one asked by myself, How to loop through nested directories in Foxpro given the initial start directory, so the code I have so far (see below) is based on the structure from the answer here.

*********************************
**** Variable initialization ****
*********************************

set asserts on
set date ymd    

public loXml as MSXML2.DOMDocument60
public loRoot as MSXML2.IXMLDOMElement
public loNode as MSXML2.IXMLDOMElement
public loFile as MSXML2.IXMLDOMElement
public loFolder as MSXML2.IXMLDOMElement
public loSubFolder as MSXML2.IXMLDOMElement

public lcRootDir as String

public lcManifestNS as String

lcRootDir = "C:\LoopTest"              && user defined root directory

lcManifestNS = "http://www.randomnamespace.com/ServerManifest/1.1"                             && namespace which all elements int his document reside

***********************************
**** Creating the XML Document ****
***********************************

loXml = createobject("MSXML2.DOMDocument.6.0")


loRoot = loXml.createNode("Element", "au:Manifest", lcManifestNS)                      && root element
loRoot.setAttribute("Path", lcRootDir)                                                 && path attrubute of root element

GetFilesRecursively(lcRootDir)                                                         && builds all folder and file child nodes under the root element

loXml.appendChild(loRoot)                                                              && append everything within the root node to the document


*********************
** same xml output **
*********************
loXml.save('C:\results\ex2_manifest_test.xml')                 && save the xml file to desired location


***************
** Functions **
***************

procedure getfilesrecursively(tcfolder)
    assert ( vartype(m.tcfolder) = 'C' )

    local lnfile, lafiles[1], lcfileorfolder, datemod                   && create local variables
    for lnfile = 1 to adir(lafiles, addbs(m.tcfolder) + '*', 'd')       && loop that will go through all direct items within initial directory
        
        lcfileorfolder = lower(lafiles[m.lnfile, 1])                    && this variable is the name of the item inside directory
        
        if empty( strtran(m.lcfileorfolder,'.') )                       && some items have names which are just . or .. etc, so ignore these items by restarting the loop
            loop
        endif
        
        datemod = strtran(dtoc(lafiles[lnfile, 3]), '/', '-') + "T" + lafiles[lnfile, 4]    && creating the datetime last modified value
                
        if directory( addbs(m.tcfolder)+m.lcfileorfolder, 1 )                               && if there is a subdirectory, re-run the main loop with updated file path

                loFolder = loRoot.appendChild(loXml.createNode("Element", "au:Folder", lcManifestNS))  && folder node under root node 
                loFolder.setAttribute("Name", lcfileorfolder)                                          && attrubutes for folder node
                loFolder.setAttribute("Date", datemod)
                
                getfilesrecursively(addbs(m.tcfolder)+m.lcfileorfolder)
               
        else                                                                                && if no further subdirectory, then insert the item into table
        
            loFile = loFolder.appendChild(loXml.createNode("Element", "au:File", lcManifestNS))    && file node under folder node
            loFile.setAttribute("Name", lcfileorfolder)                                            && attributes for file node
            loFile.setAttribute("Date", datemod)
        
        endif
    
    endfor

endproc

The main problem I am running into with this code is that all the folder directories are getting put under the root node and are not nesting properly. I understand why, but I do not clearly see how I can change this. Would it be to essentially create a new node every iteration and then append to that? I am just getting used to the syntax around FoxPro XML so some help fixing this issue would be much appreciated. In addition, it seems that any files under the root directory get put inside one of the nested directories in the XML output. You can probably see why as per my code. So ultimately, I am just looking to correct these things to get the XML output nodes to be properly nested. Maybe there is a different approach at solving this, so feel free to answer this question with an entirely new code outline if you wish.


Solution

  • You could do it using ADIR() as follows (please read comments following code):

    Local lcOutputFile, lcRoot
    lcOutputFile = 'C:\temp\nestedDirs.xml'
    lcRoot = Home()
    
    GetTreeXML(m.lcRoot, m.lcOutputFile)
    
    Procedure GetTreeXML(tcRootPath, tcOutput)
        Local lcXML
        Set Textmerge On To (m.tcOutput) Noshow
    
    \<au:Manifest xmlns:au="http://www.randomnamespace.com/ServerManifest/1.1" Path="<< ADDBS(m.tcRootPath) >>">
    
        GetTree(m.tcRootPath,'myDirectories',0)
        *!* Select * From myDirectories &&ORDER BY Filepath
        *!* Use In 'myDirectories'
    
    \</au:Manifest>
        Set Textmerge Off
        Set Textmerge To
        Return m.lcXML
    Endproc
    
    Procedure GetTree(tcPath, tcCursorName, tnLevel)
        Local lcCurDir, ix, lcPath
        Local Array laDirs[1]
        lcCurDir = Addbs(m.tcPath)
        If m.tnLevel = 0
            Create Cursor (m.tcCursorName) (FilePath c(250), Level i)
        Endif
        Insert Into (m.tcCursorName) (FilePath,Level) Values (m.lcCurDir, m.tnLevel)
        For ix = 1 To Adir(laDirs,m.lcCurDir+"*.*","DHS")
            lcPath = m.lcCurDir+laDirs[m.ix,1]
            If laDirs[m.ix,1]#"." And "D"$laDirs[m.ix,5]
                GetTree(m.lcPath, m.tcCursorName, m.tnLevel+1)
            Else
                If laDirs[m.ix,1] != '..'
                    ProcessPath( m.lcPath, m.tnLevel, laDirs[m.ix,3], laDirs[m.ix, 4] )
                Endif
            Endif
        Endfor
        \<< SPACE(2 * m.tnLevel) >></au:Folder>
    Endproc
    
    Procedure ProcessPath(tcPath, tnLevel, tdFileDate, tcFileTime)
        Local lcName, lcDate
        lcDate = Ttoc( Cast(m.tdFileDate As Datetime) + (Ctot(m.tcFileTime)-Ctot('0')), 3)
        If Right(m.tcPath,2) == '\.' && Directory
            If m.tnLevel > 0 && Skip root
                lcName = Justfname(Left(m.tcPath, Len(m.tcPath)-2))
            \<< SPACE(2 * m.tnLevel) >><au:Folder Name="<< m.lcName >>" Date="<< m.lcDate >>">
            Endif
        Else
            lcName = Justfname(m.tcPath)
            \<< SPACE(2 * (m.tnLevel+1)) >><au:File Name="<< m.lcName >>" Date="<< m.lcDate >>"/>
        Endif
    Endproc
    

    I'am not sure about the resulting XML. I followed the format of yours. To me it looks a liitle awkward but if that is the format of your manifest then it is fine (I couldn't browse the schema).

    Unrelated, in your code there were variables declared as public, please forget that there is a 'public' in VFP. Almost all of us use it only for the specific 'oApp' object created at the start of an application if any. Believe me it is dangerous to use.

    Note: VFP ships a DLL called Filer.dll. It contains an activex 'Filer.FileUtil'. It can collect folder and files with some filters applied (has more than simple *.*' file skeleton filter). And it returns the results with original casing, unlike adir() result which uppercases all. Also, you can get file creation, modification and last access times.

    Another one is Scripting.FileSystemObject. However, due to security it might not be installed.

    If Adir() is fine for you then it is simple and fast.

    EDIT: I later noticed above code would add an extra au:Folder at the end plus it wouldn't handle characters that are not allowed in XML. DOM processing would take care of both. So here is the version with DOM:

    Local lcOutputFile, lcRoot
    lcOutputFile = 'C:\temp\nestedDirs.xml'
    lcRoot = Home()
    
    GetTreeXML(m.lcRoot, m.lcOutputFile)
    
    Procedure GetTreeXML(tcRootPath, tcOutput)
        Local loXML As "MSXML2.DOMDocument.6.0", loRoot As "MSXML2.IXMLDOMElement"
        loXML = Createobject("MSXML2.DOMDocument.6.0")
        loRoot = m.loXML.createNode("Element", "au:Manifest", "http://www.randomnamespace.com/ServerManifest/1.1")
        loRoot.setAttribute("Path", m.tcRootPath)
    
        GetTree(m.tcRootPath,'myDirectories',0, m.loXML, m.loRoot)
        
        m.loXML.appendChild(m.loRoot)
        m.loXML.Save(m.tcOutput)
    Endproc
    
    Procedure GetTree(tcPath, tcCursorName, tnLevel, toDOM, toParent)
        Local lcCurDir, ix, lcPath, lcName, lcDate, loElement
        Local Array laDirs[1]
        lcCurDir = Addbs(m.tcPath)
        For ix = 1 To Adir(laDirs,m.lcCurDir+"*.*","DHS")
            lcName = laDirs[m.ix,1]
            lcDate = Ttoc( Cast(laDirs[m.ix,3] As Datetime) + (Ctot(laDirs[m.ix,4])-Ctot('0')), 3)
    
            If laDirs[m.ix,1]#"." And "D"$laDirs[m.ix,5]
                lcPath = m.lcCurDir + laDirs[m.ix,1]
                loElement = m.toDOM.createElement("au:Folder")
                m.loElement.setAttribute("Name", m.lcName)
                m.loElement.setAttribute("Date", m.lcDate)
                m.toParent.appendChild(m.loElement)
                GetTree(m.lcPath, m.tcCursorName, m.tnLevel+1, m.toDOM, m.loElement)
            Else
                If laDirs[m.ix,1] != '.' And laDirs[m.ix,1] != '..'
                    loElement = m.toDOM.createElement("au:File")
                    m.loElement.setAttribute("Name", m.lcName)
                    m.loElement.setAttribute("Date", m.lcDate)
                    m.toParent.appendChild(m.loElement)
                Endif
            Endif
        Endfor
    Endproc