github.com/mef13/migrate/v4@v4.14.3/source/gitlab/gitlab.go (about) 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 }