1 package gitlab
2
3 import (
4 "encoding/base64"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "net/http"
9 nurl "net/url"
10 "os"
11 "strconv"
12 "strings"
13 )
14
15 import (
16 "github.com/golang-migrate/migrate/v4/source"
17 "github.com/xanzy/go-gitlab"
18 )
19
20 func init() {
21 source.Register("gitlab", &Gitlab{})
22 }
23
24 const DefaultMaxItemsPerPage = 100
25
26 var (
27 ErrNoUserInfo = fmt.Errorf("no username:token provided")
28 ErrNoAccessToken = fmt.Errorf("no access token")
29 ErrInvalidHost = fmt.Errorf("invalid host")
30 ErrInvalidProjectID = fmt.Errorf("invalid project id")
31 ErrInvalidResponse = fmt.Errorf("invalid response")
32 )
33
34 type Gitlab struct {
35 client *gitlab.Client
36 url string
37
38 projectID string
39 path string
40 listOptions *gitlab.ListTreeOptions
41 getOptions *gitlab.GetFileOptions
42 migrations *source.Migrations
43 }
44
45 type Config struct {
46 }
47
48 func (g *Gitlab) Open(url string) (source.Driver, error) {
49 u, err := nurl.Parse(url)
50 if err != nil {
51 return nil, err
52 }
53
54 if u.User == nil {
55 return nil, ErrNoUserInfo
56 }
57
58 password, ok := u.User.Password()
59 if !ok {
60 return nil, ErrNoAccessToken
61 }
62
63 gn := &Gitlab{
64 client: gitlab.NewClient(nil, password),
65 url: url,
66 migrations: source.NewMigrations(),
67 }
68
69 if u.Host != "" {
70 uri := nurl.URL{
71 Scheme: "https",
72 Host: u.Host,
73 }
74
75 err = gn.client.SetBaseURL(uri.String())
76 if err != nil {
77 return nil, ErrInvalidHost
78 }
79 }
80
81 pe := strings.Split(strings.Trim(u.Path, "/"), "/")
82 if len(pe) < 1 {
83 return nil, ErrInvalidProjectID
84 }
85 gn.projectID = pe[0]
86 if len(pe) > 1 {
87 gn.path = strings.Join(pe[1:], "/")
88 }
89
90 gn.listOptions = &gitlab.ListTreeOptions{
91 Path: &gn.path,
92 Ref: &u.Fragment,
93 ListOptions: gitlab.ListOptions{
94 PerPage: DefaultMaxItemsPerPage,
95 },
96 }
97
98 gn.getOptions = &gitlab.GetFileOptions{
99 Ref: &u.Fragment,
100 }
101
102 if err := gn.readDirectory(); err != nil {
103 return nil, err
104 }
105
106 return gn, nil
107 }
108
109 func WithInstance(client *gitlab.Client, config *Config) (source.Driver, error) {
110 gn := &Gitlab{
111 client: client,
112 migrations: source.NewMigrations(),
113 }
114 if err := gn.readDirectory(); err != nil {
115 return nil, err
116 }
117 return gn, nil
118 }
119
120 func (g *Gitlab) readDirectory() error {
121 var nodes []*gitlab.TreeNode
122 for {
123 n, response, err := g.client.Repositories.ListTree(g.projectID, g.listOptions)
124 if err != nil {
125 return err
126 }
127
128 if response.StatusCode != http.StatusOK {
129 return ErrInvalidResponse
130 }
131
132 nodes = append(nodes, n...)
133 if response.CurrentPage >= response.TotalPages {
134 break
135 }
136 g.listOptions.ListOptions.Page = response.NextPage
137 }
138
139 for i := range nodes {
140 m, err := g.nodeToMigration(nodes[i])
141 if err != nil {
142 continue
143 }
144
145 if !g.migrations.Append(m) {
146 return fmt.Errorf("unable to parse file %v", nodes[i].Name)
147 }
148 }
149
150 return nil
151 }
152
153 func (g *Gitlab) nodeToMigration(node *gitlab.TreeNode) (*source.Migration, error) {
154 m := source.Regex.FindStringSubmatch(node.Name)
155 if len(m) == 5 {
156 versionUint64, err := strconv.ParseUint(m[1], 10, 64)
157 if err != nil {
158 return nil, err
159 }
160 return &source.Migration{
161 Version: uint(versionUint64),
162 Identifier: m[2],
163 Direction: source.Direction(m[3]),
164 Raw: g.path + "/" + node.Name,
165 }, nil
166 }
167 return nil, source.ErrParse
168 }
169
170 func (g *Gitlab) Close() error {
171 return nil
172 }
173
174 func (g *Gitlab) First() (version uint, er error) {
175 if v, ok := g.migrations.First(); !ok {
176 return 0, &os.PathError{Op: "first", Path: g.path, Err: os.ErrNotExist}
177 } else {
178 return v, nil
179 }
180 }
181
182 func (g *Gitlab) Prev(version uint) (prevVersion uint, err error) {
183 if v, ok := g.migrations.Prev(version); !ok {
184 return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.path, Err: os.ErrNotExist}
185 } else {
186 return v, nil
187 }
188 }
189
190 func (g *Gitlab) Next(version uint) (nextVersion uint, err error) {
191 if v, ok := g.migrations.Next(version); !ok {
192 return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.path, Err: os.ErrNotExist}
193 } else {
194 return v, nil
195 }
196 }
197
198 func (g *Gitlab) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
199 if m, ok := g.migrations.Up(version); ok {
200 f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
201 if err != nil {
202 return nil, "", err
203 }
204
205 if response.StatusCode != http.StatusOK {
206 return nil, "", ErrInvalidResponse
207 }
208
209 content, err := base64.StdEncoding.DecodeString(f.Content)
210 if err != nil {
211 return nil, "", err
212 }
213
214 return ioutil.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
215 }
216
217 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
218 }
219
220 func (g *Gitlab) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
221 if m, ok := g.migrations.Down(version); ok {
222 f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
223 if err != nil {
224 return nil, "", err
225 }
226
227 if response.StatusCode != http.StatusOK {
228 return nil, "", ErrInvalidResponse
229 }
230
231 content, err := base64.StdEncoding.DecodeString(f.Content)
232 if err != nil {
233 return nil, "", err
234 }
235
236 return ioutil.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
237 }
238
239 return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
240 }
241
View as plain text