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