1package fs 2 3import ( 4 "io" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 9 "github.com/pkg/errors" 10 "gotest.tools/assert" 11) 12 13// Manifest stores the expected structure and properties of files and directories 14// in a filesystem. 15type Manifest struct { 16 root *directory 17} 18 19type resource struct { 20 mode os.FileMode 21 uid uint32 22 gid uint32 23} 24 25type file struct { 26 resource 27 content io.ReadCloser 28} 29 30func (f *file) Type() string { 31 return "file" 32} 33 34type symlink struct { 35 resource 36 target string 37} 38 39func (f *symlink) Type() string { 40 return "symlink" 41} 42 43type directory struct { 44 resource 45 items map[string]dirEntry 46} 47 48func (f *directory) Type() string { 49 return "directory" 50} 51 52type dirEntry interface { 53 Type() string 54} 55 56// ManifestFromDir creates a Manifest by reading the directory at path. The 57// manifest stores the structure and properties of files in the directory. 58// ManifestFromDir can be used with Equal to compare two directories. 59func ManifestFromDir(t assert.TestingT, path string) Manifest { 60 if ht, ok := t.(helperT); ok { 61 ht.Helper() 62 } 63 64 manifest, err := manifestFromDir(path) 65 assert.NilError(t, err) 66 return manifest 67} 68 69func manifestFromDir(path string) (Manifest, error) { 70 info, err := os.Stat(path) 71 switch { 72 case err != nil: 73 return Manifest{}, err 74 case !info.IsDir(): 75 return Manifest{}, errors.Errorf("path %s must be a directory", path) 76 } 77 78 directory, err := newDirectory(path, info) 79 return Manifest{root: directory}, err 80} 81 82func newDirectory(path string, info os.FileInfo) (*directory, error) { 83 items := make(map[string]dirEntry) 84 children, err := ioutil.ReadDir(path) 85 if err != nil { 86 return nil, err 87 } 88 for _, child := range children { 89 fullPath := filepath.Join(path, child.Name()) 90 items[child.Name()], err = getTypedResource(fullPath, child) 91 if err != nil { 92 return nil, err 93 } 94 } 95 96 return &directory{ 97 resource: newResourceFromInfo(info), 98 items: items, 99 }, nil 100} 101 102func getTypedResource(path string, info os.FileInfo) (dirEntry, error) { 103 switch { 104 case info.IsDir(): 105 return newDirectory(path, info) 106 case info.Mode()&os.ModeSymlink != 0: 107 return newSymlink(path, info) 108 // TODO: devices, pipes? 109 default: 110 return newFile(path, info) 111 } 112} 113 114func newSymlink(path string, info os.FileInfo) (*symlink, error) { 115 target, err := os.Readlink(path) 116 return &symlink{ 117 resource: newResourceFromInfo(info), 118 target: target, 119 }, err 120} 121 122func newFile(path string, info os.FileInfo) (*file, error) { 123 // TODO: defer file opening to reduce number of open FDs? 124 readCloser, err := os.Open(path) 125 return &file{ 126 resource: newResourceFromInfo(info), 127 content: readCloser, 128 }, err 129} 130