none
none

Reputation: 12087

extracting directory hierarchy using go language

I'm trying to extract the directory hierarchy of a folder into a datastructure in go language. filepath.Walk seems to be the way to go but all I can do so far is to print the names of files and folders. Here's what I'm using:

func main() {
    visit := func(path string, info os.FileInfo, err error) error {
        if info.IsDir() {
            fmt.Println("dir:  ", path)
        } else {
            fmt.Println("file: ", path)
        }
        return nil
    }

    err := filepath.Walk("./", visit)
    if err != nil {
        log.Fatal(err)
    }
}

this prints the names of folders like:

dir:   folder1
file:  folder1/file1.txt
file:  folder1/file2.txt
file:  folder1/file3.txt
file:  folder1/file4.txt
dir:   folder1/folder2
file:  folder1/folder2/file5.txt
file:  folder1/folder2/file6.txt
file:  folder1/folder2/file7.txt
file:  folder1/folder2/file8.txt
file:  folder1/folder2/file9.txt

for tree structure I thought about using something like:

type File struct {
    Name string
    Content string
}

type Folder struct {
    Name    string
    Files   []File
    Folders []Folder
}

but of course any suggestions are welcomed.

How can I convert this to a tree structure in go? Is there an easier way to do this?

Upvotes: 8

Views: 10213

Answers (5)

Zombo
Zombo

Reputation: 1

Starting with Go 1.16, you can use fstest.MapFS as the datastructure you are asking for:

package main

import (
   "io/fs"
   "os"
   "path/filepath"
   "testing/fstest"
)

func main() {
   m := make(fstest.MapFS)
   walk := func(s string, d fs.DirEntry, e error) error {
      if e != nil { return e }
      if ! d.IsDir() {
         data, e := os.ReadFile(s)
         if e != nil { return e }
         m[s] = &fstest.MapFile{Data: data}
      }
      return nil
   }
   filepath.WalkDir(`C:\go\src\net`, walk)
   data := m[`C:\go\src\net\textproto\writer.go`].Data[:44]
   println(string(data) == "// Copyright 2010 The Go Authors. All rights")
}

https://golang.org/pkg/testing/fstest#MapFS

Upvotes: 0

Hogan
Hogan

Reputation: 21

only use one for loop and filepath.Walk

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "path"
    "path/filepath"
)

func main() {
    tree := BuildTree(os.Args[1])
    fmt.Println(tree)
}

type File struct {
    Name string
}

type Folder struct {
    Name    string
    Files   []*File
    Folders map[string]*Folder
}

func (f *Folder) String() string {
    j, _ := json.Marshal(f)
    return string(j)
}

func BuildTree(dir string) *Folder {
    dir = path.Clean(dir)
    var tree *Folder
    var nodes = map[string]interface{}{}
    var walkFun filepath.WalkFunc = func(p string, info os.FileInfo, err error) error {
        if info.IsDir() {
            nodes[p] = &Folder{path.Base(p), []*File{}, map[string]*Folder{}}
        } else {
            nodes[p] = &File{path.Base(p)}
        }
        return nil
    }
    err := filepath.Walk(dir, walkFun)
    if err != nil {
        log.Fatal(err)
    }

    for key, value := range nodes {
        var parentFolder *Folder
        if key == dir {
            tree = value.(*Folder)
            continue
        } else {
            parentFolder = nodes[path.Dir(key)].(*Folder)
        }

        switch v := value.(type) {
        case *File:
            parentFolder.Files = append(parentFolder.Files, v)
        case *Folder:
            parentFolder.Folders[v.Name] = v
        }
    }

    return tree
}

Upvotes: 2

Sergey
Sergey

Reputation: 1

Little modify

package main

import (
    "fmt"
    "path"
    "strings"
)

type File struct {
    Id   string
    Name string
}

type Folder struct {
    Name    string
    Files   []File
    Folders map[string]*Folder
}

func newFolder(name string) *Folder {
    return &Folder{name, []File{}, make(map[string]*Folder)}
}

func (f *Folder) getFolder(name string) *Folder {
    if nextF, ok := f.Folders[name]; ok {
        return nextF
    } else if f.Name == name {
        return f
    } else {
        return &Folder{}
    }
}

func (f *Folder) existFolder(name string) bool {
    for _, v := range f.Folders {
        if v.Name == name {
            return true
        }
    }
    return false
}

func (f *Folder) addFolder(folderName string) {
    if !f.existFolder(folderName) {
        f.Folders[folderName] = newFolder(folderName)
    }
}

func (f *Folder) addFile(fileName string, fileId string) {
    f.Files = append(f.Files, File{fileId, fileName})
}

func (f *Folder) getList() (result []map[string]interface{}) {
    for _, v := range f.Folders {
        result = append(result, map[string]interface{}{
            "name": v.Name,
            "type": "folder",
        })
    }

    for _, v := range f.Files {
        result = append(result, map[string]interface{}{
            "id":   v.Id,
            "name": v.Name,
            "type": "file",
        })
    }
    return
}

