blob: d396756376b3016a0aa0919b39e87f181412cf01 [file] [log] [blame]
package fs
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
// Manifest stores the expected structure and properties of files and directories
// in a filesystem.
type Manifest struct {
root *directory
}
type resource struct {
mode os.FileMode
uid uint32
gid uint32
}
type file struct {
resource
content io.ReadCloser
ignoreCariageReturn bool
compareContentFunc func(b []byte) CompareResult
}
func (f *file) Type() string {
return "file"
}
type symlink struct {
resource
target string
}
func (f *symlink) Type() string {
return "symlink"
}
type directory struct {
resource
items map[string]dirEntry
filepathGlobs map[string]*filePath
}
func (f *directory) Type() string {
return "directory"
}
type dirEntry interface {
Type() string
}
// ManifestFromDir creates a Manifest by reading the directory at path. The
// manifest stores the structure and properties of files in the directory.
// ManifestFromDir can be used with Equal to compare two directories.
func ManifestFromDir(t assert.TestingT, path string) Manifest {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
manifest, err := manifestFromDir(path)
assert.NilError(t, err)
return manifest
}
func manifestFromDir(path string) (Manifest, error) {
info, err := os.Stat(path)
switch {
case err != nil:
return Manifest{}, err
case !info.IsDir():
return Manifest{}, errors.Errorf("path %s must be a directory", path)
}
directory, err := newDirectory(path, info)
return Manifest{root: directory}, err
}
func newDirectory(path string, info os.FileInfo) (*directory, error) {
items := make(map[string]dirEntry)
children, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
for _, child := range children {
fullPath := filepath.Join(path, child.Name())
items[child.Name()], err = getTypedResource(fullPath, child)
if err != nil {
return nil, err
}
}
return &directory{
resource: newResourceFromInfo(info),
items: items,
filepathGlobs: make(map[string]*filePath),
}, nil
}
func getTypedResource(path string, info os.FileInfo) (dirEntry, error) {
switch {
case info.IsDir():
return newDirectory(path, info)
case info.Mode()&os.ModeSymlink != 0:
return newSymlink(path, info)
// TODO: devices, pipes?
default:
return newFile(path, info)
}
}
func newSymlink(path string, info os.FileInfo) (*symlink, error) {
target, err := os.Readlink(path)
if err != nil {
return nil, err
}
return &symlink{
resource: newResourceFromInfo(info),
target: target,
}, err
}
func newFile(path string, info os.FileInfo) (*file, error) {
// TODO: defer file opening to reduce number of open FDs?
readCloser, err := os.Open(path)
if err != nil {
return nil, err
}
return &file{
resource: newResourceFromInfo(info),
content: readCloser,
}, err
}