github.com/mgoltzsche/ctnr@v0.7.1-alpha/pkg/fs/tree/fsbuilder.go (about)

     1  package tree
     2  
     3  import (
     4  	"net/url"
     5  	"os"
     6  	"path"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/mgoltzsche/ctnr/pkg/fs"
    11  	"github.com/mgoltzsche/ctnr/pkg/fs/source"
    12  	"github.com/mgoltzsche/ctnr/pkg/idutils"
    13  	"github.com/openSUSE/umoci/pkg/fseval"
    14  	"github.com/opencontainers/go-digest"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  type FsBuilder struct {
    19  	fs              fs.FsNode
    20  	fsEval          fseval.FsEval
    21  	sources         *source.Sources
    22  	httpHeaderCache source.HttpHeaderCache
    23  	err             error
    24  }
    25  
    26  func FromDir(rootfs string, rootless bool) (fs.FsNode, error) {
    27  	b := NewFsBuilder(NewFS(), fs.NewFSOptions(rootless))
    28  	b.CopyDir(rootfs, "/", nil)
    29  	return b.FS()
    30  }
    31  
    32  func NewFsBuilder(rootfs fs.FsNode, opts fs.FSOptions) *FsBuilder {
    33  	fsEval := fseval.DefaultFsEval
    34  	var attrMapper fs.AttrMapper
    35  	if opts.Rootless {
    36  		fsEval = fseval.RootlessFsEval
    37  		attrMapper = fs.NewRootlessAttrMapper(opts.IdMappings)
    38  	} else {
    39  		attrMapper = fs.NewAttrMapper(opts.IdMappings)
    40  	}
    41  	return &FsBuilder{
    42  		fs:              rootfs,
    43  		fsEval:          fsEval,
    44  		sources:         source.NewSources(fsEval, attrMapper),
    45  		httpHeaderCache: source.NoopHttpHeaderCache(""),
    46  	}
    47  }
    48  
    49  func (b *FsBuilder) FS() (fs.FsNode, error) {
    50  	return b.fs, errors.Wrap(b.err, "fsbuilder")
    51  }
    52  func (b *FsBuilder) HttpHeaderCache(cache source.HttpHeaderCache) {
    53  	b.httpHeaderCache = cache
    54  }
    55  func (b *FsBuilder) Hash(attrs fs.AttrSet) (d digest.Digest, err error) {
    56  	if b.err != nil {
    57  		return d, errors.Wrap(b.err, "fsbuilder")
    58  	}
    59  	return b.fs.Hash(attrs)
    60  }
    61  
    62  func (b *FsBuilder) Write(w fs.Writer) error {
    63  	if b.err != nil {
    64  		return errors.Wrap(b.err, "fsbuilder")
    65  	}
    66  	return b.fs.Write(w)
    67  }
    68  
    69  type fileSourceFactory func(file string, fi os.FileInfo, usr *idutils.UserIds) (fs.Source, error)
    70  
    71  func (b *FsBuilder) createFile(file string, fi os.FileInfo, usr *idutils.UserIds) (fs.Source, error) {
    72  	return b.sources.File(file, fi, usr)
    73  }
    74  
    75  func (b *FsBuilder) createOverlayOrFile(file string, fi os.FileInfo, usr *idutils.UserIds) (fs.Source, error) {
    76  	return b.sources.FileOverlay(file, fi, usr)
    77  }
    78  
    79  // Copies all files that match the provided glob source pattern.
    80  // Source tar archives are extracted into dest.
    81  // Source URLs are also supported.
    82  // See https://docs.docker.com/engine/reference/builder/#add
    83  func (b *FsBuilder) AddAll(srcfs string, sources []string, dest string, usr *idutils.UserIds) {
    84  	if b.err != nil {
    85  		return
    86  	}
    87  	if len(sources) == 0 {
    88  		b.err = errors.New("add: no source provided")
    89  		return
    90  	}
    91  	if len(sources) > 1 {
    92  		dest = filepath.Clean(dest) + string(filepath.Separator)
    93  	}
    94  	for _, src := range sources {
    95  		if isUrl(src) {
    96  			b.AddURL(src, dest)
    97  			if b.err != nil {
    98  				return
    99  			}
   100  		} else {
   101  			if err := b.copy(srcfs, src, dest, usr, b.createOverlayOrFile); err != nil {
   102  				b.err = errors.Wrap(err, "add "+src)
   103  				return
   104  			}
   105  		}
   106  	}
   107  }
   108  
   109  func (b *FsBuilder) AddURL(rawURL, dest string) {
   110  	url, err := url.Parse(rawURL)
   111  	if err != nil {
   112  		b.err = errors.Wrapf(err, "add URL %s", url)
   113  		return
   114  	}
   115  	// append source base name to dest if dest ends with /
   116  	if dest, err = destFilePath(path.Dir(url.Path), dest); err != nil {
   117  		b.err = errors.Wrapf(err, "add URL %s", url)
   118  		return
   119  	}
   120  	if _, err = b.fs.AddUpper(dest, source.NewSourceURL(url, b.httpHeaderCache, idutils.UserIds{})); err != nil {
   121  		b.err = errors.Wrapf(err, "add URL %s", url)
   122  		return
   123  	}
   124  }
   125  
   126  // Copies all files that match the provided glob source pattern to dest.
   127  // See https://docs.docker.com/engine/reference/builder/#copy
   128  func (b *FsBuilder) CopyAll(srcfs string, sources []string, dest string, usr *idutils.UserIds) {
   129  	if b.err != nil {
   130  		return
   131  	}
   132  	if len(sources) == 0 {
   133  		b.err = errors.New("copy: no source provided")
   134  		return
   135  	}
   136  	if len(sources) > 1 {
   137  		dest = filepath.Clean(dest) + string(filepath.Separator)
   138  	}
   139  	for _, src := range sources {
   140  		if err := b.copy(srcfs, src, dest, usr, b.createOverlayOrFile); err != nil {
   141  			b.err = errors.Wrap(err, "copy "+src)
   142  			return
   143  		}
   144  	}
   145  }
   146  
   147  func (b *FsBuilder) copy(srcfs, src, dest string, usr *idutils.UserIds, factory fileSourceFactory) (err error) {
   148  	// sources from glob pattern
   149  	src = filepath.Join(srcfs, src)
   150  	matches, err := filepath.Glob(src)
   151  	if err != nil {
   152  		return errors.Wrap(err, "source file pattern")
   153  	}
   154  	if len(matches) == 0 {
   155  		return errors.Errorf("source pattern %q does not match any files", src)
   156  	}
   157  	if len(matches) > 1 {
   158  		dest = filepath.Clean(dest) + string(filepath.Separator)
   159  	}
   160  	for _, file := range matches {
   161  		origSrcName := filepath.Base(file)
   162  		if file, err = secureSourceFile(srcfs, file); err != nil {
   163  			return
   164  		}
   165  		if err = b.addFiles(file, origSrcName, dest, usr, factory); err != nil {
   166  			return
   167  		}
   168  	}
   169  	return
   170  }
   171  
   172  func (b *FsBuilder) AddFiles(srcFile, dest string, usr *idutils.UserIds) {
   173  	if b.err != nil {
   174  		return
   175  	}
   176  	if err := b.addFiles(srcFile, filepath.Base(srcFile), dest, usr, b.createFile); err != nil {
   177  		b.err = err
   178  	}
   179  }
   180  
   181  func (b *FsBuilder) addFiles(srcFile, origSrcName, dest string, usr *idutils.UserIds, factory fileSourceFactory) (err error) {
   182  	fi, err := b.fsEval.Lstat(srcFile)
   183  	if err != nil {
   184  		return
   185  	}
   186  	if fi.IsDir() {
   187  		var parent fs.FsNode
   188  		if parent, err = b.fs.Mkdirs(dest); err != nil {
   189  			return
   190  		}
   191  		err = b.copyDirContents(srcFile, dest, parent, usr)
   192  	} else {
   193  		var src fs.Source
   194  		if src, err = factory(srcFile, fi, usr); err != nil {
   195  			return
   196  		}
   197  		t := src.Attrs().NodeType
   198  		if t != fs.TypeDir && t != fs.TypeOverlay {
   199  			// append source base name to dest if dest ends with /
   200  			if dest, err = destFilePath(origSrcName, dest); err != nil {
   201  				return
   202  			}
   203  		}
   204  		_, err = b.fs.AddUpper(dest, src)
   205  	}
   206  	return
   207  }
   208  
   209  // Copies the directory recursively including the directory itself.
   210  func (b *FsBuilder) CopyDir(srcFile, dest string, usr *idutils.UserIds) {
   211  	if b.err != nil {
   212  		return
   213  	}
   214  	fi, err := b.fsEval.Lstat(srcFile)
   215  	if err != nil {
   216  		b.err = errors.WithMessage(err, "add")
   217  		return
   218  	}
   219  	_, err = b.copyFiles(srcFile, dest, b.fs, fi, usr)
   220  	b.err = errors.WithMessage(err, "add")
   221  }
   222  
   223  // Adds file/directory recursively
   224  func (b *FsBuilder) copyFiles(file, dest string, parent fs.FsNode, fi os.FileInfo, usr *idutils.UserIds) (r fs.FsNode, err error) {
   225  	src, err := b.sources.File(file, fi, usr)
   226  	if err != nil {
   227  		return
   228  	}
   229  	if src == nil || src.Attrs().NodeType == "" {
   230  		panic("no source returned or empty node type received from source")
   231  	}
   232  	r, err = parent.AddUpper(dest, src)
   233  	if err != nil {
   234  		return
   235  	}
   236  	if src.Attrs().NodeType == fs.TypeDir {
   237  		err = b.copyDirContents(file, dest, r, usr)
   238  	}
   239  	return
   240  }
   241  
   242  // Adds directory contents recursively
   243  func (b *FsBuilder) copyDirContents(dir, dest string, parent fs.FsNode, usr *idutils.UserIds) (err error) {
   244  	files, err := b.fsEval.Readdir(dir)
   245  	if err != nil {
   246  		return errors.New(err.Error())
   247  	}
   248  	for _, f := range files {
   249  		childSrc := filepath.Join(dir, f.Name())
   250  		if _, err = b.copyFiles(childSrc, f.Name(), parent, f, usr); err != nil {
   251  			return
   252  		}
   253  	}
   254  	return
   255  }
   256  
   257  func secureSourceFile(root, file string) (f string, err error) {
   258  	// TODO: use fseval
   259  	if f, err = filepath.EvalSymlinks(file); err != nil {
   260  		return "", errors.Wrap(err, "secure source")
   261  	}
   262  	if !filepath.HasPrefix(f, root) {
   263  		err = errors.Errorf("secure source: source file %s is outside context directory", file)
   264  	}
   265  	return
   266  }
   267  
   268  func destFilePath(srcFileName string, dest string) (string, error) {
   269  	if strings.HasSuffix(dest, "/") {
   270  		if srcFileName == "" {
   271  			return "", errors.Errorf("cannot derive file name for destination %q from source. Please specify file name within destination!", dest)
   272  		}
   273  		return filepath.Join(dest, srcFileName), nil
   274  	}
   275  	return dest, nil
   276  }
   277  
   278  func isUrl(v string) bool {
   279  	v = strings.ToLower(v)
   280  	return strings.HasPrefix(v, "https://") || strings.HasPrefix(v, "http://")
   281  }