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 }