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