Skip to content

Commit 50c5a26

Browse files
committed
internal/gcsfs: create
gcsfs is an io.FS implementation on top of GCS, with a bit of write support added. I anticipate this being useful for writing relui tasks. For testing purposes, also includes a copy of os.DirFS with write support added. For golang/go#51797. Change-Id: I294aac481857126dad9071158b2329f6d9a3805a Reviewed-on: https://go-review.googlesource.com/c/build/+/394360 Run-TryBot: Heschi Kreinick <[email protected]> Reviewed-by: Alex Rakoczy <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Peter Weinberger <[email protected]>
1 parent 29a15fa commit 50c5a26

File tree

6 files changed

+423
-0
lines changed

6 files changed

+423
-0
lines changed

internal/gcsfs/gcsfs.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// gcsfs implements io/fs for GCS, adding writability.
6+
package gcsfs
7+
8+
import (
9+
"context"
10+
"errors"
11+
"io"
12+
"io/fs"
13+
"path"
14+
"strings"
15+
"time"
16+
17+
"cloud.google.com/go/storage"
18+
"google.golang.org/api/iterator"
19+
)
20+
21+
// Create creates a new file on fsys, which must be a CreateFS.
22+
func Create(fsys fs.FS, name string) (WriteFile, error) {
23+
cfs, ok := fsys.(CreateFS)
24+
if !ok {
25+
return nil, &fs.PathError{Op: "create", Path: name, Err: errors.New("not implemented")}
26+
}
27+
return cfs.Create(name)
28+
}
29+
30+
// CreateFS is an fs.FS that supports creating writable files.
31+
type CreateFS interface {
32+
fs.FS
33+
Create(string) (WriteFile, error)
34+
}
35+
36+
// WriteFile is an fs.File that can be written to.
37+
// The behavior of writing and reading the same file is undefined.
38+
type WriteFile interface {
39+
fs.File
40+
io.Writer
41+
}
42+
43+
// gcsFS implements fs.FS for GCS.
44+
type gcsFS struct {
45+
ctx context.Context
46+
client *storage.Client
47+
bucket *storage.BucketHandle
48+
prefix string
49+
}
50+
51+
var _ = fs.FS((*gcsFS)(nil))
52+
var _ = CreateFS((*gcsFS)(nil))
53+
54+
// NewFS creates a new fs.FS that uses ctx for all of its operations.
55+
// Creating a new FS does not access the network, so they can be created
56+
// and destroyed per-context.
57+
//
58+
// Once the context has finished, all objects created by this FS should
59+
// be considered invalid. In particular, Writers and Readers will be canceled.
60+
func NewFS(ctx context.Context, client *storage.Client, bucket string) fs.FS {
61+
return &gcsFS{
62+
ctx: ctx,
63+
client: client,
64+
bucket: client.Bucket(bucket),
65+
}
66+
}
67+
68+
func (fsys *gcsFS) object(name string) *storage.ObjectHandle {
69+
return fsys.bucket.Object(path.Join(fsys.prefix, name))
70+
}
71+
72+
// Open opens the named file.
73+
func (fsys *gcsFS) Open(name string) (fs.File, error) {
74+
if !validPath(name) {
75+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
76+
}
77+
if name == "." {
78+
name = ""
79+
}
80+
return &GCSFile{
81+
fs: fsys,
82+
name: strings.TrimSuffix(name, "/"),
83+
}, nil
84+
}
85+
86+
// Create creates the named file.
87+
func (fsys *gcsFS) Create(name string) (WriteFile, error) {
88+
f, err := fsys.Open(name)
89+
if err != nil {
90+
return nil, err
91+
}
92+
return f.(*GCSFile), nil
93+
}
94+
95+
// fstest likes to send us backslashes. Treat them as invalid.
96+
func validPath(name string) bool {
97+
return fs.ValidPath(name) && !strings.ContainsRune(name, '\\')
98+
}
99+
100+
// GCSFile implements fs.File for GCS. It is also a WriteFile.
101+
type GCSFile struct {
102+
fs *gcsFS
103+
name string
104+
105+
reader io.ReadCloser
106+
writer io.WriteCloser
107+
iterator *storage.ObjectIterator
108+
}
109+
110+
var _ = fs.File((*GCSFile)(nil))
111+
var _ = fs.ReadDirFile((*GCSFile)(nil))
112+
var _ = io.WriteCloser((*GCSFile)(nil))
113+
114+
func (f *GCSFile) Close() error {
115+
if f.reader != nil {
116+
defer f.reader.Close()
117+
}
118+
if f.writer != nil {
119+
defer f.writer.Close()
120+
}
121+
122+
if f.reader != nil {
123+
err := f.reader.Close()
124+
if err != nil {
125+
return f.translateError("close", err)
126+
}
127+
}
128+
if f.writer != nil {
129+
err := f.writer.Close()
130+
if err != nil {
131+
return f.translateError("close", err)
132+
}
133+
}
134+
return nil
135+
}
136+
137+
func (f *GCSFile) Read(b []byte) (int, error) {
138+
if f.reader == nil {
139+
var err error
140+
f.reader, err = f.fs.object(f.name).NewReader(f.fs.ctx)
141+
if err != nil {
142+
return 0, f.translateError("read", err)
143+
}
144+
}
145+
n, err := f.reader.Read(b)
146+
return n, f.translateError("read", err)
147+
}
148+
149+
// Write writes to the GCS object associated with this File.
150+
//
151+
// A new object will be created unless an object with this name already exists.
152+
// Otherwise any previous object with the same name will be replaced.
153+
// The object will not be available (and any previous object will remain)
154+
// until Close has been called.
155+
func (f *GCSFile) Write(b []byte) (int, error) {
156+
if f.writer == nil {
157+
f.writer = f.fs.object(f.name).NewWriter(f.fs.ctx)
158+
}
159+
return f.writer.Write(b)
160+
}
161+
162+
// ReadDir implements io/fs.ReadDirFile.
163+
func (f *GCSFile) ReadDir(n int) ([]fs.DirEntry, error) {
164+
if f.iterator == nil {
165+
f.iterator = f.fs.iterator(f.name)
166+
}
167+
var result []fs.DirEntry
168+
var err error
169+
for {
170+
var info *storage.ObjectAttrs
171+
info, err = f.iterator.Next()
172+
if err != nil {
173+
break
174+
}
175+
result = append(result, &gcsFileInfo{info})
176+
if len(result) == n {
177+
break
178+
}
179+
}
180+
if err == iterator.Done {
181+
if n <= 0 {
182+
err = nil
183+
} else {
184+
err = io.EOF
185+
}
186+
}
187+
return result, f.translateError("readdir", err)
188+
}
189+
190+
// Stats the file.
191+
// The returned FileInfo exposes *storage.ObjectAttrs as its Sys() result.
192+
func (f *GCSFile) Stat() (fs.FileInfo, error) {
193+
// Check for a real file.
194+
attrs, err := f.fs.object(f.name).Attrs(f.fs.ctx)
195+
if err != nil && err != storage.ErrObjectNotExist {
196+
return nil, f.translateError("stat", err)
197+
}
198+
if err == nil {
199+
return &gcsFileInfo{attrs: attrs}, nil
200+
}
201+
// Check for a "directory".
202+
iter := f.fs.iterator(f.name)
203+
if _, err := iter.Next(); err == nil {
204+
return &gcsFileInfo{
205+
attrs: &storage.ObjectAttrs{
206+
Prefix: f.name + "/",
207+
},
208+
}, nil
209+
}
210+
return nil, f.translateError("stat", storage.ErrObjectNotExist)
211+
}
212+
213+
func (f *GCSFile) translateError(op string, err error) error {
214+
if err == nil || err == io.EOF {
215+
return err
216+
}
217+
nested := err
218+
if err == storage.ErrBucketNotExist || err == storage.ErrObjectNotExist {
219+
nested = fs.ErrNotExist
220+
} else if pe, ok := err.(*fs.PathError); ok {
221+
nested = pe.Err
222+
}
223+
return &fs.PathError{Op: op, Path: strings.TrimPrefix(f.name, f.fs.prefix), Err: nested}
224+
}
225+
226+
// gcsFileInfo implements fs.FileInfo and fs.DirEntry.
227+
type gcsFileInfo struct {
228+
attrs *storage.ObjectAttrs
229+
}
230+
231+
var _ = fs.FileInfo((*gcsFileInfo)(nil))
232+
var _ = fs.DirEntry((*gcsFileInfo)(nil))
233+
234+
func (fi *gcsFileInfo) Name() string {
235+
if fi.attrs.Prefix != "" {
236+
return path.Base(fi.attrs.Prefix)
237+
}
238+
return path.Base(fi.attrs.Name)
239+
}
240+
241+
func (fi *gcsFileInfo) Size() int64 {
242+
return fi.attrs.Size
243+
}
244+
245+
func (fi *gcsFileInfo) Mode() fs.FileMode {
246+
if fi.IsDir() {
247+
return fs.ModeDir | 0777
248+
}
249+
return 0666 // check fi.attrs.ACL?
250+
}
251+
252+
func (fi *gcsFileInfo) ModTime() time.Time {
253+
return fi.attrs.Updated
254+
}
255+
256+
func (fi *gcsFileInfo) IsDir() bool {
257+
return fi.attrs.Prefix != ""
258+
}
259+
260+
func (fi *gcsFileInfo) Sys() interface{} {
261+
return fi.attrs
262+
}
263+
264+
func (fi *gcsFileInfo) Info() (fs.FileInfo, error) {
265+
return fi, nil
266+
}
267+
268+
func (fi *gcsFileInfo) Type() fs.FileMode {
269+
return fi.Mode() & fs.ModeType
270+
}
271+
272+
func (fsys *gcsFS) iterator(name string) *storage.ObjectIterator {
273+
prefix := path.Join(fsys.prefix, name)
274+
if prefix != "" {
275+
prefix += "/"
276+
}
277+
return fsys.bucket.Objects(fsys.ctx, &storage.Query{
278+
Delimiter: "/",
279+
Prefix: prefix,
280+
})
281+
}

