2018-06-08 12:23:38 -04:00
|
|
|
package fs
|
|
|
|
|
|
|
|
import (
|
2022-09-22 09:38:19 -04:00
|
|
|
"fmt"
|
2018-06-08 12:23:38 -04:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
2020-02-22 12:12:14 -05:00
|
|
|
"gotest.tools/v3/assert"
|
2018-06-08 12:23:38 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
2018-11-14 06:28:16 -05:00
|
|
|
content io.ReadCloser
|
|
|
|
ignoreCariageReturn bool
|
2019-04-12 19:47:37 -04:00
|
|
|
compareContentFunc func(b []byte) CompareResult
|
2018-06-08 12:23:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *file) Type() string {
|
|
|
|
return "file"
|
|
|
|
}
|
|
|
|
|
|
|
|
type symlink struct {
|
|
|
|
resource
|
|
|
|
target string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *symlink) Type() string {
|
|
|
|
return "symlink"
|
|
|
|
}
|
|
|
|
|
|
|
|
type directory struct {
|
|
|
|
resource
|
2019-04-12 19:47:37 -04:00
|
|
|
items map[string]dirEntry
|
|
|
|
filepathGlobs map[string]*filePath
|
2018-06-08 12:23:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *directory) Type() string {
|
|
|
|
return "directory"
|
|
|
|
}
|
|
|
|
|
|
|
|
type dirEntry interface {
|
|
|
|
Type() string
|
|
|
|
}
|
|
|
|
|
2023-10-20 11:39:10 -04:00
|
|
|
// ManifestFromDir creates a [Manifest] by reading the directory at path. The
|
2018-06-08 12:23:38 -04:00
|
|
|
// manifest stores the structure and properties of files in the directory.
|
2023-10-20 11:39:10 -04:00
|
|
|
// ManifestFromDir can be used with [Equal] to compare two directories.
|
2018-06-08 12:23:38 -04:00
|
|
|
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():
|
2022-09-22 09:38:19 -04:00
|
|
|
return Manifest{}, fmt.Errorf("path %s must be a directory", path)
|
2018-06-08 12:23:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
directory, err := newDirectory(path, info)
|
|
|
|
return Manifest{root: directory}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func newDirectory(path string, info os.FileInfo) (*directory, error) {
|
|
|
|
items := make(map[string]dirEntry)
|
2022-11-05 18:25:29 -04:00
|
|
|
children, err := os.ReadDir(path)
|
2018-06-08 12:23:38 -04:00
|
|
|
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{
|
2019-04-12 19:47:37 -04:00
|
|
|
resource: newResourceFromInfo(info),
|
|
|
|
items: items,
|
|
|
|
filepathGlobs: make(map[string]*filePath),
|
2018-06-08 12:23:38 -04:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-11-05 18:25:29 -04:00
|
|
|
func getTypedResource(path string, entry os.DirEntry) (dirEntry, error) {
|
|
|
|
info, err := entry.Info()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-08 12:23:38 -04:00
|
|
|
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)
|
2018-11-14 06:28:16 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-08 12:23:38 -04:00
|
|
|
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)
|
2018-11-14 06:28:16 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-08 12:23:38 -04:00
|
|
|
return &file{
|
|
|
|
resource: newResourceFromInfo(info),
|
|
|
|
content: readCloser,
|
|
|
|
}, err
|
|
|
|
}
|