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 }