frontend/pkg/bkfs/bkfs.go (156 lines of code) (raw):
package bkfs
import (
"context"
"fmt"
"io"
"io/fs"
"path"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/tonistiigi/fsutil/types"
"github.com/tonistiigi/fsutil"
"github.com/moby/buildkit/client/llb"
)
var (
_ fs.DirEntry = (*stateRefDirEntry)(nil)
_ fs.ReadDirFS = (*StateRefFS)(nil)
_ io.ReaderAt = (*stateRefFile)(nil)
_ fs.ReadDirFS = (*nullFS)(nil)
)
type StateRefFS struct {
ctx context.Context
ref gwclient.Reference
}
func FromRef(ctx context.Context, ref gwclient.Reference) *StateRefFS {
return &StateRefFS{
ctx: ctx,
ref: ref,
}
}
func FromState(ctx context.Context, state *llb.State, client gwclient.Client, opts ...llb.ConstraintsOpt) (fs.ReadDirFS, error) {
if state == nil {
return &nullFS{}, nil
}
res, err := fetchRef(client, *state, ctx, opts...)
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
return FromRef(ctx, ref), nil
}
func fetchRef(client gwclient.Client, st llb.State, ctx context.Context, opts ...llb.ConstraintsOpt) (*gwclient.Result, error) {
def, err := st.Marshal(ctx, opts...)
if err != nil {
return nil, err
}
res, err := client.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
return res, nil
}
type stateRefDirEntry struct {
stat *types.Stat
}
func (s *stateRefDirEntry) Name() string {
return path.Base(s.stat.Path)
}
func (s *stateRefDirEntry) IsDir() bool {
return s.stat.IsDir()
}
func (s *stateRefDirEntry) Type() fs.FileMode {
return fs.FileMode(s.stat.Mode)
}
func (s *stateRefDirEntry) Info() (fs.FileInfo, error) {
info := &fsutil.StatInfo{
Stat: s.stat,
}
return info, nil
}
func (st *StateRefFS) ReadDir(name string) ([]fs.DirEntry, error) {
contents, err := st.ref.ReadDir(st.ctx, gwclient.ReadDirRequest{
Path: name,
})
if err != nil {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
}
entries := []fs.DirEntry{}
for _, stat := range contents {
dirEntry := &stateRefDirEntry{stat: stat}
entries = append(entries, dirEntry)
}
return entries, nil
}
type stateRefFile struct {
eof bool // has file been read to EOF?
path string // the full path of the file from root
ref gwclient.Reference
ctx context.Context
stat *types.Stat
offset int64
}
// close is a no-op
func (s *stateRefFile) Close() error {
return nil
}
func (s *stateRefFile) ReadAt(b []byte, off int64) (int, error) {
if off < 0 {
return 0, &fs.PathError{Op: "read", Path: s.path, Err: fs.ErrInvalid}
}
if off >= s.stat.Size {
return 0, io.EOF
}
segmentContents, err := s.ref.ReadFile(s.ctx, gwclient.ReadRequest{
Filename: s.path,
Range: &gwclient.FileRange{Offset: int(off), Length: len(b)},
})
if err != nil {
return 0, err
}
n := copy(b, segmentContents)
// ReaderAt is supposed to return a descriptive error when the number of bytes read is less than
// the length of the input buffer
if n < len(b) {
err = io.EOF
}
return n, err
}
// invariant: s.offset is the offset of the next byte to be read
func (s *stateRefFile) Read(b []byte) (int, error) {
n, err := s.ReadAt(b, s.offset)
s.offset += int64(n)
return n, err
}
func (s *stateRefFile) Stat() (fs.FileInfo, error) {
info := &fsutil.StatInfo{
Stat: s.stat,
}
return info, nil
}
func (st *StateRefFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Err: fs.ErrInvalid, Path: name, Op: "open"}
}
stat, err := st.ref.StatFile(st.ctx, gwclient.StatRequest{
Path: name,
})
if err != nil {
return nil, &fs.PathError{Err: err, Op: "open", Path: name}
}
f := &stateRefFile{
path: name,
ref: st.ref,
stat: stat,
ctx: st.ctx,
eof: false,
offset: 0,
}
return f, nil
}
type nullFS struct{}
func (st *nullFS) Open(name string) (fs.File, error) {
return nil, fmt.Errorf("nullfs: %s: %w", name, fs.ErrNotExist)
}
func (st *nullFS) ReadDir(name string) ([]fs.DirEntry, error) {
return nil, fmt.Errorf("nullfs: %s: %w", name, fs.ErrNotExist)
}