package util import ( "context" "io" "os" "path/filepath" "github.com/containerd/continuity/fs" "github.com/moby/buildkit/snapshot" "github.com/pkg/errors" "github.com/tonistiigi/fsutil" fstypes "github.com/tonistiigi/fsutil/types" ) type ReadRequest struct { Filename string Range *FileRange } type FileRange struct { Offset int Length int } func withMount(mount snapshot.Mountable, cb func(string) error) error { lm := snapshot.LocalMounter(mount) root, err := lm.Mount() if err != nil { return err } defer func() { if lm != nil { lm.Unmount() } }() if err := cb(root); err != nil { return err } if err := lm.Unmount(); err != nil { return err } lm = nil return nil } func ReadFile(ctx context.Context, mount snapshot.Mountable, req ReadRequest) ([]byte, error) { var dt []byte err := withMount(mount, func(root string) error { fp, err := fs.RootPath(root, req.Filename) if err != nil { return errors.WithStack(err) } f, err := os.Open(fp) if err != nil { // The filename here is internal to the mount, so we can restore // the request base path for error reporting. // See os.DirFS.Open for details. pe := &os.PathError{} if errors.As(err, &pe) { pe.Path = req.Filename } return errors.WithStack(err) } defer f.Close() var rdr io.Reader = f if req.Range != nil { rdr = io.NewSectionReader(f, int64(req.Range.Offset), int64(req.Range.Length)) } dt, err = io.ReadAll(rdr) if err != nil { return errors.WithStack(err) } return nil }) return dt, err } type ReadDirRequest struct { Path string IncludePattern string } func ReadDir(ctx context.Context, mount snapshot.Mountable, req ReadDirRequest) ([]*fstypes.Stat, error) { var ( rd []*fstypes.Stat fo fsutil.FilterOpt ) if req.IncludePattern != "" { fo.IncludePatterns = append(fo.IncludePatterns, req.IncludePattern) } err := withMount(mount, func(root string) error { fp, err := fs.RootPath(root, req.Path) if err != nil { return errors.WithStack(err) } return fsutil.Walk(ctx, fp, &fo, func(path string, info os.FileInfo, err error) error { if err != nil { return errors.Wrapf(err, "walking %q", root) } stat, ok := info.Sys().(*fstypes.Stat) if !ok { // This "can't happen(tm)". return errors.Errorf("expected a *fsutil.Stat but got %T", info.Sys()) } rd = append(rd, stat) if info.IsDir() { return filepath.SkipDir } return nil }) }) return rd, err } func StatFile(ctx context.Context, mount snapshot.Mountable, path string) (*fstypes.Stat, error) { var st *fstypes.Stat err := withMount(mount, func(root string) error { fp, err := fs.RootPath(root, path) if err != nil { return errors.WithStack(err) } if st, err = fsutil.Stat(fp); err != nil { // The filename here is internal to the mount, so we can restore // the request base path for error reporting. // See os.DirFS.Open for details. replaceErrorPath(err, path) return errors.WithStack(err) } return nil }) return st, err } // replaceErrorPath will override the path in an os.PathError in the error chain. // This works with the fsutil library, but it isn't necessarily the correct // way to do this because the error message of wrapped errors doesn't necessarily // update or change when a wrapped error is changed. // // Still, this method of updating the path works with the way this specific // library returns errors. func replaceErrorPath(err error, path string) { var pe *os.PathError if errors.As(err, &pe) { pe.Path = path } }