diff options
author | Alex Goodman <wagoodman@gmail.com> | 2019-09-21 16:28:45 -0400 |
---|---|---|
committer | Alex Goodman <wagoodman@gmail.com> | 2019-09-21 16:28:45 -0400 |
commit | 576709ad30571d7fb11d15229719c693cf0e92c4 (patch) | |
tree | debb1e2e4a4cb305149e51f4856e600d68a4c719 | |
parent | 4b73bc89b8bb9782443c569ecb5edbb3a8f6260c (diff) |
rework package structure
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | cmd/root.go | 2 | ||||
-rw-r--r-- | filetree/cache.go | 79 | ||||
-rw-r--r-- | filetree/data.go | 161 | ||||
-rw-r--r-- | filetree/data_test.go | 41 | ||||
-rw-r--r-- | filetree/efficiency.go | 111 | ||||
-rw-r--r-- | filetree/efficiency_test.go | 80 | ||||
-rw-r--r-- | filetree/node.go | 315 | ||||
-rw-r--r-- | filetree/node_test.go | 168 | ||||
-rw-r--r-- | filetree/tree.go | 371 | ||||
-rw-r--r-- | filetree/tree_test.go | 791 | ||||
-rw-r--r-- | filetree/types.go | 66 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | image/docker_image.go | 293 | ||||
-rw-r--r-- | image/docker_layer.go | 78 | ||||
-rw-r--r-- | image/root.go | 10 | ||||
-rw-r--r-- | image/testing.go | 19 | ||||
-rw-r--r-- | image/types.go | 80 | ||||
-rw-r--r-- | runtime/ci/evaluator.go (renamed from runtime/ci_evaluator.go) | 11 | ||||
-rw-r--r-- | runtime/ci/evaluator_test.go (renamed from runtime/ci_evaluator_test.go) | 6 | ||||
-rw-r--r-- | runtime/ci/reference_file.go (renamed from runtime/reference_file.go) | 2 | ||||
-rw-r--r-- | runtime/ci/rule.go (renamed from runtime/ci_rule.go) | 6 | ||||
-rw-r--r-- | runtime/export/export.go (renamed from runtime/export.go) | 27 | ||||
-rw-r--r-- | runtime/export/export_test.go (renamed from runtime/export_test.go) | 11 | ||||
-rw-r--r-- | runtime/run.go | 32 | ||||
-rw-r--r-- | runtime/ui/details_controller.go (renamed from ui/details_controller.go) | 2 | ||||
-rw-r--r-- | runtime/ui/filetree_controller.go (renamed from ui/filetree_controller.go) | 2 | ||||
-rw-r--r-- | runtime/ui/filetree_viewmodel.go (renamed from ui/filetree_viewmodel.go) | 2 | ||||
-rw-r--r-- | runtime/ui/filetree_viewmodel_test.go (renamed from ui/filetree_viewmodel_test.go) | 6 | ||||
-rw-r--r-- | runtime/ui/filter_controller.go (renamed from ui/filter_controller.go) | 0 | ||||
-rw-r--r-- | runtime/ui/layer_controller.go (renamed from ui/layer_controller.go) | 3 | ||||
-rw-r--r-- | runtime/ui/status_controller.go (renamed from ui/status_controller.go) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileShowAggregateChanges.txt (renamed from ui/testdata/TestFileShowAggregateChanges.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeDirCollapse.txt (renamed from ui/testdata/TestFileTreeDirCollapse.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeDirCollapseAll.txt (renamed from ui/testdata/TestFileTreeDirCollapseAll.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeDirCursorRight.txt (renamed from ui/testdata/TestFileTreeDirCursorRight.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeFilterTree.txt (renamed from ui/testdata/TestFileTreeFilterTree.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeGoCase.txt (renamed from ui/testdata/TestFileTreeGoCase.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeHideAddedRemovedModified.txt (renamed from ui/testdata/TestFileTreeHideAddedRemovedModified.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeHideTypeWithFilter.txt (renamed from ui/testdata/TestFileTreeHideTypeWithFilter.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeHideUnmodified.txt (renamed from ui/testdata/TestFileTreeHideUnmodified.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeNoAttributes.txt (renamed from ui/testdata/TestFileTreeNoAttributes.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreePageDown.txt (renamed from ui/testdata/TestFileTreePageDown.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreePageUp.txt (renamed from ui/testdata/TestFileTreePageUp.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeRestrictedHeight.txt (renamed from ui/testdata/TestFileTreeRestrictedHeight.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/testdata/TestFileTreeSelectLayer.txt (renamed from ui/testdata/TestFileTreeSelectLayer.txt) | 0 | ||||
-rw-r--r-- | runtime/ui/ui.go (renamed from ui/ui.go) | 4 | ||||
-rw-r--r-- | utils/format.go | 9 |
49 files changed, 72 insertions, 2721 deletions
@@ -30,7 +30,7 @@ test-coverage: build ./.scripts/test-coverage.sh validate: - grep -R 'const allowTestDataCapture = false' ui/ + grep -R 'const allowTestDataCapture = false' runtime/ui/ go vet ./... @! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/' golangci-lint run diff --git a/cmd/root.go b/cmd/root.go index 2ed2534..9dadad2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/wagoodman/dive/dive/filetree" "io/ioutil" "os" "path" @@ -11,7 +12,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/wagoodman/dive/filetree" "github.com/wagoodman/dive/utils" ) diff --git a/filetree/cache.go b/filetree/cache.go deleted file mode 100644 index 82c1795..0000000 --- a/filetree/cache.go +++ /dev/null @@ -1,79 +0,0 @@ -package filetree - -import ( - "github.com/sirupsen/logrus" -) - -type TreeCacheKey struct { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int -} - -type TreeCache struct { - refTrees []*FileTree - cache map[TreeCacheKey]*FileTree -} - -func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) *FileTree { - key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop} - if value, exists := cache.cache[key]; exists { - return value - } - - value := cache.buildTree(key) - cache.cache[key] = value - return value -} - -func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree { - newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop) - for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ { - err := newTree.CompareAndMark(cache.refTrees[idx]) - if err != nil { - logrus.Errorf("unable to build tree: %+v", err) - } - } - return newTree -} - -func (cache *TreeCache) Build() { - var bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int - - // case 1: layer compare (top tree SIZE is fixed (BUT floats forward), Bottom tree SIZE changes) - for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ { - bottomTreeStart = 0 - topTreeStop = selectIdx - - if selectIdx == 0 { - bottomTreeStop = selectIdx - topTreeStart = selectIdx - } else { - bottomTreeStop = selectIdx - 1 - topTreeStart = selectIdx - } - - cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) - } - - // case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes) - for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ { - bottomTreeStart = 0 - topTreeStop = selectIdx - if selectIdx == 0 { - bottomTreeStop = selectIdx - topTreeStart = selectIdx - } else { - bottomTreeStop = 0 - topTreeStart = 1 - } - - cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) - } -} - -func NewFileTreeCache(refTrees []*FileTree) TreeCache { - - return TreeCache{ - refTrees: refTrees, - cache: make(map[TreeCacheKey]*FileTree), - } -} diff --git a/filetree/data.go b/filetree/data.go deleted file mode 100644 index 743ecb0..0000000 --- a/filetree/data.go +++ /dev/null @@ -1,161 +0,0 @@ -package filetree - -import ( - "archive/tar" - "fmt" - "io" - - "github.com/cespare/xxhash" - "github.com/sirupsen/logrus" -) - -const ( - Unmodified DiffType = iota - Modified - Added - Removed -) - -var GlobalFileTreeCollapse bool - -// NewNodeData creates an empty NodeData struct for a FileNode -func NewNodeData() *NodeData { - return &NodeData{ - ViewInfo: *NewViewInfo(), - FileInfo: FileInfo{}, - DiffType: Unmodified, - } -} - -// Copy duplicates a NodeData -func (data *NodeData) Copy() *NodeData { - return &NodeData{ - ViewInfo: *data.ViewInfo.Copy(), - FileInfo: *data.FileInfo.Copy(), - DiffType: data.DiffType, - } -} - -// NewViewInfo creates a default ViewInfo -func NewViewInfo() (view *ViewInfo) { - return &ViewInfo{ - Collapsed: GlobalFileTreeCollapse, - Hidden: false, - } -} - -// Copy duplicates a ViewInfo -func (view *ViewInfo) Copy() (newView *ViewInfo) { - newView = NewViewInfo() - *newView = *view - return newView -} - -func getHashFromReader(reader io.Reader) uint64 { - h := xxhash.New() - - buf := make([]byte, 1024) - for { - n, err := reader.Read(buf) - if err != nil && err != io.EOF { - logrus.Panic(err) - } - if n == 0 { - break - } - - _, err = h.Write(buf[:n]) - if err != nil { - logrus.Panic(err) - } - } - - return h.Sum64() -} - -// NewFileInfo extracts the metadata from a tar header and file contents and generates a new FileInfo object. -func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo { - if header.Typeflag == tar.TypeDir { - return FileInfo{ - Path: path, - TypeFlag: header.Typeflag, - Linkname: header.Linkname, - hash: 0, - Size: header.FileInfo().Size(), - Mode: header.FileInfo().Mode(), - Uid: header.Uid, - Gid: header.Gid, - IsDir: header.FileInfo().IsDir(), - } - } - - hash := getHashFromReader(reader) - - return FileInfo{ - Path: path, - TypeFlag: header.Typeflag, - Linkname: header.Linkname, - hash: hash, - Size: header.FileInfo().Size(), - Mode: header.FileInfo().Mode(), - Uid: header.Uid, - Gid: header.Gid, - IsDir: header.FileInfo().IsDir(), - } -} - -// Copy duplicates a FileInfo -func (data *FileInfo) Copy() *FileInfo { - if data == nil { - return nil - } - return &FileInfo{ - Path: data.Path, - TypeFlag: data.TypeFlag, - Linkname: data.Linkname, - hash: data.hash, - Size: data.Size, - Mode: data.Mode, - Uid: data.Uid, - Gid: data.Gid, - IsDir: data.IsDir, - } -} - -// Compare determines the DiffType between two FileInfos based on the type and contents of each given FileInfo -func (data *FileInfo) Compare(other FileInfo) DiffType { - if data.TypeFlag == other.TypeFlag { - if data.hash == other.hash && - data.Mode == other.Mode && - data.Uid == other.Uid && - data.Gid == other.Gid { - return Unmodified - } - } - return Modified -} - -// String of a DiffType -func (diff DiffType) String() string { - switch diff { - case Unmodified: - return "Unmodified" - case Modified: - return "Modified" - case Added: - return "Added" - case Removed: - return "Removed" - default: - return fmt.Sprintf("%d", int(diff)) - } -} - -// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ, -// in which case we can only determine that there is "a change". -func (diff DiffType) merge(other DiffType) DiffType { - if diff == other { - return diff - } - return Modified -} diff --git a/filetree/data_test.go b/filetree/data_test.go deleted file mode 100644 index 351d6df..0000000 --- a/filetree/data_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package filetree - -import ( - "testing" -) - -func TestAssignDiffType(t *testing.T) { - tree := NewFileTree() - node, _, err := tree.AddPath("/usr", *BlankFileChangeInfo("/usr")) - if err != nil { - t.Errorf("Expected no error from fetching path. got: %v", err) - } - node.Data.DiffType = Modified - if tree.Root.Children["usr"].Data.DiffType != Modified { - t.Fail() - } -} - -func TestMergeDiffTypes(t *testing.T) { - a := Unmodified - b := Unmodified - merged := a.merge(b) - if merged != Unmodified { - t.Errorf("Expected Unchaged (0) but got %v", merged) - } - a = Modified - b = Unmodified - merged = a.merge(b) - if merged != Modified { - t.Errorf("Expected Unchaged (0) but got %v", merged) - } -} - -func BlankFileChangeInfo(path string) (f *FileInfo) { - result := FileInfo{ - Path: path, - TypeFlag: 1, - hash: 123, - } - return &result -} diff --git a/filetree/efficiency.go b/filetree/efficiency.go deleted file mode 100644 index e0b21cf..0000000 --- a/filetree/efficiency.go +++ /dev/null @@ -1,111 +0,0 @@ -package filetree - -import ( - "fmt" - "sort" - - "github.com/sirupsen/logrus" -) - -// Len is required for sorting. -func (efs EfficiencySlice) Len() int { - return len(efs) -} - -// Swap operation is required for sorting. -func (efs EfficiencySlice) Swap(i, j int) { - efs[i], efs[j] = efs[j], efs[i] -} - -// Less comparison is required for sorting. -func (efs EfficiencySlice) Less(i, j int) bool { - return efs[i].CumulativeSize < efs[j].CumulativeSize -} - -// Efficiency returns the score and file set of the given set of FileTrees (layers). This is loosely based on: -// 1. Files that are duplicated across layers discounts your score, weighted by file size -// 2. Files that are removed discounts your score, weighted by the original file size -func Efficiency(trees []*FileTree) (float64, EfficiencySlice) { - efficiencyMap := make(map[string]*EfficiencyData) - inefficientMatches := make(EfficiencySlice, 0) - currentTree := 0 - - visitor := func(node *FileNode) error { - path := node.Path() - if _, ok := efficiencyMap[path]; !ok { - efficiencyMap[path] = &EfficiencyData{ - Path: path, - Nodes: make([]*FileNode, 0), - minDiscoveredSize: -1, - } - } - data := efficiencyMap[path] - - // this node may have had children that were deleted, however, we won't explicitly list out every child, only - // the top-most parent with the cumulative size. These operations will need to be done on the full (stacked) - // tree. - // Note: whiteout files may also represent directories, so we need to find out if this was previously a file or dir. - var sizeBytes int64 - - if node.IsWhiteout() { - sizer := func(curNode *FileNode) error { - sizeBytes += curNode.Data.FileInfo.Size - return nil - } - stackedTree := StackTreeRange(trees, 0, currentTree-1) - previousTreeNode, err := stackedTree.GetNode(node.Path()) - if err != nil { - logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err)) - } else if previousTreeNode.Data.FileInfo.IsDir { - err = previousTreeNode.VisitDepthChildFirst(sizer, nil) - if err != nil { - logrus.Errorf("unable to propagate whiteout dir: %+v", err) - } - } - - } else { - sizeBytes = node.Data.FileInfo.Size - } - - data.CumulativeSize += sizeBytes - if data.minDiscoveredSize < 0 || sizeBytes < data.minDiscoveredSize { - data.minDiscoveredSize = sizeBytes - } - data.Nodes = append(data.Nodes, node) - - if len(data.Nodes) == 2 { - inefficientMatches = append(inefficientMatches, data) - } - - return nil - } - visitEvaluator := func(node *FileNode) bool { - return node.IsLeaf() - } - for idx, tree := range trees { - currentTree = idx - err := tree.VisitDepthChildFirst(visitor, visitEvaluator) - if err != nil { - logrus.Errorf("unable to propagate ref tree: %+v", err) - } - } - - // calculate the score - var minimumPathSizes int64 - var discoveredPathSizes int64 - - for _, value := range efficiencyMap { - minimumPathSizes += value.minDiscoveredSize - discoveredPathSizes += value.CumulativeSize - } - var score float64 - if discoveredPathSizes == 0 { - score = 1.0 - } else { - score = float64(minimumPathSizes) / float64(discoveredPathSizes) - } - - sort.Sort(inefficientMatches) - - return score, inefficientMatches -} diff --git a/filetree/efficiency_test.go b/filetree/efficiency_test.go deleted file mode 100644 index 831ceb5..0000000 --- a/filetree/efficiency_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package filetree - -import ( - "testing" -) - -func checkError(t *testing.T, err error, message string) { - if err != nil { - t.Errorf(message+": %+v", err) - } -} - -func TestEfficency(t *testing.T) { - trees := make([]*FileTree, 3) - for idx := range trees { - trees[idx] = NewFileTree() - } - - _, _, err := trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000}) - checkError(t, err, "could not setup test") - - _, _, err = trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000}) - checkError(t, err, "could not setup test") - - _, _, err = trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000}) - checkError(t, err, "could not setup test") - _, _, err = trees[1].AddPath("/etc/athing", FileInfo{Size: 10000}) - checkError(t, err, "could not setup test") - - _, _, err = trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx")) - checkError(t, err, "could not setup test") - - var expectedScore = 0.75 - var expectedMatches = EfficiencySlice{ - &EfficiencyData{Path: "/etc/nginx/nginx.conf", CumulativeSize: 7000}, - } - actualScore, actualMatches := Efficiency(trees) - - if expectedScore != actualScore { - t.Errorf("Expected score of %v but go %v", expectedScore, actualScore) - } - - if len(actualMatches) != len(expectedMatches) { - for _, match := range actualMatches { - t.Logf(" match: %+v", match) - } - t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches)) - } - - if expectedMatches[0].Path != actualMatches[0].Path { - t.Errorf("Expected path of %s but go %s", expectedMatches[0].Path, actualMatches[0].Path) - } - - if expectedMatches[0].CumulativeSize != actualMatches[0].CumulativeSize { - t.Errorf("Expected cumulative size of %v but go %v", expectedMatches[0].CumulativeSize, actualMatches[0].CumulativeSize) - } -} - -func TestEfficency_ScratchImage(t *testing.T) { - trees := make([]*FileTree, 3) - for idx := range trees { - trees[idx] = NewFileTree() - } - - _, _, err := trees[0].AddPath("/nothing", FileInfo{Size: 0}) - checkError(t, err, "could not setup test") - - var expectedScore = 1.0 - var expectedMatches = EfficiencySlice{} - actualScore, actualMatches := Efficiency(trees) - - if expectedScore != actualScore { - t.Errorf("Expected score of %v but go %v", expectedScore, actualScore) - } - - if len(actualMatches) > 0 { - t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches)) - } - -} diff --git a/filetree/node.go b/filetree/node.go deleted file mode 100644 index d136f0d..0000000 --- a/filetree/node.go +++ /dev/null @@ -1,315 +0,0 @@ -package filetree - -import ( - "archive/tar" - "fmt" - "sort" - "strings" - - "github.com/sirupsen/logrus" - - "github.com/dustin/go-humanize" - "github.com/fatih/color" - "github.com/phayes/permbits" -) - -const ( - AttributeFormat = "%s%s %11s %10s " -) - -var diffTypeColor = map[DiffType]*color.Color{ - Added: color.New(color.FgGreen), - Removed: color.New(color.FgRed), - Modified: color.New(color.FgYellow), - Unmodified: color.New(color.Reset), -} - -// NewNode creates a new FileNode relative to the given parent node with a payload. -func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) { - node = new(FileNode) - node.Name = name - node.Data = *NewNodeData() - node.Data.FileInfo = *data.Copy() - - node.Children = make(map[string]*FileNode) - node.Parent = parent - if parent != nil { - node.Tree = parent.Tree - } - - return node -} - -// renderTreeLine returns a string representing this FileNode in the context of a greater ASCII tree. -func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string { - var otherBranches string - for _, space := range spaces { - if space { - otherBranches += noBranchSpace - } else { - otherBranches += branchSpace - } - } - - thisBranch := middleItem - if last { - thisBranch = lastItem - } - - collapsedIndicator := uncollapsedItem - if collapsed { - collapsedIndicator = collapsedItem - } - - return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine -} - -// Copy duplicates the existing node relative to a new parent node. -func (node *FileNode) Copy(parent *FileNode) *FileNode { - newNode := NewNode(parent, node.Name, node.Data.FileInfo) - newNode.Data.ViewInfo = node.Data.ViewInfo - newNode.Data.DiffType = node.Data.DiffType - for name, child := range node.Children { - newNode.Children[name] = child.Copy(newNode) - child.Parent = newNode - } - return newNode -} - -// AddChild creates a new node relative to the current FileNode. -func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) { - // never allow processing of purely whiteout flag files (for now) - if strings.HasPrefix(name, doubleWhiteoutPrefix) { - return nil - } - - 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() - } else { - node.Children[name] = child - node.Tree.Size++ - } - - return child -} - -// Remove deletes the current FileNode from it's parent FileNode's relations. -func (node *FileNode) Remove() error { - if node == node.Tree.Root { - return fmt.Errorf("cannot remove the tree root") - } - for _, child := range node.Children { - err := child.Remove() - if err != nil { - return err - } - } - delete(node.Parent.Children, node.Name) - node.Tree.Size-- - return nil -} - -// String shows the filename formatted into the proper color (by DiffType), additionally indicating if it is a symlink. -func (node *FileNode) String() string { - var display string - if node == nil { - return "" - } - - display = node.Name - if node.Data.FileInfo.TypeFlag == tar.TypeSymlink || node.Data.FileInfo.TypeFlag == tar.TypeLink { - display += " → " + node.Data.FileInfo.Linkname - } - return diffTypeColor[node.Data.DiffType].Sprint(display) -} - -// MetadatString returns the FileNode metadata in a columnar string. -func (node *FileNode) MetadataSt |