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 &copy, 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  }