github.com/abemedia/appcast@v0.4.0/target/github/github.go (about)

     1  package github
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"io/fs"
     8  	"net/http"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/abemedia/appcast/target"
    14  	"github.com/google/go-github/github"
    15  	"golang.org/x/oauth2"
    16  )
    17  
    18  type Config struct {
    19  	Owner  string
    20  	Repo   string
    21  	Branch string
    22  	Folder string
    23  }
    24  
    25  type githubTarget struct {
    26  	client *github.RepositoriesService
    27  	owner  string
    28  	repo   string
    29  	branch string
    30  	path   string
    31  }
    32  
    33  func New(c Config) (target.Target, error) {
    34  	ctx := context.Background()
    35  	ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")})
    36  	client := github.NewClient(oauth2.NewClient(ctx, ts)).Repositories
    37  
    38  	// Ensure config is valid.
    39  	if c.Branch == "" {
    40  		repo, _, err := client.Get(ctx, c.Owner, c.Repo)
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  		c.Branch = *repo.DefaultBranch
    45  	} else {
    46  		_, _, err := client.GetBranch(ctx, c.Owner, c.Repo, c.Branch)
    47  		if err != nil {
    48  			return nil, err
    49  		}
    50  	}
    51  
    52  	t := &githubTarget{
    53  		client: client,
    54  		owner:  c.Owner,
    55  		repo:   c.Repo,
    56  		branch: c.Branch,
    57  		path:   c.Folder,
    58  	}
    59  
    60  	return t, nil
    61  }
    62  
    63  func (t *githubTarget) NewWriter(ctx context.Context, filename string) (io.WriteCloser, error) {
    64  	w := &fileWriter{
    65  		t:    t,
    66  		ctx:  ctx,
    67  		path: path.Join(t.path, filename),
    68  	}
    69  	return w, nil
    70  }
    71  
    72  func (t *githubTarget) NewReader(ctx context.Context, filename string) (io.ReadCloser, error) {
    73  	opt := &github.RepositoryContentGetOptions{Ref: t.branch}
    74  	file, _, r, err := t.client.GetContents(ctx, t.owner, t.repo, path.Join(t.path, filename), opt)
    75  	if err != nil {
    76  		if r.StatusCode == http.StatusNotFound {
    77  			return nil, &fs.PathError{Op: "read", Path: filename, Err: fs.ErrNotExist}
    78  		}
    79  		return nil, err
    80  	}
    81  
    82  	content, err := file.GetContent()
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	return io.NopCloser(strings.NewReader(content)), nil
    88  }
    89  
    90  func (t *githubTarget) Remove(ctx context.Context, filename string) error {
    91  	path := path.Join(t.path, filename)
    92  	getOpt := &github.RepositoryContentGetOptions{Ref: t.branch}
    93  	file, _, r, err := t.client.GetContents(ctx, t.owner, t.repo, path, getOpt)
    94  	if err != nil {
    95  		if r != nil && r.StatusCode == http.StatusNotFound {
    96  			return &fs.PathError{Op: "remove", Path: filename, Err: fs.ErrNotExist}
    97  		}
    98  		return err
    99  	}
   100  	_, _, err = t.client.DeleteFile(ctx, t.owner, t.repo, path, &github.RepositoryContentFileOptions{
   101  		Message: github.String("Delete " + path),
   102  		Branch:  &t.branch,
   103  		SHA:     file.SHA,
   104  	})
   105  	return err
   106  }
   107  
   108  func (t *githubTarget) Sub(dir string) target.Target {
   109  	sub := *t
   110  	sub.path = path.Join(t.path, dir)
   111  	return &sub
   112  }
   113  
   114  func (t *githubTarget) URL(_ context.Context, filename string) (string, error) {
   115  	return "https://raw.githubusercontent.com/" + path.Join(t.owner, t.repo, t.branch, t.path, filename), nil
   116  }
   117  
   118  type fileWriter struct {
   119  	bytes.Buffer
   120  
   121  	t    *githubTarget
   122  	ctx  context.Context //nolint:containedctx
   123  	path string
   124  }
   125  
   126  func (w *fileWriter) Close() error {
   127  	getOpt := &github.RepositoryContentGetOptions{Ref: w.t.branch}
   128  	file, _, res, err := w.t.client.GetContents(w.ctx, w.t.owner, w.t.repo, w.path, getOpt)
   129  	if err != nil && (res == nil || res.StatusCode != http.StatusNotFound) {
   130  		return err
   131  	}
   132  
   133  	opt := &github.RepositoryContentFileOptions{Content: w.Bytes()}
   134  	if w.t.branch != "" {
   135  		opt.Branch = &w.t.branch
   136  	}
   137  
   138  	if res.StatusCode == http.StatusNotFound {
   139  		opt.Message = github.String("Create " + w.path)
   140  		_, _, err = w.t.client.CreateFile(w.ctx, w.t.owner, w.t.repo, w.path, opt)
   141  
   142  		// Retry if writing failed due to race condition.
   143  		// This can occur when creating a file and updating it right away in which case it might still return 404.
   144  		// if e, ok := err.(*github.ErrorResponse); ok &&
   145  		// 	e.Response.StatusCode == http.StatusUnprocessableEntity &&
   146  		// 	e.Message == "Invalid request.\n\n\"sha\" wasn't supplied." {
   147  		// 	return w.Close()
   148  		// }
   149  	} else {
   150  		opt.Message = github.String("Update " + w.path)
   151  		opt.SHA = file.SHA
   152  		_, _, err = w.t.client.UpdateFile(w.ctx, w.t.owner, w.t.repo, w.path, opt)
   153  	}
   154  
   155  	return err
   156  }