added file attributes + headers
This commit is contained in:
parent
90c86234c4
commit
3590a7cf46
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,7 +12,7 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
/tmp
|
||||
/build
|
||||
/_vendor*
|
||||
/vendor
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
|
39
ui/ui.go
39
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user