mirror of https://github.com/docker/cli.git
279 lines
7.2 KiB
Go
279 lines
7.2 KiB
Go
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"gotest.tools/v3/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(io.NopCloser(strings.NewReader(content)))
|
|
return nil
|
|
}
|
|
return os.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(io.NopCloser(bytes.NewReader(raw)))
|
|
return nil
|
|
}
|
|
return os.WriteFile(path.Path(), raw, defaultFileMode)
|
|
}
|
|
}
|
|
|
|
// WithReaderContent copies the reader contents to the file at Path
|
|
func WithReaderContent(r io.Reader) PathOp {
|
|
return func(path Path) error {
|
|
if m, ok := path.(manifestFile); ok {
|
|
m.SetContent(io.NopCloser(r))
|
|
return nil
|
|
}
|
|
f, err := os.OpenFile(path.Path(), os.O_WRONLY, defaultFileMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, r)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 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 os.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 fmt.Errorf("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 := os.ReadDir(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
sourcePath := filepath.Join(source, entry.Name())
|
|
destPath := filepath.Join(dest, entry.Name())
|
|
err = copyEntry(entry, destPath, sourcePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyEntry(entry os.DirEntry, destPath string, sourcePath string) error {
|
|
if entry.IsDir() {
|
|
if err := os.Mkdir(destPath, 0755); err != nil {
|
|
return err
|
|
}
|
|
return copyDirectory(sourcePath, destPath)
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return copySymLink(sourcePath, destPath)
|
|
}
|
|
return copyFile(sourcePath, destPath)
|
|
}
|
|
|
|
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 := os.ReadFile(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.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 fmt.Errorf("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 fmt.Errorf("WithTimestamp not implemented for manifests")
|
|
}
|
|
return os.Chtimes(root.Path(), atime, mtime)
|
|
}
|
|
}
|