blob: c3c33587b60001b87f056450e1e4fad98dd3ab6a [file] [log] [blame]
package fs
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/pkg/errors"
"gotest.tools/assert"
)
const defaultFileMode = 0644
// PathOp is a function which accepts a Path and performs an operation on that
// path. When called with real filesystem objects (File or Dir) a PathOp modifies
// the filesystem at the path. When used with a Manifest object a PathOp updates
// the manifest to expect a value.
type PathOp func(path Path) error
type manifestResource interface {
SetMode(mode os.FileMode)
SetUID(uid uint32)
SetGID(gid uint32)
}
type manifestFile interface {
manifestResource
SetContent(content io.ReadCloser)
}
type manifestDirectory interface {
manifestResource
AddSymlink(path, target string) error
AddFile(path string, ops ...PathOp) error
AddDirectory(path string, ops ...PathOp) error
}
// WithContent writes content to a file at Path
func WithContent(content string) PathOp {
return func(path Path) error {
if m, ok := path.(manifestFile); ok {
m.SetContent(ioutil.NopCloser(strings.NewReader(content)))
return nil
}
return ioutil.WriteFile(path.Path(), []byte(content), defaultFileMode)
}
}
// WithBytes write bytes to a file at Path
func WithBytes(raw []byte) PathOp {
return func(path Path) error {
if m, ok := path.(manifestFile); ok {
m.SetContent(ioutil.NopCloser(bytes.NewReader(raw)))
return nil
}
return ioutil.WriteFile(path.Path(), raw, defaultFileMode)
}
}
// AsUser changes ownership of the file system object at Path
func AsUser(uid, gid int) PathOp {
return func(path Path) error {
if m, ok := path.(manifestResource); ok {
m.SetUID(uint32(uid))
m.SetGID(uint32(gid))
return nil
}
return os.Chown(path.Path(), uid, gid)
}
}
// WithFile creates a file in the directory at path with content
func WithFile(filename, content string, ops ...PathOp) PathOp {
return func(path Path) error {
if m, ok := path.(manifestDirectory); ok {
ops = append([]PathOp{WithContent(content), WithMode(defaultFileMode)}, ops...)
return m.AddFile(filename, ops...)
}
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
if err := createFile(fullpath, content); err != nil {
return err
}
return applyPathOps(&File{path: fullpath}, ops)
}
}
func createFile(fullpath string, content string) error {
return ioutil.WriteFile(fullpath, []byte(content), defaultFileMode)
}
// WithFiles creates all the files in the directory at path with their content
func WithFiles(files map[string]string) PathOp {
return func(path Path) error {
if m, ok := path.(manifestDirectory); ok {
for filename, content := range files {
// TODO: remove duplication with WithFile
if err := m.AddFile(filename, WithContent(content), WithMode(defaultFileMode)); err != nil {
return err
}
}
return nil
}
for filename, content := range files {
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
if err := createFile(fullpath, content); err != nil {
return err
}
}
return nil
}
}
// FromDir copies the directory tree from the source path into the new Dir
func FromDir(source string) PathOp {
return func(path Path) error {
if _, ok := path.(manifestDirectory); ok {
return errors.New("use manifest.FromDir")
}
return copyDirectory(source, path.Path())
}
}
// WithDir creates a subdirectory in the directory at path. Additional PathOp
// can be used to modify the subdirectory
func WithDir(name string, ops ...PathOp) PathOp {
const defaultMode = 0755
return func(path Path) error {
if m, ok := path.(manifestDirectory); ok {
ops = append([]PathOp{WithMode(defaultMode)}, ops...)
return m.AddDirectory(name, ops...)
}
fullpath := filepath.Join(path.Path(), filepath.FromSlash(name))
err := os.MkdirAll(fullpath, defaultMode)
if err != nil {
return err
}
return applyPathOps(&Dir{path: fullpath}, ops)
}
}
// Apply the PathOps to the File
func Apply(t assert.TestingT, path Path, ops ...PathOp) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
assert.NilError(t, applyPathOps(path, ops))
}
func applyPathOps(path Path, ops []PathOp) error {
for _, op := range ops {
if err := op(path); err != nil {
return err
}
}
return nil
}
// WithMode sets the file mode on the directory or file at path
func WithMode(mode os.FileMode) PathOp {
return func(path Path) error {
if m, ok := path.(manifestResource); ok {
m.SetMode(mode)
return nil
}
return os.Chmod(path.Path(), mode)
}
}
func copyDirectory(source, dest string) error {
entries, err := ioutil.ReadDir(source)
if err != nil {
return err
}
for _, entry := range entries {
sourcePath := filepath.Join(source, entry.Name())
destPath := filepath.Join(dest, entry.Name())
switch {
case entry.IsDir():
if err := os.Mkdir(destPath, 0755); err != nil {
return err
}
if err := copyDirectory(sourcePath, destPath); err != nil {
return err
}
case entry.Mode()&os.ModeSymlink != 0:
if err := copySymLink(sourcePath, destPath); err != nil {
return err
}
default:
if err := copyFile(sourcePath, destPath); err != nil {
return err
}
}
}
return nil
}
func copySymLink(source, dest string) error {
link, err := os.Readlink(source)
if err != nil {
return err
}
return os.Symlink(link, dest)
}
func copyFile(source, dest string) error {
content, err := ioutil.ReadFile(source)
if err != nil {
return err
}
return ioutil.WriteFile(dest, content, 0644)
}
// WithSymlink creates a symlink in the directory which links to target.
// Target must be a path relative to the directory.
//
// Note: the argument order is the inverse of os.Symlink to be consistent with
// the other functions in this package.
func WithSymlink(path, target string) PathOp {
return func(root Path) error {
if v, ok := root.(manifestDirectory); ok {
return v.AddSymlink(path, target)
}
return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
}
}
// WithHardlink creates a link in the directory which links to target.
// Target must be a path relative to the directory.
//
// Note: the argument order is the inverse of os.Link to be consistent with
// the other functions in this package.
func WithHardlink(path, target string) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return errors.New("WithHardlink not implemented for manifests")
}
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
}
}
// WithTimestamps sets the access and modification times of the file system object
// at path.
func WithTimestamps(atime, mtime time.Time) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return errors.New("WithTimestamp not implemented for manifests")
}
return os.Chtimes(root.Path(), atime, mtime)
}
}