|
| 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 | +} |
0 commit comments