golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/gcsfs/gcsfs.go (about) 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 "fmt" 11 "io" 12 "io/fs" 13 "net/url" 14 "path" 15 "strings" 16 "time" 17 18 "cloud.google.com/go/storage" 19 "google.golang.org/api/iterator" 20 ) 21 22 // FromURL creates a new FS from a file:// or gs:// URL. 23 // client is only used for gs:// URLs and can be nil otherwise. 24 func FromURL(ctx context.Context, client *storage.Client, base string) (fs.FS, error) { 25 u, err := url.Parse(base) 26 if err != nil { 27 return nil, err 28 } 29 switch u.Scheme { 30 case "gs": 31 if u.Host == "" { 32 return nil, fmt.Errorf("missing bucket in %q", base) 33 } 34 fsys := NewFS(ctx, client, u.Host) 35 if prefix := strings.TrimPrefix(u.Path, "/"); prefix != "" { 36 return fs.Sub(fsys, prefix) 37 } 38 return fsys, nil 39 case "file": 40 return DirFS(u.Path), nil 41 default: 42 return nil, fmt.Errorf("unsupported scheme %q", u.Scheme) 43 } 44 } 45 46 // Create creates a new file on fsys, which must be a CreateFS. 47 func Create(fsys fs.FS, name string) (WriterFile, error) { 48 cfs, ok := fsys.(CreateFS) 49 if !ok { 50 return nil, &fs.PathError{Op: "create", Path: name, Err: fmt.Errorf("not implemented on type %T", fsys)} 51 } 52 return cfs.Create(name) 53 } 54 55 // CreateFS is an fs.FS that supports creating writable files. 56 type CreateFS interface { 57 fs.FS 58 Create(string) (WriterFile, error) 59 } 60 61 // WriterFile is an fs.File that can be written to. 62 // The behavior of writing and reading the same file is undefined. 63 type WriterFile interface { 64 fs.File 65 io.Writer 66 } 67 68 // WriteFile is like os.WriteFile for CreateFSs. 69 func WriteFile(fsys fs.FS, filename string, contents []byte) error { 70 f, err := Create(fsys, filename) 71 if err != nil { 72 return err 73 } 74 defer f.Close() 75 if _, err := f.Write(contents); err != nil { 76 return err 77 } 78 return f.Close() 79 } 80 81 // gcsFS implements fs.FS for GCS. 82 type gcsFS struct { 83 ctx context.Context 84 client *storage.Client 85 bucket *storage.BucketHandle 86 prefix string 87 } 88 89 var _ = fs.FS((*gcsFS)(nil)) 90 var _ = CreateFS((*gcsFS)(nil)) 91 var _ = fs.SubFS((*gcsFS)(nil)) 92 93 // NewFS creates a new fs.FS that uses ctx for all of its operations. 94 // Creating a new FS does not access the network, so they can be created 95 // and destroyed per-context. 96 // 97 // Once the context has finished, all objects created by this FS should 98 // be considered invalid. In particular, Writers and Readers will be canceled. 99 func NewFS(ctx context.Context, client *storage.Client, bucket string) fs.FS { 100 return &gcsFS{ 101 ctx: ctx, 102 client: client, 103 bucket: client.Bucket(bucket), 104 } 105 } 106 107 func (fsys *gcsFS) object(name string) *storage.ObjectHandle { 108 return fsys.bucket.Object(path.Join(fsys.prefix, name)) 109 } 110 111 // Open opens the named file. 112 func (fsys *gcsFS) Open(name string) (fs.File, error) { 113 if !validPath(name) { 114 return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} 115 } 116 if name == "." { 117 name = "" 118 } 119 return &GCSFile{ 120 fs: fsys, 121 name: strings.TrimSuffix(name, "/"), 122 }, nil 123 } 124 125 // Create creates the named file. 126 func (fsys *gcsFS) Create(name string) (WriterFile, error) { 127 f, err := fsys.Open(name) 128 if err != nil { 129 return nil, err 130 } 131 return f.(*GCSFile), nil 132 } 133 134 func (fsys *gcsFS) Sub(dir string) (fs.FS, error) { 135 copy := *fsys 136 copy.prefix = path.Join(fsys.prefix, dir) 137 return ©, nil 138 } 139 140 // fstest likes to send us backslashes. Treat them as invalid. 141 func validPath(name string) bool { 142 return fs.ValidPath(name) && !strings.ContainsRune(name, '\\') 143 } 144 145 // GCSFile implements fs.File for GCS. It is also a WriteFile. 146 type GCSFile struct { 147 fs *gcsFS 148 name string 149 150 reader io.ReadCloser 151 writer io.WriteCloser 152 iterator *storage.ObjectIterator 153 } 154 155 var _ = fs.File((*GCSFile)(nil)) 156 var _ = fs.ReadDirFile((*GCSFile)(nil)) 157 var _ = io.WriteCloser((*GCSFile)(nil)) 158 159 func (f *GCSFile) Close() error { 160 if f.reader != nil { 161 defer f.reader.Close() 162 } 163 if f.writer != nil { 164 defer f.writer.Close() 165 } 166 167 if f.reader != nil { 168 err := f.reader.Close() 169 if err != nil { 170 return f.translateError("close", err) 171 } 172 } 173 if f.writer != nil { 174 err := f.writer.Close() 175 if err != nil { 176 return f.translateError("close", err) 177 } 178 } 179 return nil 180 } 181 182 func (f *GCSFile) Read(b []byte) (int, error) { 183 if f.reader == nil { 184 reader, err := f.fs.object(f.name).NewReader(f.fs.ctx) 185 if err != nil { 186 return 0, f.translateError("read", err) 187 } 188 f.reader = reader 189 } 190 n, err := f.reader.Read(b) 191 return n, f.translateError("read", err) 192 } 193 194 // Write writes to the GCS object associated with this File. 195 // 196 // A new object will be created unless an object with this name already exists. 197 // Otherwise any previous object with the same name will be replaced. 198 // The object will not be available (and any previous object will remain) 199 // until Close has been called. 200 func (f *GCSFile) Write(b []byte) (int, error) { 201 if f.writer == nil { 202 f.writer = f.fs.object(f.name).NewWriter(f.fs.ctx) 203 } 204 return f.writer.Write(b) 205 } 206 207 // ReadDir implements io/fs.ReadDirFile. 208 func (f *GCSFile) ReadDir(n int) ([]fs.DirEntry, error) { 209 if f.iterator == nil { 210 f.iterator = f.fs.iterator(f.name) 211 } 212 var result []fs.DirEntry 213 var err error 214 for { 215 var info *storage.ObjectAttrs 216 info, err = f.iterator.Next() 217 if err != nil { 218 break 219 } 220 result = append(result, &gcsFileInfo{info}) 221 if len(result) == n { 222 break 223 } 224 } 225 if err == iterator.Done { 226 if n <= 0 { 227 err = nil 228 } else { 229 err = io.EOF 230 } 231 } 232 return result, f.translateError("readdir", err) 233 } 234 235 // Stats the file. 236 // The returned FileInfo exposes *storage.ObjectAttrs as its Sys() result. 237 func (f *GCSFile) Stat() (fs.FileInfo, error) { 238 // Check for a real file. 239 attrs, err := f.fs.object(f.name).Attrs(f.fs.ctx) 240 if err != nil && err != storage.ErrObjectNotExist { 241 return nil, f.translateError("stat", err) 242 } 243 if err == nil { 244 return &gcsFileInfo{attrs: attrs}, nil 245 } 246 // Check for a "directory". 247 iter := f.fs.iterator(f.name) 248 if _, err := iter.Next(); err == nil { 249 return &gcsFileInfo{ 250 attrs: &storage.ObjectAttrs{ 251 Prefix: f.name + "/", 252 }, 253 }, nil 254 } 255 return nil, f.translateError("stat", storage.ErrObjectNotExist) 256 } 257 258 func (f *GCSFile) translateError(op string, err error) error { 259 if err == nil || err == io.EOF { 260 return err 261 } 262 nested := err 263 if err == storage.ErrBucketNotExist || err == storage.ErrObjectNotExist { 264 nested = fs.ErrNotExist 265 } else if pe, ok := err.(*fs.PathError); ok { 266 nested = pe.Err 267 } 268 return &fs.PathError{Op: op, Path: strings.TrimPrefix(f.name, f.fs.prefix), Err: nested} 269 } 270 271 // gcsFileInfo implements fs.FileInfo and fs.DirEntry. 272 type gcsFileInfo struct { 273 attrs *storage.ObjectAttrs 274 } 275 276 var _ = fs.FileInfo((*gcsFileInfo)(nil)) 277 var _ = fs.DirEntry((*gcsFileInfo)(nil)) 278 279 func (fi *gcsFileInfo) Name() string { 280 if fi.attrs.Prefix != "" { 281 return path.Base(fi.attrs.Prefix) 282 } 283 return path.Base(fi.attrs.Name) 284 } 285 286 func (fi *gcsFileInfo) Size() int64 { 287 return fi.attrs.Size 288 } 289 290 func (fi *gcsFileInfo) Mode() fs.FileMode { 291 if fi.IsDir() { 292 return fs.ModeDir | 0777 293 } 294 return 0666 // check fi.attrs.ACL? 295 } 296 297 func (fi *gcsFileInfo) ModTime() time.Time { 298 return fi.attrs.Updated 299 } 300 301 func (fi *gcsFileInfo) IsDir() bool { 302 return fi.attrs.Prefix != "" 303 } 304 305 func (fi *gcsFileInfo) Sys() interface{} { 306 return fi.attrs 307 } 308 309 func (fi *gcsFileInfo) Info() (fs.FileInfo, error) { 310 return fi, nil 311 } 312 313 func (fi *gcsFileInfo) Type() fs.FileMode { 314 return fi.Mode() & fs.ModeType 315 } 316 317 func (fsys *gcsFS) iterator(name string) *storage.ObjectIterator { 318 prefix := path.Join(fsys.prefix, name) 319 if prefix != "" { 320 prefix += "/" 321 } 322 return fsys.bucket.Objects(fsys.ctx, &storage.Query{ 323 Delimiter: "/", 324 Prefix: prefix, 325 }) 326 }