func isFile(str string) bool {
    if path.Ext(str) != "" {
        return true
    }
    return false
}

func DeleteEmptyElements(s []string) []string {
    var r []string
    for _, str := range s {
        if str != "" {
            r = append(r, str)
        }
    }
    return r
}

type IS map[string]string

func main() {
    arrayPaths := []interface{}{
        IS{
            "id":       "1",
            "filePath": "/print/some/com.png",
        },
        IS{
            "id":       "2",
            "filePath": "/print/some2/com412412.png",
        },
        IS{
            "id":       "3",
            "filePath": "/print/some2/41241241241.png",
        },
    }

    breadcrumb := "/print/some2"

    startPath := "/"
    rootFolder := newFolder(startPath)

    for _, path := range arrayPaths {
        filePath := path.(IS)["filePath"]
        fileId := path.(IS)["id"]
        splitPath := DeleteEmptyElements(strings.Split(filePath, "/"))
        tmpFolder := rootFolder
        for _, item := range splitPath {
            if isFile(item) {
                tmpFolder.addFile(item, fileId)
            } else {
                if item != startPath {
                    tmpFolder.addFolder(item)
                }
                tmpFolder = tmpFolder.getFolder(item)
            }
        }
    }

    currentFolder := rootFolder.getFolder("/")
    breadcrumbElements := DeleteEmptyElements(strings.Split(breadcrumb, "/"))
    for i, v := range breadcrumbElements {
        if currentFolder.existFolder(v) {
            currentFolder = currentFolder.getFolder(v)
            if i == len(breadcrumbElements)-1 {
                break
            }
        } else {
            currentFolder = currentFolder.getFolder(v)
        }
    }

    fmt.Println(currentFolder.getList())
}

Upvotes: 0

Marcin Wyszynski
Marcin Wyszynski

Reputation: 2248

I needed something similar for a little app of mine so I wrote a tiny separate library which is available for your viewing pleasure on Github. Since I needed built-in JSON serialization for the returned os.FileInfo, I added it as well.

I know it comes too late for the original author of this question but posting it here anyway in case someone is looking for something similar. Pull requests readily accepted :)

Upvotes: 4

Thomas Kappler
Thomas Kappler

Reputation: 4115

AFAIK there is nothing ready-made for this in the Go standard lib.

Tree structures lend themselves well to a recursive approach. I defined addFile and addFolder methods on your File and Folder types. Starting with a root folder, you can then call these methods in Walk. If you get a/b/c, we'll be calling root.addFile(a, b, c), a.addFile(b, c), b.addFile(c).

I also changed Folder.Folders to a map, because filepath.Walk always gives us full paths, so we can split those and look up their components in the folder map.

Here is some quick and dirty code that probably has bugs and doesn't do full error checking. It only works for the current directory, but that should be easy to fix.

I also added a String() method on Folder, which is recognized by the compiler and will be used when printing out instances of the type.

package main

import (
    "log"
    "os"
    "path/filepath"
    "strings"
)

type File struct {
    Name string
}

type Folder struct {
    Name    string
    Files   []File
    Folders map[string]*Folder
}

func newFolder(name string) *Folder {
    return &Folder{name, []File{}, make(map[string]*Folder)}
}

func (f *Folder) getFolder(name string) *Folder {
    if nextF, ok := f.Folders[name]; ok {
        return nextF
    } else {
        log.Fatalf("Expected nested folder %v in %v\n", name, f.Name)
    }
    return &Folder{} // cannot happen
}

func (f *Folder) addFolder(path []string) {
    for i, segment := range path {
        if i == len(path)-1 { // last segment == new folder
            f.Folders[segment] = newFolder(segment)
        } else {
            f.getFolder(segment).addFolder(path[1:])
        }
    }
}

func (f *Folder) addFile(path []string) {
    for i, segment := range path {
        if i == len(path)-1 { // last segment == file
            f.Files = append(f.Files, File{segment})
        } else {
            f.getFolder(segment).addFile(path[1:])
            return
        }
    }
}

func (f *Folder) String() string {
    var str string
    for _, file := range f.Files {
        str += f.Name + string(filepath.Separator) + file.Name + "\n"
    }
    for _, folder := range f.Folders {
        str += folder.String()
    }
    return str
}

func main() {
    startPath := "."
    rootFolder := newFolder(startPath)

    visit := func(path string, info os.FileInfo, err error) error {
        segments := strings.Split(path, string(filepath.Separator))
        if info.IsDir() {
            if path != startPath {
                rootFolder.addFolder(segments)
            }
        } else {
            rootFolder.addFile(segments)
        }
        return nil
    }

    err := filepath.Walk(startPath, visit)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("%v\n", rootFolder)
}

Upvotes: 4

Related Questions