...

Source file src/edge-infra.dev/pkg/f8n/devinfra/github/ghfs/ghfs.go

Documentation: edge-infra.dev/pkg/f8n/devinfra/github/ghfs

     1  // Package ghfs provides a fs.FS implementation backed by remote GitHub repo
     2  // contents, fetched via the GitHub API.
     3  //
     4  // Current limitations / shortcomings:
     5  //
     6  //   - All files have a modified time of 0s since Unix epoch. It may be possible to
     7  //     fetch last commit that modified the file in question and retrieve the time
     8  //     the commit was authored to provide an accurate ModTime() value.
     9  package ghfs
    10  
    11  import (
    12  	"context"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"time"
    17  
    18  	"github.com/google/go-github/v47/github"
    19  )
    20  
    21  // FS is a file system backed by remote GitHub repo contents for a specific
    22  // owner/repo at a specific reference provided during instantiation.
    23  type FS struct {
    24  	fetcher // Responsible for actually pulling GH repo contents and downloading
    25  }
    26  
    27  var (
    28  	_ fs.FS         = &FS{}
    29  	_ fs.ReadDirFS  = &FS{}
    30  	_ fs.ReadFileFS = &FS{}
    31  )
    32  
    33  type File struct {
    34  	rc io.ReadCloser
    35  	i  *info
    36  }
    37  
    38  var _ fs.File = &File{}
    39  
    40  func (f *File) Stat() (fs.FileInfo, error) { return f.i, nil }
    41  func (f *File) Read(b []byte) (int, error) { return f.rc.Read(b) }
    42  func (f *File) Close() error               { return f.rc.Close() }
    43  
    44  type Dir struct {
    45  	i      *info
    46  	files  []info
    47  	offset int
    48  }
    49  
    50  var (
    51  	_ fs.File        = &Dir{}
    52  	_ fs.ReadDirFile = &Dir{}
    53  )
    54  
    55  func (d *Dir) Stat() (fs.FileInfo, error) { return d.i, nil }
    56  func (d *Dir) Close() error               { return nil }
    57  func (d *Dir) Read(_ []byte) (int, error) {
    58  	return 0, &fs.PathError{Op: "read", Path: d.i.name, Err: fmt.Errorf("is a directory")}
    59  }
    60  
    61  func (d *Dir) ReadDir(count int) ([]fs.DirEntry, error) {
    62  	n := len(d.files) - d.offset
    63  	if n == 0 {
    64  		// Don't return EOF for bogus count
    65  		if count <= 0 {
    66  			return nil, nil
    67  		}
    68  		// No more files
    69  		return nil, io.EOF
    70  	}
    71  	// If count isn't bigger than the items we have left, use it for n
    72  	if count > 0 && n > count {
    73  		n = count
    74  	}
    75  	// Create list of DirEntry for total entries we are going to read
    76  	list := make([]fs.DirEntry, n)
    77  	for i := range list {
    78  		list[i] = &d.files[d.offset+i]
    79  	}
    80  	// Update offset based on how much we just read
    81  	d.offset += n
    82  	return list, nil
    83  }
    84  
    85  // info stores the information about the actual file descriptor in GitHub, it
    86  // implements [fs.FileInfo] and [fs.DirEntry] to make implementation of various
    87  // fs interfaces easier.
    88  type info struct {
    89  	size int64
    90  	mode fs.FileMode
    91  	name string
    92  }
    93  
    94  var _ fs.FileInfo = &info{}
    95  
    96  func (i *info) Sys() any           { return nil }
    97  func (i *info) IsDir() bool        { return i.mode == fs.ModeDir }
    98  func (i *info) Name() string       { return i.name }
    99  func (i *info) Size() int64        { return i.size }
   100  func (i *info) Mode() fs.FileMode  { return i.mode }
   101  func (i *info) ModTime() time.Time { return time.Unix(0, 0) }
   102  
   103  var _ fs.DirEntry = &info{}
   104  
   105  func (i *info) Type() fs.FileMode          { return i.Mode().Type() }
   106  func (i *info) Info() (fs.FileInfo, error) { return i, nil }
   107  
   108  type RefNotFoundError struct {
   109  	Owner string
   110  	Repo  string
   111  	Ref   string
   112  }
   113  
   114  func (rnfe RefNotFoundError) Error() string {
   115  	return fmt.Sprintf("the requested ref %s was not found at ", rnfe.Ref)
   116  }
   117  
   118  // New creates a GitHub-backed FS implementation for the specified owner and
   119  // repo.
   120  //
   121  // Refs passed to new that do not exist will result in an error as the request to
   122  // open a file on a non-existent ref will always be a 404.
   123  //
   124  // NOTE: We store the context on the FS because it cannot be passed per-request
   125  // and still implement the [fs.FS] interfaces.
   126  func New(ctx context.Context, gh *github.Client, owner, repo, ref string) (*FS, error) {
   127  	_, resp, err := gh.Repositories.GetBranch(ctx, owner, repo, ref, false)
   128  	if err != nil {
   129  		if resp.StatusCode == 404 {
   130  			return nil, RefNotFoundError{Owner: owner, Repo: repo, Ref: ref}
   131  		}
   132  		return nil, err
   133  	}
   134  
   135  	return &FS{fetcher: &ghFetcher{gh, ctx, owner, repo, ref}}, nil
   136  }
   137  
   138  // Open returns a File downloaded from a GitHub repository.
   139  //
   140  // https://docs.github.com/en/free-pro-team@latest/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
   141  func (f *FS) Open(name string) (fs.File, error) {
   142  	file, dir, err := f.getContents(name)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	switch {
   148  	case file != nil:
   149  		rc, err := f.downloadContents(file)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  		return &File{rc, infoFromRepoContent(file)}, nil
   154  	case dir != nil:
   155  		d := &Dir{files: make([]info, 0, len(dir)-1), offset: 0}
   156  		for _, c := range dir {
   157  			if c.GetPath() == name {
   158  				d.i = infoFromRepoContent(c)
   159  				continue
   160  			}
   161  			d.files = append(d.files, *infoFromRepoContent(c))
   162  		}
   163  		return d, nil
   164  	default:
   165  		return nil, fmt.Errorf("unexpected state, either file or directory " +
   166  			"contents should have been returned")
   167  	}
   168  }
   169  
   170  // ReadDir implements [fs.ReadDirFS]
   171  func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
   172  	_, dir, err := f.getContents(name)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	if dir == nil {
   177  		return nil, fmt.Errorf("%s is not a directory", name)
   178  	}
   179  
   180  	entries := make([]fs.DirEntry, 0, len(dir)-1)
   181  	for _, c := range dir {
   182  		// Don't include directory itself
   183  		if c.GetPath() == name {
   184  			continue
   185  		}
   186  		entries = append(entries, infoFromRepoContent(c))
   187  	}
   188  
   189  	return entries, nil
   190  }
   191  
   192  // ReadFile implements [fs.ReadFileFS]
   193  func (f *FS) ReadFile(name string) ([]byte, error) {
   194  	file, _, err := f.getContents(name)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	if file == nil {
   200  		return nil, fmt.Errorf("unexpected state, file should have been returned")
   201  	}
   202  
   203  	rc, err := f.downloadContents(file)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	defer rc.Close()
   208  
   209  	// From go_sdk/src/os/file.go
   210  	fSize := file.GetSize()
   211  	fSize++ // one byte for final read at EOF
   212  
   213  	if fSize < 512 {
   214  		fSize = 512
   215  	}
   216  	data := make([]byte, 0, fSize)
   217  	for {
   218  		if len(data) >= cap(data) {
   219  			d := append(data[:cap(data)], 0)
   220  			data = d[:len(data)]
   221  		}
   222  		n, err := rc.Read(data[len(data):cap(data)])
   223  		data = data[:len(data)+n]
   224  		if err != nil {
   225  			if err == io.EOF {
   226  				err = nil
   227  			}
   228  			return data, err
   229  		}
   230  	}
   231  }
   232  
   233  func infoFromRepoContent(content *github.RepositoryContent) *info {
   234  	i := &info{
   235  		size: int64(content.GetSize()),
   236  		name: content.GetName(),
   237  	}
   238  
   239  	switch {
   240  	case content.GetTarget() != "":
   241  		i.mode = fs.ModeSymlink
   242  	case content.GetType() == "dir":
   243  		i.mode = fs.ModeDir
   244  	case content.GetType() == "file" || content.GetType() == "submodule":
   245  		i.mode = fs.FileMode(0)
   246  	}
   247  
   248  	return i
   249  }
   250  

View as plain text