1 package github
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "net/http"
9 nurl "net/url"
10 "os"
11 "path"
12 "strings"
13
14 "github.com/golang-migrate/migrate/v4/source"
15 "github.com/google/go-github/v35/github"
16 )
17
18 func init() {
19 source.Register("github", &Github{})
20 }
21
22 var (
23 ErrNoUserInfo = fmt.Errorf("no username:token provided")
24 ErrNoAccessToken = fmt.Errorf("no access token")
25 ErrInvalidRepo = fmt.Errorf("invalid repo")
26 ErrInvalidGithubClient = fmt.Errorf("expected *github.Client")
27 ErrNoDir = fmt.Errorf("no directory")
28 )
29
30 type Github struct {
31 config *Config
32 client *github.Client
33 options *github.RepositoryContentGetOptions
34 migrations *source.Migrations
35 }
36
37 type Config struct {
38 Owner string
39 Repo string
40 Path string
41 Ref string
42 }
43
44 func (g *Github) Open(url string) (source.Driver, error) {
45 u, err := nurl.Parse(url)
46 if err != nil {
47 return nil, err
48 }
49
50
51 var client *http.Client
52 if u.User != nil {
53 password, ok := u.User.Password()
54 if !ok {
55 return nil, ErrNoUserInfo
56 }
57
58 tr := &github.BasicAuthTransport{
59 Username: u.User.Username(),
60 Password: password,
61 }
62 client = tr.Client()
63 }
64
65 gn := &Github{
66 client: github.NewClient(client),
67 migrations: source.NewMigrations(),
68 options: &github.RepositoryContentGetOptions{Ref: u.Fragment},
69 }
70
71 gn.ensureFields()
72
73
74 gn.config.Owner = u.Host
75 pe := strings.Split(strings.Trim(u.Path, "/"), "/")
76 if len(pe) < 1 {
77 return nil, ErrInvalidRepo
78 }
79 gn.config.Repo = pe[0]
80 if len(pe) > 1 {
81 gn.config.Path = strings.Join(pe[1:], "/")
82 }
83
84 if err := gn.readDirectory(); err != nil {
85 return nil, err
86 }
87
88 return gn, nil
89 }
90
91 func WithInstance(client *github.Client, config *Config) (source.Driver, error) {
92 gn := &Github{
93 client: client,
94 config: config,
95 migrations: source.NewMigrations(),
96 options: &github.RepositoryContentGetOptions{Ref: config.Ref},
97 }
98
99 if err := gn.readDirectory(); err != nil {
100 return nil, err
101 }
102
103 return gn, nil
104 }
105
106 func (g *Github) readDirectory() error {
107 g.ensureFields()
108
109 fileContent, dirContents, _, err := g.client.Repositories.GetContents(
110 context.Background(),
111 g.config.Owner,
112 g.config.Repo,
113 g.config.Path,
114 g.options,
115 )
116
117 if err != nil {
118 return err
119 }
120 if fileContent != nil {
121 return ErrNoDir
122 }
123
124 for _, fi := range dirContents {
125 m, err := source.DefaultParse(*fi.Name)
126 if err != nil {
127 continue
128 }
129 if !g.migrations.Append(m) {
130 return fmt.Errorf("unable to parse file %v", *fi.Name)
131 }
132 }
133
134 return nil
135 }
136
137 func (g *Github) ensureFields() {
138 if g.config == nil {
139 g.config = &Config{}
140 }
141 }
142
143 func (g *Github) Close() error {
144 return nil
145 }
146
147 func (g *Github) First() (version uint, err error) {
148 g.ensureFields()
149
150 if v, ok := g.migrations.First(); !ok {
151 return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
152 } else {
153 return v, nil
154 }
155 }
156
157 func (g *Github) Prev(version uint) (prevVersion uint, err error) {
158 g.ensureFields()
159
160 if v, ok := g.migrations.Prev(version); !ok {
161 return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
162 } else {
163 return v, nil
164 }
165 }
166
167 func (g *Github) Next(version uint) (nextVersion uint, err error) {
168 g.ensureFields()
169
170 if v, ok := g.migrations.Next(version); !ok {
171 return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
172 } else {
173 return v, nil
174 }
175 }
176
177 func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
178 g.ensureFields()
179
180 if m, ok := g.migrations.Up(version); ok {
181 file, _, _, err := g.client.Repositories.GetContents(
182 context.Background(),
183 g.config.Owner,
184 g.config.Repo,
185 path.Join(g.config.Path, m.Raw),
186 g.options,
187 )
188
189 if err != nil {
190 return nil, "", err
191 }
192 if file != nil {
193 r, err := file.GetContent()
194 if err != nil {
195 return nil, "", err
196 }
197 return ioutil.NopCloser(strings.NewReader(r)), m.Identifier, nil
198 }
199 }
200 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
201 }
202
203 func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
204 g.ensureFields()
205
206 if m, ok := g.migrations.Down(version); ok {
207 file, _, _, err := g.client.Repositories.GetContents(
208 context.Background(),
209 g.config.Owner,
210 g.config.Repo,
211 path.Join(g.config.Path, m.Raw),
212 g.options,
213 )
214
215 if err != nil {
216 return nil, "", err
217 }
218 if file != nil {
219 r, err := file.GetContent()
220 if err != nil {
221 return nil, "", err
222 }
223 return ioutil.NopCloser(strings.NewReader(r)), m.Identifier, nil
224 }
225 }
226 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
227 }
228
View as plain text