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/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/tree.go b/filetree/tree.go index e1b4efe..921cc1e 100644 --- a/filetree/tree.go +++ b/filetree/tree.go @@ -33,10 +33,10 @@ func NewFileTree() (tree *FileTree) { } func (tree *FileTree) String() string { - var renderLine func(string, []bool, bool, 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,7 @@ 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) + result += child.MetadataString() + " " + 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) @@ -131,7 +131,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 +145,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 +159,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/image/image.go b/image/image.go index 96bc0cd..6cdd462 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 50110b2..5bbfd99 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -8,12 +8,14 @@ import ( "github.com/wagoodman/docker-image-explorer/filetree" "github.com/fatih/color" "strings" + "github.com/lunixbochs/vtclean" ) type FileTreeView struct { Name string gui *gocui.Gui view *gocui.View + header *gocui.View TreeIndex int ModelTree *filetree.FileTree ViewTree *filetree.FileTree @@ -34,7 +36,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 @@ -43,7 +45,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 { @@ -71,6 +78,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 } @@ -200,7 +210,7 @@ func (view *FileTreeView) Render() 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..77c3e6f 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()) } diff --git a/ui/statusview.go b/ui/statusview.go index 719d097..99cc579 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 b5ee34a..0ac6759 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -13,6 +13,7 @@ const debug = false var Formatting struct { Header func(...interface{})(string) + StatusBar func(...interface{})(string) } var Views struct { @@ -23,7 +24,7 @@ var Views struct { } type View interface { - Setup(*gocui.View) error + Setup(*gocui.View, *gocui.View) error CursorDown() error CursorUp() error Render() error @@ -102,24 +103,41 @@ 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 { @@ -127,11 +145,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) } @@ -145,7 +165,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 {