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