Home > Software engineering >  Clean out folders which only have empty folders from tree
Clean out folders which only have empty folders from tree

Time:11-25

I have a slice of

type Node struct {
   Id       string
   Children []Node
}

I have a diretory structure modelled by this slice. It can happen that there are multi level folder structure in this directory which eventually do not have any files in them. See:ű

folder1/folder2/folder3/folder4
folder1/file1.txt

I would want to clean up those folders which only have empty folders in them. So in this example only folder1 would remain with a file in it, everything below would be deleted. However I can't seem to come up with a good idea to do so. I'm perfectly fine with creating a new tree and not mutating the original one, but I don't know how I could traverse the tree effectively and see if the last child is childless and then go back to the root and remove that child which has turned out to be just a list of empty folders. Any idea would be welcomed!

My initial solution which only removes leaves and not the parent folder also:

func removeChildlessFolders(original, tree []Node) []Node {
    for i, node := range original {
        if len(node.Children) == 0 {
            continue
        }

        dir := Node{}
        dir.Id = node.Id
        dir.Children = append(dir.Children, node.Children...)
        tree = append(tree, dir)
        removeChildlessFolders(original[i].Children, node.Children)
    }

    return tree
}

CodePudding user response:

Firstly good question but it is very difficult for others to reproduce the use case that you have. From next time try adding a reproducible code that people can use and quickly test their approaches and give the result. Like you have passed the root but how you have initialised it? If someone needs to help you out, they need to first build the tree. Generally this is inconvenient. Nevertheless let's get to the solution.

Directory structure

Input Dir

test-folder
├── folder1
│   └── folder2
│       └── folder3
├── folder4
│   ├── folder5
│   └── joker
└── folder6
    └── file.txt

Expected Result

test-folder
└── folder6
    └── file.txt

Node definition

Firstly, I don't know how you have created the tree of dirs. If you have hardcoded it, then it's a different issue but the way an n-ary tree is usually populated, then you need to define the Node with the self referential pointer. Not with exact slice. So I would define the Node in the following way

type Node struct {
    Id       string
    Children []*Node
}

Helper Method

This is a helper method to check if a path is pointing to a directory

func ifDir(path string) bool {
    file, err := os.Open(path)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    info, err := file.Stat()
    if err != nil {
        panic(err)
    }
    if info.IsDir() {
        return true
    }
    return false
}

How to populate the tree

This is a simple and iterative way to input an n-ary tree using a queue. Golang doesn't provide and queue implementation but golang channels are actually queues only. I have kept it to 500 because we can not create a dynamic buffered channel in golang. This number should be good for almost any scenarios IMHO.

func buildTreeFromDir(baseDir string) *Node {
    _, err := ioutil.ReadDir(baseDir)
    if err != nil {
        return nil
    }
    root := &Node{
        Id: baseDir,
    }
    //////////
    queue := make(chan *Node, 500) // Consider that there can not be any dir with > 500 depth
    queue <- root
    for {
        if len(queue) == 0 {
            break
        }
        data, ok := <-queue
        if ok {
            // Iterate all the contents in the dir
            curDir := (*data).Id
            if ifDir(curDir) {
                contents, _ := ioutil.ReadDir(curDir)

                data.Children = make([]*Node, len(contents))
                for i, content := range contents {
                    node := new(Node)
                    node.Id = filepath.Join(curDir, content.Name())
                    data.Children[i] = node
                    if content.IsDir() {
                        queue <- node
                    }
                }
            }
        }
    }
    return root
}

Another helper method

This is just to print the dir tree. Just for debugging purpose.

func printDirTree(root *Node) {
    fmt.Println(root.Id)
    for _, each := range root.Children {
        printDirTree(each)
    }
    if len(root.Children) == 0 {
        fmt.Println("===")
    }

}

Finally your solution.

Pretty straightforward. Do let me know if you have any questions.

func recursiveEmptyDelete(root *Node) {
    // If the current root is not pointing to any dir
    if root == nil {
        return
    }
    for _, each := range root.Children {
        recursiveEmptyDelete(each)
    }
    if !ifDir(root.Id) {
        return
    } else if content, _ := ioutil.ReadDir(root.Id); len(content) != 0 {
        return
    }
    os.Remove(root.Id)
}

Here is the main()

func main() {
    root := buildTreeFromDir("test-folder")
    printDirTree(root)
    recursiveEmptyDelete(root)
}

CodePudding user response:

Let me see if I can help you. First I want to say that this solution can be improved for sure! BTW, it's the best that I was able to do, and hope you find some insights for achieving what you need. Here, I'm going to show you the scenarios managed.

Scenario 1

Input:

  • folder1/folder2/folder3/folder4/file1.txt

Output:

  • folder1/file1.txt

Scenario 2

Input:

  • folder1/folder2/folder3/folder4/file1.txt
  • folder1/folder5/

Output:

  • folder1/file1.txt

Scenario 3

Input:

  • folder1/folder2/folder3/folder4/file1.txt
  • folder1/folder5/file2.txt

Output:

  • folder1/file1.txt
  • folder1/file2.txt

Code

Here, I'm going to present you the relevant parts of my solution.

The Node struct

type Node struct {
    name     string
    isDir    bool
    children []Node
}

The getSafePath function

func getSafePath(parent Node, safePath string, foldersToDel []string) (string, []string) {
    if len(parent.children) == 1 && !parent.children[0].isDir {
        fileName := filepath.Base(parent.children[0].name)
        if err := os.Rename(parent.children[0].name, fmt.Sprintf("%v/%v", safePath, fileName)); err != nil {
            panic(err)
        }
        return safePath, foldersToDel
    }

    if len(parent.children) == 1 && parent.children[0].isDir {
        foldersToDel = append(foldersToDel, parent.children[0].name)
        newChildren := []Node{}
        newChildren = append(newChildren, parent.children[0].children...)
        parent.children = newChildren

        safePath, foldersToDel = getSafePath(parent, safePath, foldersToDel)
        return safePath, foldersToDel
    }

    for _, v := range parent.children {
        if !v.isDir {
            fileName := filepath.Base(v.name)
            if err := os.Rename(v.name, fmt.Sprintf("%v/%v", safePath, fileName)); err != nil {
                panic(err)
            }
        } else {
            foldersToDel = append(foldersToDel, v.name)
            safePath, foldersToDel = getSafePath(v, safePath, foldersToDel)
        }
    }

    return safePath, foldersToDel
}

This function is responsible for moving the files that are in a leaf-level directory up.

The main.go file

// code omitted for brevity
safeDir := "folder1"
foldersToDel := []string{}
_, foldersToDel = getSafePath(end, safeDir, foldersToDel)

for i := len(foldersToDel) - 1; i >= 0; i-- {
    if err := os.Remove(foldersToDel[i]); err != nil {
        panic(err)
    }
}

Let me know if this helps or there are other scenarios that you've to manage.

  • Related