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 }