diff --git a/.gitignore b/.gitignore index 79dd381..cada87e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out - +/tmp /build /_vendor* /vendor diff --git a/filetree/data.go b/filetree/data.go index 05114b9..7617842 100644 --- a/filetree/data.go +++ b/filetree/data.go @@ -18,7 +18,7 @@ const ( type NodeData struct { ViewInfo ViewInfo - FileInfo *FileInfo + FileInfo FileInfo DiffType DiffType } @@ -28,9 +28,10 @@ type ViewInfo struct { } type FileInfo struct { - Path string - Typeflag byte - MD5sum [16]byte + Path string + Typeflag byte + MD5sum [16]byte + TarHeader tar.Header } type DiffType int @@ -38,7 +39,7 @@ type DiffType int func NewNodeData() (*NodeData) { return &NodeData{ ViewInfo: *NewViewInfo(), - FileInfo: nil, + FileInfo: FileInfo{}, DiffType: Unchanged, } } @@ -46,7 +47,7 @@ func NewNodeData() (*NodeData) { func (data *NodeData) Copy() (*NodeData) { return &NodeData{ ViewInfo: *data.ViewInfo.Copy(), - FileInfo: data.FileInfo.Copy(), + FileInfo: *data.FileInfo.Copy(), DiffType: data.DiffType, } } @@ -71,6 +72,7 @@ func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { Path: path, Typeflag: header.Typeflag, MD5sum: [16]byte{}, + TarHeader: *header, } } fileBytes := make([]byte, header.Size) @@ -78,10 +80,12 @@ func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { if err != nil && err != io.EOF { panic(err) } + return FileInfo{ - Path: path, - Typeflag: header.Typeflag, - MD5sum: md5.Sum(fileBytes), + Path: path, + Typeflag: header.Typeflag, + MD5sum: md5.Sum(fileBytes), + TarHeader: *header, } } @@ -112,19 +116,14 @@ func (data *FileInfo) Copy() *FileInfo { return nil } return &FileInfo{ - Path: data.Path, - Typeflag: data.Typeflag, - MD5sum: data.MD5sum, + Path: data.Path, + Typeflag: data.Typeflag, + MD5sum: data.MD5sum, + TarHeader: data.TarHeader, } } -func (data *FileInfo) getDiffType(other *FileInfo) DiffType { - if data == nil && other == nil { - return Unchanged - } - if data == nil || other == nil { - return Changed - } +func (data *FileInfo) getDiffType(other FileInfo) DiffType { if data.Typeflag == other.Typeflag { if bytes.Compare(data.MD5sum[:], other.MD5sum[:]) == 0 { return Unchanged diff --git a/filetree/data_test.go b/filetree/data_test.go index 5c0c7e8..f107e30 100644 --- a/filetree/data_test.go +++ b/filetree/data_test.go @@ -6,7 +6,7 @@ import ( func TestAssignDiffType(t *testing.T) { tree := NewFileTree() - node, err := tree.AddPath("/usr", BlankFileChangeInfo("/usr")) + node, err := tree.AddPath("/usr", *BlankFileChangeInfo("/usr")) if err != nil { t.Errorf("Expected no error from fetching path. got: %v", err) } diff --git a/filetree/node.go b/filetree/node.go index 7acd9d4..979ecf1 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -6,6 +6,13 @@ import ( "github.com/fatih/color" "fmt" + "github.com/phayes/permbits" + "github.com/dustin/go-humanize" + "github.com/wagoodman/docker-image-explorer/_vendor-20180604210951/github.com/Microsoft/go-winio/archive/tar" +) + +const ( + AttributeFormat = "%s%s %10s %10s " ) type FileNode struct { @@ -16,13 +23,12 @@ type FileNode struct { Children map[string]*FileNode } -func NewNode(parent *FileNode, name string, data *FileInfo) (node *FileNode) { +func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) { node = new(FileNode) node.Name = name node.Data = *NewNodeData() - if data != nil { - node.Data.FileInfo = data.Copy() - } + node.Data.FileInfo = *data.Copy() + node.Children = make(map[string]*FileNode) node.Parent = parent if parent != nil { @@ -42,11 +48,11 @@ func (node *FileNode) Copy(parent *FileNode) *FileNode { return newNode } -func (node *FileNode) AddChild(name string, data *FileInfo) (child *FileNode) { +func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) { child = NewNode(node, name, data) if node.Children[name] != nil { // tree node already exists, replace the payload, keep the children - node.Children[name].Data.FileInfo = data.Copy() + node.Children[name].Data.FileInfo = *data.Copy() } else { node.Children[name] = child node.Tree.Size++ @@ -68,6 +74,7 @@ func (node *FileNode) Remove() error { func (node *FileNode) String() string { var style *color.Color + var display string if node == nil { return "" } @@ -83,7 +90,42 @@ func (node *FileNode) String() string { default: style = color.New(color.BgMagenta) } - return style.Sprint(node.Name) + display = node.Name + if node.Data.FileInfo.TarHeader.Typeflag == tar.TypeSymlink || node.Data.FileInfo.TarHeader.Typeflag == tar.TypeLink { + display += " -> " + node.Data.FileInfo.TarHeader.Linkname + } + return style.Sprint(display) +} + +func (node *FileNode) MetadataString() string { + var style *color.Color + if node == nil { + return "" + } + switch node.Data.DiffType { + case Added: + style = color.New(color.FgGreen) + case Removed: + style = color.New(color.FgRed) + case Changed: + style = color.New(color.FgYellow) + case Unchanged: + style = color.New(color.Reset) + default: + style = color.New(color.BgMagenta) + } + + fileMode := permbits.FileMode(node.Data.FileInfo.TarHeader.FileInfo().Mode()).String() + dir := "-" + if node.Data.FileInfo.TarHeader.FileInfo().IsDir() { + dir = "d" + } + user := node.Data.FileInfo.TarHeader.Uid + group := node.Data.FileInfo.TarHeader.Gid + userGroup := fmt.Sprintf("%d:%d", user, group) + size := humanize.Bytes(uint64(node.Data.FileInfo.TarHeader.FileInfo().Size())) + + return style.Sprint(fmt.Sprintf(AttributeFormat,dir, fileMode, userGroup, size)) } func (node *FileNode) VisitDepthChildFirst(visiter Visiter, evaluator VisitEvaluator) error { diff --git a/filetree/node_test.go b/filetree/node_test.go index 4ad1e78..bdc7c92 100644 --- a/filetree/node_test.go +++ b/filetree/node_test.go @@ -12,14 +12,14 @@ func TestAddChild(t *testing.T) { Path: "stufffffs", } - one := tree.Root.AddChild("first node!", &payload) + one := tree.Root.AddChild("first node!", payload) - two := tree.Root.AddChild("nil node!", nil) + two := tree.Root.AddChild("nil node!", FileInfo{}) - tree.Root.AddChild("third node!", nil) - two.AddChild("forth, one level down...", nil) - two.AddChild("fifth, one level down...", nil) - two.AddChild("fifth, one level down...", nil) + tree.Root.AddChild("third node!", FileInfo{}) + two.AddChild("forth, one level down...", FileInfo{}) + two.AddChild("fifth, one level down...", FileInfo{}) + two.AddChild("fifth, one level down...", FileInfo{}) expected, actual = 5, tree.Size if expected != actual { @@ -36,17 +36,14 @@ func TestAddChild(t *testing.T) { t.Errorf("Expected 'twos' number of children to be %d got %d.", expected, actual) } - expectedFC := &FileInfo{ + expectedFC := FileInfo{ Path: "stufffffs", } actualFC := one.Data.FileInfo - if *expectedFC != *actualFC { + if expectedFC.Path != actualFC.Path { t.Errorf("Expected 'ones' payload to be %+v got %+v.", expectedFC, actualFC) } - if two.Data.FileInfo != nil { - t.Errorf("Expected 'twos' payload to be nil got %+v.", two.Data.FileInfo) - } } @@ -54,11 +51,11 @@ func TestRemoveChild(t *testing.T) { var expected, actual int tree := NewFileTree() - tree.Root.AddChild("first", nil) - two := tree.Root.AddChild("nil", nil) - tree.Root.AddChild("third", nil) - forth := two.AddChild("forth", nil) - two.AddChild("fifth", nil) + tree.Root.AddChild("first", FileInfo{}) + two := tree.Root.AddChild("nil", FileInfo{}) + tree.Root.AddChild("third", FileInfo{}) + forth := two.AddChild("forth", FileInfo{}) + two.AddChild("fifth", FileInfo{}) forth.Remove() @@ -87,7 +84,7 @@ func TestRemoveChild(t *testing.T) { func TestPath(t *testing.T) { expected := "/etc/nginx/nginx.conf" tree := NewFileTree() - node, _ := tree.AddPath(expected, nil) + node, _ := tree.AddPath(expected, FileInfo{}) actual := node.Path() if expected != actual { @@ -97,8 +94,8 @@ func TestPath(t *testing.T) { func TestIsWhiteout(t *testing.T) { tree1 := NewFileTree() - p1, _ := tree1.AddPath("/etc/nginx/public1", nil) - p2, _ := tree1.AddPath("/etc/nginx/.wh.public2", nil) + p1, _ := tree1.AddPath("/etc/nginx/public1", FileInfo{}) + p2, _ := tree1.AddPath("/etc/nginx/.wh.public2", FileInfo{}) if p1.IsWhiteout() != false { t.Errorf("Expected Path '%s' to **not** be a whiteout file", p1.Name) @@ -111,15 +108,15 @@ func TestIsWhiteout(t *testing.T) { func TestDiffTypeFromAddedChildren(t *testing.T) { tree := NewFileTree() - node, _ := tree.AddPath("/usr", BlankFileChangeInfo("/usr")) + node, _ := tree.AddPath("/usr", *BlankFileChangeInfo("/usr")) node.Data.DiffType = Unchanged info1 := BlankFileChangeInfo("/usr/bin") - node, _ = tree.AddPath("/usr/bin", info1) + node, _ = tree.AddPath("/usr/bin", *info1) node.Data.DiffType = Added info2 := BlankFileChangeInfo("/usr/bin2") - node, _ = tree.AddPath("/usr/bin2", info2) + node, _ = tree.AddPath("/usr/bin2", *info2) node.Data.DiffType = Removed tree.Root.Children["usr"].deriveDiffType(Unchanged) @@ -130,14 +127,14 @@ func TestDiffTypeFromAddedChildren(t *testing.T) { } func TestDiffTypeFromRemovedChildren(t *testing.T) { tree := NewFileTree() - node, _ := tree.AddPath("/usr", BlankFileChangeInfo("/usr")) + node, _ := tree.AddPath("/usr", *BlankFileChangeInfo("/usr")) info1 := BlankFileChangeInfo("/usr/.wh.bin") - node, _ = tree.AddPath("/usr/.wh.bin", info1) + node, _ = tree.AddPath("/usr/.wh.bin", *info1) node.Data.DiffType = Removed info2 := BlankFileChangeInfo("/usr/.wh.bin2") - node, _ = tree.AddPath("/usr/.wh.bin2", info2) + node, _ = tree.AddPath("/usr/.wh.bin2", *info2) node.Data.DiffType = Removed tree.Root.Children["usr"].deriveDiffType(Unchanged) diff --git a/filetree/tree.go b/filetree/tree.go index e1b4efe..01e405f 100644 --- a/filetree/tree.go +++ b/filetree/tree.go @@ -32,11 +32,11 @@ func NewFileTree() (tree *FileTree) { return tree } -func (tree *FileTree) String() string { - var renderLine func(string, []bool, bool, bool) string +func (tree *FileTree) String(showAttributes bool) string { + var renderTreeLine func(string, []bool, bool, bool) string var walkTree func(*FileNode, []bool, int) string - renderLine = func(nodeText string, spaces []bool, last bool, collapsed bool) string { + renderTreeLine = func(nodeText string, spaces []bool, last bool, collapsed bool) string { var otherBranches string for _, space := range spaces { if space { @@ -73,7 +73,10 @@ func (tree *FileTree) String() string { } last := idx == (len(node.Children) - 1) showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0 - result += renderLine(child.String(), spaces, last, showCollapsed) + if showAttributes { + result += child.MetadataString() + " " + } + result += renderTreeLine(child.String(), spaces, last, showCollapsed) if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed { spacesChild := append(spaces, last) result += walkTree(child, spacesChild, depth+1) @@ -82,7 +85,7 @@ func (tree *FileTree) String() string { return result } - return "." + newLine + walkTree(tree.Root, []bool{}, 0) + return walkTree(tree.Root, []bool{}, 0) } func (tree *FileTree) Copy() *FileTree { @@ -131,7 +134,7 @@ func (tree *FileTree) Stack(upper *FileTree) error { } func (tree *FileTree) GetNode(path string) (*FileNode, error) { - nodeNames := strings.Split(path, "/") + nodeNames := strings.Split(strings.Trim(path, "/"), "/") node := tree.Root for _, name := range nodeNames { if name == "" { @@ -145,9 +148,9 @@ func (tree *FileTree) GetNode(path string) (*FileNode, error) { return node, nil } -func (tree *FileTree) AddPath(path string, data *FileInfo) (*FileNode, error) { +func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, error) { // fmt.Printf("ADDPATH: %s %+v\n", path, data) - nodeNames := strings.Split(path, "/") + nodeNames := strings.Split(strings.Trim(path, "/"), "/") node := tree.Root for idx, name := range nodeNames { if name == "" { @@ -159,7 +162,7 @@ func (tree *FileTree) AddPath(path string, data *FileInfo) (*FileNode, error) { } else { // don't attach the payload. The payload is destined for the // Path's end node, not any intermediary node. - node = node.AddChild(name, nil) + node = node.AddChild(name, FileInfo{}) } // attach payload to the last specified node diff --git a/filetree/tree_test.go b/filetree/tree_test.go index 283bd39..44b8e60 100644 --- a/filetree/tree_test.go +++ b/filetree/tree_test.go @@ -15,9 +15,6 @@ func stringInSlice(a string, list []string) bool { } func AssertDiffType(node *FileNode, expectedDiffType DiffType) error { - if node.Data.FileInfo == nil { - return fmt.Errorf("expected *FileInfo but got nil at Path %s", node.Path()) - } if node.Data.DiffType != expectedDiffType { return fmt.Errorf("Expecting node at %s to have DiffType %v, but had %v", node.Path(), expectedDiffType, node.Data.DiffType) } @@ -26,18 +23,18 @@ func AssertDiffType(node *FileNode, expectedDiffType DiffType) error { func TestPrintTree(t *testing.T) { tree := NewFileTree() - tree.Root.AddChild("first node!", nil) - two := tree.Root.AddChild("second node!", nil) - tree.Root.AddChild("third node!", nil) - two.AddChild("forth, one level down...", nil) + tree.Root.AddChild("first node!", FileInfo{}) + two := tree.Root.AddChild("second node!", FileInfo{}) + tree.Root.AddChild("third node!", FileInfo{}) + two.AddChild("forth, one level down...", FileInfo{}) - expected := `. -├── first node! + expected := +`├── first node! ├── second node! │ └── forth, one level down... └── third node! ` - actual := tree.String() + actual := tree.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) @@ -47,15 +44,15 @@ func TestPrintTree(t *testing.T) { func TestAddPath(t *testing.T) { tree := NewFileTree() - tree.AddPath("/etc/nginx/nginx.conf", nil) - tree.AddPath("/etc/nginx/public", nil) - tree.AddPath("/var/run/systemd", nil) - tree.AddPath("/var/run/bashful", nil) - tree.AddPath("/tmp", nil) - tree.AddPath("/tmp/nonsense", nil) + tree.AddPath("/etc/nginx/nginx.conf", FileInfo{}) + tree.AddPath("/etc/nginx/public", FileInfo{}) + tree.AddPath("/var/run/systemd", FileInfo{}) + tree.AddPath("/var/run/bashful", FileInfo{}) + tree.AddPath("/tmp", FileInfo{}) + tree.AddPath("/tmp/nonsense", FileInfo{}) - expected := `. -├── etc + expected := +`├── etc │ └── nginx │ ├── nginx.conf │ └── public @@ -66,7 +63,7 @@ func TestAddPath(t *testing.T) { ├── bashful └── systemd ` - actual := tree.String() + actual := tree.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) @@ -76,18 +73,18 @@ func TestAddPath(t *testing.T) { func TestRemovePath(t *testing.T) { tree := NewFileTree() - tree.AddPath("/etc/nginx/nginx.conf", nil) - tree.AddPath("/etc/nginx/public", nil) - tree.AddPath("/var/run/systemd", nil) - tree.AddPath("/var/run/bashful", nil) - tree.AddPath("/tmp", nil) - tree.AddPath("/tmp/nonsense", nil) + tree.AddPath("/etc/nginx/nginx.conf", FileInfo{}) + tree.AddPath("/etc/nginx/public", FileInfo{}) + tree.AddPath("/var/run/systemd", FileInfo{}) + tree.AddPath("/var/run/bashful", FileInfo{}) + tree.AddPath("/tmp", FileInfo{}) + tree.AddPath("/tmp/nonsense", FileInfo{}) tree.RemovePath("/var/run/bashful") tree.RemovePath("/tmp") - expected := `. -├── etc + expected := +`├── etc │ └── nginx │ ├── nginx.conf │ └── public @@ -95,7 +92,7 @@ func TestRemovePath(t *testing.T) { └── run └── systemd ` - actual := tree.String() + actual := tree.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) @@ -111,20 +108,20 @@ func TestStack(t *testing.T) { tree1 := NewFileTree() - tree1.AddPath("/etc/nginx/public", nil) - tree1.AddPath(payloadKey, nil) - tree1.AddPath("/var/run/bashful", nil) - tree1.AddPath("/tmp", nil) - tree1.AddPath("/tmp/nonsense", nil) + tree1.AddPath("/etc/nginx/public", FileInfo{}) + tree1.AddPath(payloadKey, FileInfo{}) + tree1.AddPath("/var/run/bashful", FileInfo{}) + tree1.AddPath("/tmp", FileInfo{}) + tree1.AddPath("/tmp/nonsense", FileInfo{}) tree2 := NewFileTree() // add new files - tree2.AddPath("/etc/nginx/nginx.conf", nil) + tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{}) // modify current files - tree2.AddPath(payloadKey, &payloadValue) + tree2.AddPath(payloadKey, payloadValue) // whiteout the following files - tree2.AddPath("/var/run/.wh.bashful", nil) - tree2.AddPath("/.wh.tmp", nil) + tree2.AddPath("/var/run/.wh.bashful", FileInfo{}) + tree2.AddPath("/.wh.tmp", FileInfo{}) err := tree1.Stack(tree2) @@ -132,8 +129,8 @@ func TestStack(t *testing.T) { t.Errorf("Could not stack refTrees: %v", err) } - expected := `. -├── etc + expected := +`├── etc │ └── nginx │ ├── nginx.conf │ └── public @@ -147,11 +144,11 @@ func TestStack(t *testing.T) { t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey) } - if *node.Data.FileInfo != payloadValue { + if node.Data.FileInfo.Path != payloadValue.Path { t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data.FileInfo) } - actual := tree1.String() + actual := tree1.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) @@ -161,18 +158,18 @@ func TestStack(t *testing.T) { func TestCopy(t *testing.T) { tree := NewFileTree() - tree.AddPath("/etc/nginx/nginx.conf", nil) - tree.AddPath("/etc/nginx/public", nil) - tree.AddPath("/var/run/systemd", nil) - tree.AddPath("/var/run/bashful", nil) - tree.AddPath("/tmp", nil) - tree.AddPath("/tmp/nonsense", nil) + tree.AddPath("/etc/nginx/nginx.conf", FileInfo{}) + tree.AddPath("/etc/nginx/public", FileInfo{}) + tree.AddPath("/var/run/systemd", FileInfo{}) + tree.AddPath("/var/run/bashful", FileInfo{}) + tree.AddPath("/tmp", FileInfo{}) + tree.AddPath("/tmp/nonsense", FileInfo{}) tree.RemovePath("/var/run/bashful") tree.RemovePath("/tmp") - expected := `. -├── etc + expected := +`├── etc │ └── nginx │ ├── nginx.conf │ └── public @@ -182,7 +179,7 @@ func TestCopy(t *testing.T) { ` NewFileTree := tree.Copy() - actual := NewFileTree.String() + actual := NewFileTree.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) @@ -201,18 +198,14 @@ func TestCompareWithNoChanges(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } - lowerTree.AddPath(value, &fakeData) - upperTree.AddPath(value, &fakeData) + lowerTree.AddPath(value, fakeData) + upperTree.AddPath(value, fakeData) } lowerTree.Compare(upperTree) asserter := func(n *FileNode) error { if n.Path() == "/" { return nil } - if n.Data.FileInfo == nil { - t.Errorf("Expected *FileInfo but got nil") - return fmt.Errorf("expected *FileInfo but got nil") - } if (n.Data.DiffType) != Unchanged { t.Errorf("Expecting node at %s to have DiffType unchanged, but had %v", n.Path(), n.Data.DiffType) } @@ -231,7 +224,7 @@ func TestCompareWithAdds(t *testing.T) { upperPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin", "/usr/bin/bash"} for _, value := range lowerPaths { - lowerTree.AddPath(value, &FileInfo{ + lowerTree.AddPath(value, FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -239,7 +232,7 @@ func TestCompareWithAdds(t *testing.T) { } for _, value := range upperPaths { - upperTree.AddPath(value, &FileInfo{ + upperTree.AddPath(value, FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -291,12 +284,12 @@ func TestCompareWithChanges(t *testing.T) { paths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"} for _, value := range paths { - lowerTree.AddPath(value, &FileInfo{ + lowerTree.AddPath(value, FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }) - upperTree.AddPath(value, &FileInfo{ + upperTree.AddPath(value, FileInfo{ Path: value, Typeflag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, @@ -348,7 +341,7 @@ func TestCompareWithRemoves(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } - lowerTree.AddPath(value, &fakeData) + lowerTree.AddPath(value, fakeData) } for _, value := range upperPaths { @@ -357,7 +350,7 @@ func TestCompareWithRemoves(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } - upperTree.AddPath(value, &fakeData) + upperTree.AddPath(value, fakeData) } lowerTree.Compare(upperTree) @@ -397,12 +390,12 @@ func TestCompareWithRemoves(t *testing.T) { func TestStackRange(t *testing.T) { tree := NewFileTree() - tree.AddPath("/etc/nginx/nginx.conf", nil) - tree.AddPath("/etc/nginx/public", nil) - tree.AddPath("/var/run/systemd", nil) - tree.AddPath("/var/run/bashful", nil) - tree.AddPath("/tmp", nil) - tree.AddPath("/tmp/nonsense", nil) + tree.AddPath("/etc/nginx/nginx.conf", FileInfo{}) + tree.AddPath("/etc/nginx/public", FileInfo{}) + tree.AddPath("/var/run/systemd", FileInfo{}) + tree.AddPath("/var/run/bashful", FileInfo{}) + tree.AddPath("/tmp", FileInfo{}) + tree.AddPath("/tmp/nonsense", FileInfo{}) tree.RemovePath("/var/run/bashful") tree.RemovePath("/tmp") @@ -418,7 +411,7 @@ func TestStackRange(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } - lowerTree.AddPath(value, &fakeData) + lowerTree.AddPath(value, fakeData) } for _, value := range upperPaths { @@ -427,7 +420,7 @@ func TestStackRange(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}, } - upperTree.AddPath(value, &fakeData) + upperTree.AddPath(value, fakeData) } trees := []*FileTree{lowerTree, upperTree, tree} StackRange(trees, 2) @@ -445,7 +438,7 @@ func TestRemoveOnIterate(t *testing.T) { Typeflag: 1, MD5sum: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, } - node, err := tree.AddPath(value, &fakeData) + node, err := tree.AddPath(value, fakeData) if err == nil && stringInSlice(node.Path(), []string{"/etc"}) { node.Data.ViewInfo.Hidden = true } @@ -458,12 +451,12 @@ func TestRemoveOnIterate(t *testing.T) { return nil }, nil) - expected := `. -└── usr + expected := +`└── usr ├── bin └── something ` - actual := tree.String() + actual := tree.String(false) if expected != actual { t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) } diff --git a/image/image.go b/image/image.go index 96bc0cd..4934fee 100644 --- a/image/image.go +++ b/image/image.go @@ -19,6 +19,10 @@ import ( "golang.org/x/net/context" ) +const ( + LayerFormat = "%-25s %7s %s" +) + func check(e error) { if e != nil { panic(e) @@ -56,7 +60,7 @@ func (layer *Layer) String() string { if len(layer.History.Tags) > 0 { id = "[" + strings.Join(layer.History.Tags, ",") + "]" } - return fmt.Sprintf("%25s %7s %s", id, humanize.Bytes(uint64(layer.History.Size)), layer.History.CreatedBy) + return fmt.Sprintf(LayerFormat, id, humanize.Bytes(uint64(layer.History.Size)), layer.History.CreatedBy) } func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { @@ -103,7 +107,7 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { tree.Name = name fileInfos := getFileList(tarReader, header) for _, element := range fileInfos { - tree.AddPath(element.Path, &element) + tree.AddPath(element.Path, element) } layerMap[tree.Name] = tree } @@ -184,9 +188,9 @@ func saveImage(imageID string) (string, string) { return imageTarPath, tmpDir } -func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileInfo { +func getFileList(parentReader *tar.Reader, header *tar.Header) []filetree.FileInfo { var files []filetree.FileInfo - var tarredBytes = make([]byte, h.Size) + var tarredBytes = make([]byte, header.Size) _, err := parentReader.Read(tarredBytes) if err != nil { diff --git a/ui/filetreeview.go b/ui/filetreeview.go index 774fb74..ad9bd6a 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -7,8 +7,11 @@ import ( "strings" + "strings" + "github.com/fatih/color" "github.com/jroimartin/gocui" + "github.com/lunixbochs/vtclean" "github.com/wagoodman/docker-image-explorer/filetree" ) @@ -16,6 +19,7 @@ type FileTreeView struct { Name string gui *gocui.Gui view *gocui.View + header *gocui.View TreeIndex int ModelTree *filetree.FileTree ViewTree *filetree.FileTree @@ -36,7 +40,7 @@ func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTr return treeview } -func (view *FileTreeView) Setup(v *gocui.View) error { +func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error { // set view options view.view = v @@ -45,7 +49,12 @@ func (view *FileTreeView) Setup(v *gocui.View) error { //view.view.Highlight = true //view.view.SelBgColor = gocui.ColorGreen //view.view.SelFgColor = gocui.ColorBlack - view.view.Frame = true + view.view.Frame = false + + view.header = header + view.header.Editable = false + view.header.Wrap = false + view.header.Frame = false // set keybindings if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil { @@ -76,6 +85,9 @@ func (view *FileTreeView) Setup(v *gocui.View) error { view.updateViewTree() view.Render() + headerStr := fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") + fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false))) + return nil } @@ -110,6 +122,8 @@ func (view *FileTreeView) setLayer(layerIndex int) error { } func (view *FileTreeView) CursorDown() error { + // cannot easily (quickly) check the model length, allow the view + // to let us know what is a valid bounds (i.e. when it hits an empty line) err := CursorDown(view.gui, view.view) if err == nil { view.TreeIndex++ @@ -118,9 +132,11 @@ func (view *FileTreeView) CursorDown() error { } func (view *FileTreeView) CursorUp() error { - err := CursorUp(view.gui, view.view) - if err == nil { - view.TreeIndex-- + if view.TreeIndex > 0 { + err := CursorUp(view.gui, view.view) + if err == nil { + view.TreeIndex-- + } } return view.Render() } @@ -130,16 +146,11 @@ func (view *FileTreeView) getAbsPositionNode() (node *filetree.FileNode) { var evaluator func(*filetree.FileNode) bool var dfsCounter int - // special case: the root node is never visited - if view.TreeIndex == 0 { - return view.ModelTree.Root - } - visiter = func(curNode *filetree.FileNode) error { - dfsCounter++ if dfsCounter == view.TreeIndex { node = curNode } + dfsCounter++ return nil } var filterBytes []byte @@ -236,12 +247,12 @@ func (view *FileTreeView) KeyHelp() string { func (view *FileTreeView) Render() error { // print the tree to the view - lines := strings.Split(view.ViewTree.String(), "\n") + lines := strings.Split(view.ViewTree.String(true), "\n") view.gui.Update(func(g *gocui.Gui) error { view.view.Clear() for idx, line := range lines { if idx == view.TreeIndex { - fmt.Fprintln(view.view, Formatting.Header(line)) + fmt.Fprintln(view.view, Formatting.StatusBar(vtclean.Clean(line, false))) } else { fmt.Fprintln(view.view, line) } diff --git a/ui/layerview.go b/ui/layerview.go index 76af82b..d100194 100644 --- a/ui/layerview.go +++ b/ui/layerview.go @@ -5,12 +5,14 @@ import ( "github.com/jroimartin/gocui" "github.com/wagoodman/docker-image-explorer/image" + "github.com/lunixbochs/vtclean" ) type LayerView struct { Name string gui *gocui.Gui view *gocui.View + header *gocui.View LayerIndex int Layers []*image.Layer } @@ -26,16 +28,22 @@ func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerview return layerview } -func (view *LayerView) Setup(v *gocui.View) error { +func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error { // set view options view.view = v + view.view.Editable = false view.view.Wrap = false //view.view.Highlight = true //view.view.SelBgColor = gocui.ColorGreen //view.view.SelFgColor = gocui.ColorBlack view.view.Frame = false + view.header = header + view.header.Editable = false + view.header.Wrap = false + view.header.Frame = false + // set keybindings if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil { return err @@ -44,9 +52,10 @@ func (view *LayerView) Setup(v *gocui.View) error { return err } - view.Render() + headerStr := fmt.Sprintf(image.LayerFormat, "Image ID", "Size", "Command") + fmt.Fprintln(view.header, Formatting.Header(vtclean.Clean(headerStr, false))) - return nil + return view.Render() } func (view *LayerView) Render() error { @@ -57,7 +66,7 @@ func (view *LayerView) Render() error { idx := (len(view.Layers)-1) - revIdx if idx == view.LayerIndex { - fmt.Fprintln(view.view, Formatting.Header(layer.String())) + fmt.Fprintln(view.view, Formatting.StatusBar(layer.String())) } else { fmt.Fprintln(view.view, layer.String()) } @@ -70,7 +79,7 @@ func (view *LayerView) Render() error { } func (view *LayerView) CursorDown() error { - if int(view.LayerIndex) < len(view.Layers) { + if view.LayerIndex < len(view.Layers) { err := CursorDown(view.gui, view.view) if err == nil { view.LayerIndex++ @@ -82,7 +91,7 @@ func (view *LayerView) CursorDown() error { } func (view *LayerView) CursorUp() error { - if int(view.LayerIndex) > 0 { + if view.LayerIndex > 0 { err := CursorUp(view.gui, view.view) if err == nil { view.LayerIndex-- diff --git a/ui/statusview.go b/ui/statusview.go index b661810..5d440c8 100644 --- a/ui/statusview.go +++ b/ui/statusview.go @@ -23,7 +23,7 @@ func NewStatusView(name string, gui *gocui.Gui) (statusview *StatusView) { return statusview } -func (view *StatusView) Setup(v *gocui.View) error { +func (view *StatusView) Setup(v *gocui.View, header *gocui.View) error { // set view options view.view = v diff --git a/ui/ui.go b/ui/ui.go index e3d0136..10cf544 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "errors" "log" "github.com/fatih/color" @@ -12,7 +13,8 @@ import ( const debug = false var Formatting struct { - Header func(...interface{}) string + Header func(...interface{}) string + StatusBar func(...interface{}) string } var Views struct { @@ -24,7 +26,7 @@ var Views struct { } type View interface { - Setup(*gocui.View) error + Setup(*gocui.View, *gocui.View) error CursorDown() error CursorUp() error Render() error @@ -63,7 +65,7 @@ func CursorDown(g *gocui.Gui, v *gocui.View) error { // todo: handle error } if len(line) == 0 { - return nil + return errors.New("unable to move cursor down, empty line") } if err := v.SetCursor(cx, cy+1); err != nil { ox, oy := v.Origin() @@ -121,24 +123,40 @@ func layout(g *gocui.Gui) error { } debugCols := maxX - debugWidth bottomRows := 1 - if view, err := g.SetView(Views.Layer.Name, -1, -1, splitCols, maxY-bottomRows); err != nil { + headerRows := 1 + + // Layers + if view, err := g.SetView(Views.Layer.Name, -1, -1+headerRows, splitCols, maxY-bottomRows); err != nil { if err != gocui.ErrUnknownView { return err } - Views.Layer.Setup(view) + if header, err := g.SetView(Views.Layer.Name+"header", -1, -1, splitCols, headerRows); err != nil { + if err != gocui.ErrUnknownView { + return err + } + Views.Layer.Setup(view, header) - if _, err := g.SetCurrentView(Views.Layer.Name); err != nil { - return err + if _, err := g.SetCurrentView(Views.Layer.Name); err != nil { + return err + } } } - if view, err := g.SetView(Views.Tree.Name, splitCols, -1, debugCols, maxY-bottomRows); err != nil { + // Filetree + if view, err := g.SetView(Views.Tree.Name, splitCols, -1+headerRows, debugCols, maxY-bottomRows); err != nil { if err != gocui.ErrUnknownView { return err } - Views.Tree.Setup(view) + if header, err := g.SetView(Views.Tree.Name+"header", splitCols, -1, debugCols, headerRows); err != nil { + if err != gocui.ErrUnknownView { + return err + } + Views.Tree.Setup(view, header) + } } + + // Debug pane if debug { if _, err := g.SetView("debug", debugCols, -1, maxX, maxY-bottomRows); err != nil { if err != gocui.ErrUnknownView { @@ -146,11 +164,13 @@ func layout(g *gocui.Gui) error { } } } + + // StatusBar if view, err := g.SetView(Views.Status.Name, -1, maxY-bottomRows-1, maxX, maxY); err != nil { if err != gocui.ErrUnknownView { return err } - Views.Status.Setup(view) + Views.Status.Setup(view, nil) } if view, err := g.SetView(Views.Command.Name, -1, maxY-bottomRows-2, maxX, maxY-1); err != nil { @@ -171,7 +191,8 @@ func Render() { } func Run(layers []*image.Layer, refTrees []*filetree.FileTree) { - Formatting.Header = color.New(color.ReverseVideo, color.Bold).SprintFunc() + Formatting.StatusBar = color.New(color.ReverseVideo, color.Bold).SprintFunc() + Formatting.Header = color.New(color.Bold).SprintFunc() g, err := gocui.NewGui(gocui.OutputNormal) if err != nil {