-
Notifications
You must be signed in to change notification settings - Fork 207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement an iofs.FS over the REAPI Tree proto #2955
Changes from 4 commits
3847a73
4bcdc6e
66adc8d
b07be3d
a8d735c
270050d
14ca741
8337dc5
e9916aa
a8789a7
503560e
131d052
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Steps to build with remote execution locally: | ||
# 1) git clone https://github.com/thought-machine/please-servers | ||
# 2) cd please-servers && plz localremote | ||
# 3) you can then build and run with --profile localremote in this repo | ||
|
||
[Remote] | ||
URL = 127.0.0.1:7772 | ||
CasUrl = 127.0.0.1:7777 | ||
AssetUrl = 127.0.0.1:7776 | ||
NumExecutors = 20 | ||
# This file should be kept up to date with the file in grpcutil/token.txt from please-servers | ||
TokenFile = please-servers-token.txt | ||
Secure = false | ||
DisplayUrl = http://localhost:7779 | ||
Instance = mettle |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ovES3eR7-nBs5pgCpyrfY0kzepyrKK7w |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
go_library( | ||
name = "fs", | ||
srcs = [ | ||
"fs.go", | ||
"info.go", | ||
], | ||
deps = [ | ||
"///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/client", | ||
"///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/digest", | ||
"///third_party/go/github.com_bazelbuild_remote-apis//build/bazel/remote/execution/v2", | ||
"///third_party/go/google.golang.org_protobuf//proto", | ||
], | ||
) | ||
|
||
go_test( | ||
name = "fs_test", | ||
srcs = ["fs_test.go"], | ||
deps = [ | ||
":fs", | ||
"///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/client", | ||
"///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/digest", | ||
"///third_party/go/github.com_bazelbuild_remote-apis//build/bazel/remote/execution/v2", | ||
"///third_party/go/github.com_golang_protobuf//proto", | ||
"///third_party/go/github.com_golang_protobuf//ptypes/wrappers", | ||
"///third_party/go/github.com_stretchr_testify//assert", | ||
"///third_party/go/github.com_stretchr_testify//require", | ||
], | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
// Package fs provides an io/fs.FS implementation over the remote execution API content addressable store (CAS) | ||
package fs | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
iofs "io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/bazelbuild/remote-apis-sdks/go/pkg/client" | ||
"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" | ||
pb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" | ||
) | ||
|
||
// Client is an interface to the REAPI CAS | ||
type Client interface { | ||
ReadBlob(ctx context.Context, d digest.Digest) ([]byte, *client.MovedBytesMetadata, error) | ||
} | ||
|
||
// CASFileSystem is an fs.FS implemented on top of a Tree proto. This will download files as they are needed. | ||
type CASFileSystem struct { | ||
c Client | ||
root *pb.Directory | ||
directories map[digest.Digest]*pb.Directory | ||
workingDir string | ||
} | ||
|
||
// New creates a new filesystem on top of the given proto, using client to download files from the CAS on demand. | ||
func New(c Client, tree *pb.Tree, workingDir string) *CASFileSystem { | ||
directories := make(map[digest.Digest]*pb.Directory, len(tree.Children)) | ||
for _, child := range tree.Children { | ||
dg, err := digest.NewFromMessage(child) | ||
if err != nil { | ||
panic(fmt.Errorf("failed to create CASFileSystem: failed to calculate digest: %v", err)) | ||
} | ||
directories[dg] = child | ||
} | ||
|
||
return &CASFileSystem{ | ||
c: c, | ||
root: tree.Root, | ||
directories: directories, | ||
workingDir: workingDir, | ||
} | ||
} | ||
|
||
// Open opens the file with the given name | ||
func (fs *CASFileSystem) Open(name string) (iofs.File, error) { | ||
return fs.open(filepath.Join(fs.workingDir, name)) | ||
} | ||
|
||
// FindNode returns the node proto for the given name. Up to one of the node types may be set, where none being set | ||
// representing the path not existing. | ||
func (fs *CASFileSystem) FindNode(name string) (*pb.FileNode, *pb.DirectoryNode, *pb.SymlinkNode, error) { | ||
return fs.findNode(fs.root, filepath.Join(fs.workingDir, name)) | ||
} | ||
|
||
func (fs *CASFileSystem) open(name string) (iofs.File, error) { | ||
fileNode, dirNode, linkNode, err := fs.findNode(fs.root, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if linkNode != nil { | ||
if filepath.IsAbs(linkNode.Target) { | ||
return nil, fmt.Errorf("%v: symlink target was absolute which is invalid", name) | ||
} | ||
return fs.open(filepath.Join(filepath.Dir(name), linkNode.Target)) | ||
} | ||
|
||
if fileNode != nil { | ||
return fs.openFile(fileNode) | ||
} | ||
if dirNode != nil { | ||
return fs.openDir(dirNode) | ||
} | ||
return nil, os.ErrNotExist | ||
} | ||
|
||
// openFile downloads a file from the CAS and returns it as an iofs.File | ||
func (fs *CASFileSystem) openFile(f *pb.FileNode) (*file, error) { | ||
bs, _, err := fs.c.ReadBlob(context.Background(), digest.NewFromProtoUnvalidated(f.Digest)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
i := info{ | ||
size: int64(len(bs)), | ||
name: f.Name, | ||
} | ||
|
||
return &file{ | ||
ReadSeeker: bytes.NewReader(bs), | ||
info: i.withProperties(f.NodeProperties), | ||
}, nil | ||
} | ||
|
||
func (fs *CASFileSystem) openDir(d *pb.DirectoryNode) (iofs.File, error) { | ||
dirPb := fs.directories[digest.NewFromProtoUnvalidated(d.Digest)] | ||
i := &info{ | ||
name: d.Name, | ||
isDir: true, | ||
} | ||
return &dir{ | ||
info: i.withProperties(dirPb.NodeProperties), | ||
pb: dirPb, | ||
children: fs.directories, | ||
}, nil | ||
} | ||
|
||
func (fs *CASFileSystem) findNode(wd *pb.Directory, name string) (*pb.FileNode, *pb.DirectoryNode, *pb.SymlinkNode, error) { | ||
name, rest, hasToBeDir := strings.Cut(name, string(filepath.Separator)) | ||
// Must be a dodgy symlink that goes past our tree. | ||
if name == ".." || name == "." { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think "." should be ErrNotExist |
||
return nil, nil, nil, os.ErrNotExist | ||
} | ||
|
||
for _, d := range wd.Directories { | ||
if d.Name == name { | ||
dirPb := fs.directories[digest.NewFromProtoUnvalidated(d.Digest)] | ||
if rest == "" { | ||
return nil, d, nil, nil | ||
} | ||
return fs.findNode(dirPb, rest) | ||
} | ||
} | ||
|
||
// If the path contains a /, we only resolve against dirs. | ||
if hasToBeDir { | ||
return nil, nil, nil, os.ErrNotExist | ||
} | ||
|
||
for _, f := range wd.Files { | ||
if f.Name == name { | ||
return f, nil, nil, nil | ||
} | ||
} | ||
|
||
for _, l := range wd.Symlinks { | ||
if l.Name == name { | ||
return nil, nil, l, nil | ||
} | ||
} | ||
return nil, nil, nil, os.ErrNotExist | ||
} | ||
|
||
type file struct { | ||
io.ReadSeeker | ||
*info | ||
} | ||
|
||
func (b *file) Stat() (iofs.FileInfo, error) { | ||
return b, nil | ||
} | ||
func (b *file) Close() error { | ||
return nil | ||
} | ||
|
||
type dir struct { | ||
pb *pb.Directory | ||
children map[digest.Digest]*pb.Directory | ||
*info | ||
} | ||
|
||
// ReadDir is a slightly incorrect implementation of ReadDir. It deviates slightly as it will report all files have 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment should be updated now we have the filesize |
||
// size. This seems to work for our limited purposes though. | ||
func (p *dir) ReadDir(n int) ([]iofs.DirEntry, error) { | ||
dirSize := n | ||
if n <= 0 { | ||
dirSize = len(p.pb.Files) + len(p.pb.Symlinks) + len(p.pb.Files) | ||
} | ||
ret := make([]iofs.DirEntry, 0, dirSize) | ||
for _, dirNode := range p.pb.Directories { | ||
if n > 0 && len(ret) == n { | ||
return ret, nil | ||
} | ||
dir := p.children[digest.NewFromProtoUnvalidated(dirNode.Digest)] | ||
ret = append(ret, &info{ | ||
name: dirNode.Name, | ||
isDir: true, | ||
typeMode: os.ModeDir, | ||
mode: os.FileMode(dir.NodeProperties.UnixMode.Value), | ||
modTime: dir.NodeProperties.GetMtime().AsTime(), | ||
}) | ||
} | ||
for _, file := range p.pb.Files { | ||
if n > 0 && len(ret) == n { | ||
return ret, nil | ||
} | ||
ret = append(ret, &info{ | ||
name: file.Name, | ||
mode: os.FileMode(file.NodeProperties.UnixMode.Value), | ||
// TODO(jpoole): technically we could calculate this on demand by allowing info.Size() to download the file | ||
// from the CAS... we don't need to for now though. | ||
size: 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i thought the CAS stored the filesize without us needing to download it and calculated this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah! It's on the digest! Nice spot. |
||
}) | ||
} | ||
for _, link := range p.pb.Symlinks { | ||
if n > 0 && len(ret) == n { | ||
return ret, nil | ||
} | ||
ret = append(ret, &info{ | ||
name: link.Name, | ||
mode: os.FileMode(link.NodeProperties.UnixMode.Value), | ||
typeMode: os.ModeSymlink, | ||
}) | ||
} | ||
return ret, nil | ||
} | ||
|
||
func (p *dir) Stat() (iofs.FileInfo, error) { | ||
return p, nil | ||
} | ||
|
||
func (p *dir) Read(_ []byte) (int, error) { | ||
return 0, errors.New("attempt to read a directory") | ||
} | ||
|
||
func (p *dir) Close() error { | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it returns it as an iofs.File
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah as in *file implements the iofs.File interface