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