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