package ghfs import ( "context" "fmt" "io" "io/fs" "github.com/google/go-github/v47/github" ) // fetcher defines the operations we need to take against GitHub so that we can // easily provide alternative implementations type fetcher interface { getContents(name string) (*github.RepositoryContent, []*github.RepositoryContent, error) downloadContents(c *github.RepositoryContent) (io.ReadCloser, error) } // ghFetcher talks to GitHub and is used by default type ghFetcher struct { gh *github.Client ctx context.Context owner, repo, ref string } func (f *ghFetcher) getContents(name string) ( *github.RepositoryContent, []*github.RepositoryContent, error, ) { file, dir, res, err := f.gh.Repositories.GetContents( f.ctx, f.owner, f.repo, name, &github.RepositoryContentGetOptions{Ref: f.ref}, ) if err := f.responseToErr(name, res, err); err != nil { return nil, nil, err } return file, dir, nil } func (f *ghFetcher) downloadContents(file *github.RepositoryContent) (io.ReadCloser, error) { if file.GetDownloadURL() == "" { return nil, fmt.Errorf("unexpected state: file has no download URL") } dlRes, err := f.gh.Client().Get(file.GetDownloadURL()) if err := f.responseToErr(file.GetPath(), &github.Response{Response: dlRes}, err); err != nil { return nil, err } return dlRes.Body, nil } // responseToErr checks the response and error from a GitHub operation and // consolidates it into a single error to check func (f *ghFetcher) responseToErr(name string, res *github.Response, err error) error { // Failure response codes, explicitly check because the request can fail with // a nil err. switch { case res.StatusCode == 404: return fmt.Errorf("%w: %s not found in %s/%s@%s", fs.ErrNotExist, name, f.owner, f.repo, f.ref) case res.StatusCode == 403: return fmt.Errorf("%w: failed to retrieve %s from %s/%s@%s", fs.ErrPermission, name, f.owner, f.repo, f.ref) case err != nil: return fmt.Errorf("failed to retrieve %s from %s/%s@%s: %w", name, f.owner, f.repo, f.ref, err) default: return nil } }