commit 0bd99eaff778e5508c3551016e390865ea5b876c Author: Blallo Date: Sun Mar 1 23:09:00 2020 +0100 init diff --git a/dir_utils.go b/dir_utils.go new file mode 100644 index 0000000..cffa3e4 --- /dev/null +++ b/dir_utils.go @@ -0,0 +1,29 @@ +package main + +import ( + "os" +) + +func ListDir(path string) ([]os.FileInfo, error) { + var files []os.FileInfo + f, err := os.Open(path) + if err != nil { + return files, err + } + defer f.Close() + return f.Readdir(-1) +} + +func AnyDirectoryDownThere(path string) bool { + files, err := ListDir(path) + if err != nil { + panic(err) + } + + for _, file := range files { + if file.IsDir() { + return true + } + } + return false +} diff --git a/dir_utils_test.go b/dir_utils_test.go new file mode 100644 index 0000000..e6ade64 --- /dev/null +++ b/dir_utils_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +var subdirs = map[string][]string{"sub1": []string{"s1s1", "s1s2", "s1s3"}, "sub2": []string{"s2s1", "s2s2"}, "sub3": []string{}} +var dirWithSubdirs string +var dirWithoutSubdirs string + +func mkchild(root, dir string) (string, error) { + childPath := filepath.Join(root, dir) + var mode os.FileMode = 2_148_532_735 // means "dtrwxrwxrwx" + err := os.Mkdir(childPath, mode) + return childPath, err +} + +func createTestDirs() string { + root, err := ioutil.TempDir("", "ruspa_*") + if err != nil { + panic(err) + } + for dirName, subdirList := range subdirs { + dir, err := mkchild(root, dirName) + if err != nil { + panic(err) + } + for _, subdir := range subdirList { + if _, err = mkchild(dir, subdir); err != nil { + panic(err) + } + } + } + return root +} + +func removeTestDirs(root string) { + for dirName, subdirList := range subdirs { + dirPath := filepath.Join(root, dirName) + for _, subdir := range subdirList { + subdirPath := filepath.Join(dirPath, subdir) + if err := os.Remove(subdirPath); err != nil { + panic(err) + } + } + err := os.Remove(dirPath) + if err != nil { + panic(err) + } + } + if err := os.Remove(root); err != nil { + panic(err) + } +} + +func TestListDir(t *testing.T) { + root := createTestDirs() + defer removeTestDirs(root) + dirs, err := ListDir(root) + if err != nil { + t.Error(err) + } + for _, dir := range dirs { + if _, ok := subdirs[dir.Name()]; !ok { + t.Errorf("missing %s in %s\n", dir, dirs) + } + } + shouldBeEmpty, err := ListDir(filepath.Join(root, "sub3")) + if err != nil { + panic(err) + } + if len(shouldBeEmpty) != 0 { + t.Error("ListDir of sub3 should be empty") + } +} + +func TestAnyDirectoryDownThere(t *testing.T) { + root := createTestDirs() + defer removeTestDirs(root) + pathNotEmpty := filepath.Join(root, "sub1") + if !AnyDirectoryDownThere(pathNotEmpty) { + list, _ := ListDir(pathNotEmpty) + t.Logf("%s -> %s\n", pathNotEmpty, list) + t.Error("sub1 is empty, but should be") + } + pathEmpty := filepath.Join(root, "sub3") + if AnyDirectoryDownThere(pathEmpty) { + list, _ := ListDir(pathEmpty) + t.Logf("%s -> %s\n", pathEmpty, list) + t.Error("sub3 is NOT empty, but it should be") + } +} diff --git a/dynamic_tree.go b/dynamic_tree.go new file mode 100644 index 0000000..aea8db6 --- /dev/null +++ b/dynamic_tree.go @@ -0,0 +1,401 @@ +package main + +import ( + "errors" + "fmt" + "path/filepath" + "strings" +) + +const ( + TopType = "top" + IntermediateType = "intermediate" + BottomType = "bottom" + SingleType = "single" +) + +var ErrUnknownType = errors.New("unknown type") +var ErrShouldBeBottom = errors.New("should be of type Bottom") +var ErrChannelError = errors.New("channel errored") +var ( + NodeB = "│" + NodeT = "├──" + NodeL = "└──" + NodePad = " " +) + +type Node interface { + Size() int64 + Spawn(int) error + Collect() error + Collector() chan int64 + Level() int +} + +func isLastLevel(node Node, maxLevel int) bool { + return maxLevel-1 == node.Level() +} + +func isBeforeLastLevel(node Node, maxLevel int) bool { + return maxLevel-1 > node.Level() +} + +////////////////////////////// +/////////// Top ////////////// +////////////////////////////// + +type Top struct { + path string + size int64 + tree []Node + collect chan int64 + level int +} + +func NewTop(path string) *Top { + t := &Top{path: path, level: 0} + t.collect = make(chan int64) + return t +} + +func (t *Top) Size() int64 { + return t.size +} + +func (t *Top) Spawn(maxLevel int) error { + if t.Level() >= maxLevel { + return ErrShouldBeBottom + } + files, err := ListDir(t.path) + if err != nil { + return err + } + + for _, info := range files { + switch mode := info.Mode(); { + case mode.IsDir(): + var child Node + path := filepath.Join(t.path, info.Name()) + if isLastLevel(t, maxLevel) || !AnyDirectoryDownThere(path) { + child = t.newBottom(path) + } else { + child = t.newIntermediate(path) + } + go child.Spawn(maxLevel) + defer func() { + go child.Collect() + }() + case !mode.IsDir() && mode.IsRegular(): + t.collect <- info.Size() + } + } + return nil +} + +func (t *Top) Collect() error { + for s := range t.collect { + t.size += s + } + return nil +} + +func (t *Top) Collector() chan int64 { + return t.collect +} + +func (t *Top) Level() int { + return t.level +} + +func (t *Top) Name() string { + return filepath.Base(t.path) +} + +func (t *Top) String() string { + var out string + var lines []string + out += fmt.Sprintf("(%d) %s\n", t.Size(), t.path) + treeSize := len(t.tree) + for _, child := range t.tree { + lines = append(lines, fmt.Sprintf("%s", child)) + } + for a := 0; a < treeSize-1; a++ { + childLines := strings.Split(lines[a], "\n") + out += fmt.Sprintf(" %s%s\n", NodeT, childLines[0]) + for b := 1; b < len(childLines); b++ { + out += fmt.Sprintf(" %s%s\n", NodeB, childLines[b]) + } + } + childLines := strings.Split(lines[treeSize-1], "\n") + out += fmt.Sprintf(" %s%s\n", NodeL, childLines[0]) + for a := 1; a < len(childLines); a++ { + out += fmt.Sprintf(" %s%s\n", NodePad, childLines[a]) + } + return out +} + +func (t *Top) addToTree(child Node) { + t.tree = append(t.tree, child) +} + +func (t *Top) newChild(path, kind string) Node { + switch { + case kind == IntermediateType: + i := NewIntermediate(path, t) + t.addToTree(i) + return i + case kind == BottomType: + b := NewBottom(path, t) + t.addToTree(b) + return b + default: + panic(ErrUnknownType) + } +} + +func (t *Top) newIntermediate(path string) *Intermediate { + return t.newChild(path, IntermediateType).(*Intermediate) +} + +func (t *Top) newBottom(path string) *Bottom { + return t.newChild(path, BottomType).(*Bottom) +} + +////////////////////////////// +/////// Intermediate ///////// +////////////////////////////// + +type Intermediate struct { + path string + size int64 + parent Node + tree []Node + collect chan int64 + refer chan int64 + level int +} + +func NewIntermediate(path string, parent Node) *Intermediate { + i := &Intermediate{path: path, refer: parent.Collector(), parent: parent} + i.collect = make(chan int64) + i.level = parent.Level() + 1 + return i +} + +func (i *Intermediate) Size() int64 { + return i.size +} + +func (i *Intermediate) Spawn(maxLevel int) error { + if i.Level() >= maxLevel { + return ErrShouldBeBottom + } + + files, err := ListDir(i.path) + if err != nil { + return err + } + + for _, info := range files { + switch mode := info.Mode(); { + case mode.IsDir(): + var child Node + path := filepath.Join(i.path, info.Name()) + if isLastLevel(i, maxLevel) || !AnyDirectoryDownThere(path) { + child = i.newBottom(path) + } else { + child = i.newIntermediate(path) + } + go child.Spawn(maxLevel) + defer func() { + go child.Collect() + }() + case !mode.IsDir() && mode.IsRegular(): + i.collect <- info.Size() + } + } + + return nil +} + +func (i *Intermediate) Collect() error { + for s := range i.collect { + i.size += s + i.refer <- i.size + } + close(i.refer) + return nil +} + +func (i *Intermediate) Collector() chan int64 { + return i.collect +} + +func (i *Intermediate) Level() int { + return i.level +} + +func (i *Intermediate) Name() string { + return filepath.Base(i.path) +} + +func (i *Intermediate) String() string { + var lines []string + out := fmt.Sprintf("(%d) %s\n", i.Size(), i.Name()) + treeSize := len(i.tree) + for _, child := range i.tree { + lines = append(lines, fmt.Sprintf("%s", child)) + } + + for a := 0; a < treeSize-1; a++ { + childLines := strings.Split(lines[a], "\n") + out += fmt.Sprintf("%s%s%s\n", NodePad, NodeT, childLines[0]) + for b := 1; b < len(childLines); b++ { + out += fmt.Sprintf("%s%s%s\n", NodePad, NodeB, childLines[b]) + } + } + + childLines := strings.Split(lines[treeSize-1], "\n") + lenChildLines := len(childLines) + out += fmt.Sprintf("%s%s%s\n", NodePad, NodeL, childLines[0]) + for a := 1; a < lenChildLines-2; a++ { + out += fmt.Sprintf("%s%s%s\n", NodePad, NodePad, childLines[a]) + } + if lenChildLines > 0 { + out += fmt.Sprintf("%s%s%s", NodePad, NodePad, childLines[lenChildLines-1]) + } + return out +} + +func (i *Intermediate) addToTree(child Node) { + i.tree = append(i.tree, child) +} + +func (i *Intermediate) newChild(path, kind string) Node { + switch { + case kind == IntermediateType: + c := NewIntermediate(path, i) + i.addToTree(c) + return c + case kind == BottomType: + b := NewBottom(path, i) + i.addToTree(b) + return b + default: + panic(ErrUnknownType) + } +} + +func (i *Intermediate) newIntermediate(path string) *Intermediate { + return i.newChild(path, IntermediateType).(*Intermediate) +} + +func (i *Intermediate) newBottom(path string) *Bottom { + return i.newChild(path, BottomType).(*Bottom) +} + +////////////////////////////// +////////// Bottom //////////// +////////////////////////////// + +type Bottom struct { + path string + size int64 + parent Node + walker Walker + collect chan int64 + refer chan int64 + level int +} + +func NewBottom(path string, parent Node) *Bottom { + b := &Bottom{path: path, refer: parent.Collector(), parent: parent} + b.collect = make(chan int64) + b.level = parent.Level() + 1 + return b +} + +func (b *Bottom) Size() int64 { + return b.size +} + +func (b *Bottom) Spawn(maxLevel int) error { + b.walker = NewRealWalker(b.path, b.collect) + go b.walker.Walk() + return nil +} + +func (b *Bottom) Collect() error { + for s := range b.collect { + b.size += s + b.refer <- b.size + } + close(b.refer) + return nil +} + +func (b *Bottom) Collector() chan int64 { + return b.collect +} + +func (b *Bottom) Level() int { + return b.level +} + +func (b *Bottom) Name() string { + return filepath.Base(b.path) +} + +func (b *Bottom) String() string { + return fmt.Sprintf("(%d) %s", b.Size(), b.Name()) +} + +////////////////////////////// +////////// Single //////////// +////////////////////////////// + +type Single struct { + path string + size int64 + walker Walker + collect chan int64 +} + +func (s *Single) Size() int64 { + return s.size +} + +func (s *Single) Spawn(maxLevel int) error { + s.walker = NewRealWalker(s.path, s.collect) + go s.walker.Walk() + return nil +} + +func (s *Single) Collect() error { + for v := range s.collect { + s.size += v + } + return nil +} + +func (s *Single) Collector() chan int64 { + return s.collect +} + +func (s *Single) Level() int { + return 0 +} + +func NewSingle(path string) *Single { + s := &Single{path: path} + s.collect = make(chan int64) + return s +} + +////////////////////////////// +////////// Walker //////////// +////////////////////////////// + +type Walker interface { + Walk() +} diff --git a/dynamic_tree_test.go b/dynamic_tree_test.go new file mode 100644 index 0000000..84651c8 --- /dev/null +++ b/dynamic_tree_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "testing" +) + +const result = `(0) / + ├──(0) i1 + │ ├──(0) i1s1 + │ │ ├──(0) i1s1b1 + │ │ ├──(0) i1s1b2 + │ │ └──(0) i1s1b3 + │ │ (0) i1s1b3 + │ ├──(0) i1s2 + │ │ ├──(0) i1s2b1 + │ │ └──(0) i1s2b2 + │ │ (0) i1s2b2 + │ ├──(0) i1s3 + │ │ ├──(0) i1s3s1 + │ │ │ ├──(0) i1s3s1b1 + │ │ │ ├──(0) i1s3s1b2 + │ │ │ └──(0) i1s3s1b3 + │ │ │ (0) i1s3s1b3 + │ │ └──(0) i1s3b1 + │ │ (0) i1s3b1 + │ └──(0) i1s4 + │ (0) i1s4b1 + ├──(0) i2 + │ ├──(0) i2b1 + │ └──(0) i2b2 + │ (0) i2b2 + └──(0) i3 + ├──(0) i3s1 + │ └──(0) i3s1b1 + │ (0) i3s1b1 + ├──(0) i3s2 + │ ├──(0) i3s2b1 + │ └──(0) i3s2b2 + │ (0) i3s2b2 + └──(0) i3s3 + (0) i3s3b1 +` + +func createTree() *Top { + t := NewTop("/") + i1 := t.newChild("i1", IntermediateType).(*Intermediate) + i2 := t.newChild("i2", IntermediateType).(*Intermediate) + i3 := t.newChild("i3", IntermediateType).(*Intermediate) + i1s1 := i1.newChild("i1s1", IntermediateType).(*Intermediate) + i1s2 := i1.newChild("i1s2", IntermediateType).(*Intermediate) + i1s3 := i1.newChild("i1s3", IntermediateType).(*Intermediate) + i1s4 := i1.newChild("i1s4", IntermediateType).(*Intermediate) + i3s1 := i3.newChild("i3s1", IntermediateType).(*Intermediate) + i3s2 := i3.newChild("i3s2", IntermediateType).(*Intermediate) + i3s3 := i3.newChild("i3s3", IntermediateType).(*Intermediate) + _ = i1s1.newChild("i1s1b1", BottomType).(*Bottom) + _ = i1s1.newChild("i1s1b2", BottomType).(*Bottom) + _ = i1s1.newChild("i1s1b3", BottomType).(*Bottom) + _ = i1s2.newChild("i1s2b1", BottomType).(*Bottom) + _ = i1s2.newChild("i1s2b2", BottomType).(*Bottom) + i1s3s1 := i1s3.newChild("i1s3s1", IntermediateType).(*Intermediate) + _ = i1s3.newChild("i1s3b1", BottomType).(*Bottom) + _ = i1s4.newChild("i1s4b1", BottomType).(*Bottom) + _ = i2.newChild("i2b1", BottomType).(*Bottom) + _ = i2.newChild("i2b2", BottomType).(*Bottom) + _ = i3s1.newChild("i3s1b1", BottomType).(*Bottom) + _ = i3s2.newChild("i3s2b1", BottomType).(*Bottom) + _ = i3s2.newChild("i3s2b2", BottomType).(*Bottom) + _ = i3s3.newChild("i3s3b1", BottomType).(*Bottom) + _ = i1s3s1.newChild("i1s3s1b1", BottomType).(*Bottom) + _ = i1s3s1.newChild("i1s3s1b2", BottomType).(*Bottom) + _ = i1s3s1.newChild("i1s3s1b3", BottomType).(*Bottom) + + return t +} + +func TestString(t *testing.T) { + tree := createTree() + repr := fmt.Sprintf("%s", tree) + if repr != result { + t.Errorf("%s", repr) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fbfa0fb --- /dev/null +++ b/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "time" +) + +func main() { + var path string + var depth int + flag.StringVar(&path, "path", ".", "Path from where to start the walk from") + flag.IntVar(&depth, "depth", 0, "Depth to display") + flag.Parse() + + t := NewTop(path) + go t.Spawn(depth) + go t.Collect() + for { + select { + case <-time.After(500 * time.Millisecond): + fmt.Printf("\033[H\033[2J") + fmt.Println(t) + } + } +} diff --git a/real_walker.go b/real_walker.go new file mode 100644 index 0000000..feeef71 --- /dev/null +++ b/real_walker.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + "path/filepath" +) + +type RealWalker struct { + path string + report chan int64 +} + +func (r *RealWalker) walkFunc(path string, info os.FileInfo, err error) error { + switch mode := info.Mode(); { + case mode.IsRegular(): + r.report <- info.Size() + } + return nil +} + +func (r *RealWalker) Walk() { + filepath.Walk(r.path, r.walkFunc) +} + +func NewRealWalker(path string, report chan int64) *RealWalker { + r := &RealWalker{path: path, report: report} + return r +}