package fsutil import ( "context" "io" "io/ioutil" "os" "path" "path/filepath" "sort" "strings" "github.com/pkg/errors" "github.com/tonistiigi/fsutil/types" ) type FS interface { Walk(context.Context, filepath.WalkFunc) error Open(string) (io.ReadCloser, error) } func NewFS(root string, opt *WalkOpt) FS { return &fs{ root: root, opt: opt, } } type fs struct { root string opt *WalkOpt } func (fs *fs) Walk(ctx context.Context, fn filepath.WalkFunc) error { return Walk(ctx, fs.root, fs.opt, fn) } func (fs *fs) Open(p string) (io.ReadCloser, error) { return os.Open(filepath.Join(fs.root, p)) } type Dir struct { Stat types.Stat FS FS } func SubDirFS(dirs []Dir) (FS, error) { sort.Slice(dirs, func(i, j int) bool { return dirs[i].Stat.Path < dirs[j].Stat.Path }) m := map[string]Dir{} for _, d := range dirs { if path.Base(d.Stat.Path) != d.Stat.Path { return nil, errors.Errorf("subdir %s must be single file", d.Stat.Path) } if _, ok := m[d.Stat.Path]; ok { return nil, errors.Errorf("invalid path %s", d.Stat.Path) } m[d.Stat.Path] = d } return &subDirFS{m: m, dirs: dirs}, nil } type subDirFS struct { m map[string]Dir dirs []Dir } func (fs *subDirFS) Walk(ctx context.Context, fn filepath.WalkFunc) error { for _, d := range fs.dirs { fi := &StatInfo{Stat: &d.Stat} if !fi.IsDir() { return errors.Errorf("fs subdir %s not mode directory", d.Stat.Path) } if err := fn(d.Stat.Path, fi, nil); err != nil { return err } if err := d.FS.Walk(ctx, func(p string, fi os.FileInfo, err error) error { stat, ok := fi.Sys().(*types.Stat) if !ok { return errors.Wrapf(err, "invalid fileinfo without stat info: %s", p) } stat.Path = path.Join(d.Stat.Path, stat.Path) if stat.Linkname != "" { if fi.Mode()&os.ModeSymlink != 0 { if strings.HasPrefix(stat.Linkname, "/") { stat.Linkname = path.Join("/"+d.Stat.Path, stat.Linkname) } } else { stat.Linkname = path.Join(d.Stat.Path, stat.Linkname) } } return fn(filepath.Join(d.Stat.Path, p), &StatInfo{stat}, nil) }); err != nil { return err } } return nil } func (fs *subDirFS) Open(p string) (io.ReadCloser, error) { parts := strings.SplitN(filepath.Clean(p), string(filepath.Separator), 2) if len(parts) == 0 { return ioutil.NopCloser(&emptyReader{}), nil } d, ok := fs.m[parts[0]] if !ok { return nil, os.ErrNotExist } return d.FS.Open(parts[1]) } type emptyReader struct { } func (*emptyReader) Read([]byte) (int, error) { return 0, io.EOF }