2

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.

Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
DizzleBeans
  • 173
  • 6
  • @stefan-wuebbe I am tagging you in the comments here because you answered my previous related question so you might have a good understanding of the code structure I am using! – DizzleBeans Jan 06 '23 at 21:05
  • 1
    I removed tags that are not really relevant. The main body of the code requires VFP and others do not really have a place here. BTW, I am impressed with the details you are giving as a new user. That doesn't happen often. Thanks. – Cetin Basoz Jan 08 '23 at 14:34
  • 1
    Before I forget, you are asking for an XML creation and thus walking the tree here for all nodes is needed. If you were to create something like windows explorer, then an Ole Treeview with nodes populating only on demand would create a lightning fast result even for the entire disk(s). You can find article and sample code for that here: https://www.levelextreme.com/Home/ShowHeader?Activator=23&ID=39249 and here https://comunidadvfp.blogspot.com/2014/09/utilizando-el-control-treeview-44.html – Cetin Basoz Jan 08 '23 at 14:42
  • Just a minor addition to Cetin's input: the result of running your code as it currently looks with Vfp's `Home()` folder as Root as a test case looked kind of unexpected to me at first glance, especially for the several "graphics" sub-folders and their content as well as their missing parent\folder\ hierarchies. I could be wrong of course, possibly having misunderstood your intention though :) – Stefan Wuebbe Jan 09 '23 at 05:40
  • @StefanWuebbe was your previous comment referring to Cetins code or mine? I would not expect mine to return the correct directory structure results which is why I'm here! haha. – DizzleBeans Jan 09 '23 at 17:08
  • @DizzleBeans , it was for you :) And meant as a for-what-its-worth observation that surprised me, and can probably be reproduced by you since the Home() folder of Vfp might be more or less identical assuming we both had done a regular VFP9 IDE setup. So you as the question owner and design boss would decide whether the hopefully reproducible observation would be unexpected or not, and then act accordingly, e.g. debug and fix when that behavior is not the desired one, like multiple same-name subfolders would get handled correctly in your real use-case scenario or not – Stefan Wuebbe Jan 09 '23 at 19:33

1 Answers1

2

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
Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
  • Thanks @CetinBasoz. I got your code to work after I changed a couple lines: mainly the If statement inside GetTree() should also check for laDirs[m.ix,1]#"..", as well as the "D" check should be "....D" which it seems is how adir() documentation wants it to be written. After these changes, the XML output looks correct to me. Thanks for the well written answer as always Cetin! And let me know if these changes make sense to you.. – DizzleBeans Jan 09 '23 at 18:01
  • 1
    @DizzleBeans, I had SET EXACT at it default OFF so ".." is included but you are right on that, it is safer to add it explicitly. "D"$laDirs[m.ix,5] already checks if attributes contain D, and instead using ....D is dangerous. It might be a Directory where Hidden and\or System attributes set, then using ....D would fail. – Cetin Basoz Jan 09 '23 at 23:42