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  }