internal/gcsfs/gcsfs_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package gcsfs
6+
7+
import (
8+
"context"
9+
"flag"
10+
"io/fs"
11+
"io/ioutil"
12+
"path/filepath"
13+
"testing"
14+
"testing/fstest"
15+
"time"
16+
17+
"cloud.google.com/go/storage"
18+
"google.golang.org/api/option"
19+
)
20+
21+
var slowTest = flag.Bool("slow", false, "run slow tests that access GCS")
22+
23+
func TestGCSFS(t *testing.T) {
24+
if !*slowTest {
25+
t.Skip("reads a largeish GCS bucket")
26+
}
27+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
28+
defer cancel()
29+
client, err := storage.NewClient(context.Background(), option.WithScopes(storage.ScopeReadOnly))
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
fsys := NewFS(ctx, client, "vcs-test")
34+
expected := []string{
35+
"auth/or401.zip",
36+
"bzr/hello.zip",
37+
}
38+
if err := fstest.TestFS(fsys, expected...); err != nil {
39+
t.Error(err)
40+
}
41+
42+
sub, err := fs.Sub(fsys, "auth")
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if err := fstest.TestFS(sub, "or401.zip"); err != nil {
47+
t.Error(err)
48+
}
49+
}
50+
51+
func TestDirFS(t *testing.T) {
52+
if err := fstest.TestFS(DirFS("./testdata/dirfs"), "a", "b", "dir/x"); err != nil {
53+
t.Fatal(err)
54+
}
55+
}
56+
57+
func TestDirFSWrite(t *testing.T) {
58+
temp := t.TempDir()
59+
fsys := DirFS(temp)
60+
f, err := Create(fsys, "fsystest.txt")
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
if _, err := f.Write([]byte("hey\n")); err != nil {
65+
t.Fatal(err)
66+
}
67+
if err := f.Close(); err != nil {
68+
t.Fatal(err)
69+
}
70+
b, err := ioutil.ReadFile(filepath.Join(temp, "fsystest.txt"))
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
if string(b) != "hey\n" {
75+
t.Fatalf("unexpected file contents %q, want %q", string(b), "hey\n")
76+
}
77+
}

0 commit comments

Comments
 (0)