whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# aws_s3
|
||||
|
||||
`s3://<bucket>/<prefix>`
|
||||
@@ -0,0 +1,152 @@
|
||||
package awss3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3iface"
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("s3", &s3Driver{})
|
||||
}
|
||||
|
||||
type s3Driver struct {
|
||||
s3client s3iface.S3API
|
||||
config *Config
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Bucket string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (s *s3Driver) Open(folder string) (source.Driver, error) {
|
||||
config, err := parseURI(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return WithInstance(s3.New(sess), config)
|
||||
}
|
||||
|
||||
func WithInstance(s3client s3iface.S3API, config *Config) (source.Driver, error) {
|
||||
driver := &s3Driver{
|
||||
config: config,
|
||||
s3client: s3client,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
if err := driver.loadMigrations(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func parseURI(uri string) (*Config, error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := strings.Trim(u.Path, "/")
|
||||
if prefix != "" {
|
||||
prefix += "/"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Bucket: u.Host,
|
||||
Prefix: prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) loadMigrations() error {
|
||||
output, err := s.s3client.ListObjects(&s3.ListObjectsInput{
|
||||
Bucket: aws.String(s.config.Bucket),
|
||||
Prefix: aws.String(s.config.Prefix),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, object := range output.Contents {
|
||||
_, fileName := path.Split(aws.StringValue(object.Key))
|
||||
m, err := source.DefaultParse(fileName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !s.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", aws.StringValue(object.Key))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) First() (uint, error) {
|
||||
v, ok := s.migrations.First()
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) Prev(version uint) (uint, error) {
|
||||
v, ok := s.migrations.Prev(version)
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) Next(version uint) (uint, error) {
|
||||
v, ok := s.migrations.Next(version)
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *s3Driver) ReadUp(version uint) (io.ReadCloser, string, error) {
|
||||
if m, ok := s.migrations.Up(version); ok {
|
||||
return s.open(m)
|
||||
}
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func (s *s3Driver) ReadDown(version uint) (io.ReadCloser, string, error) {
|
||||
if m, ok := s.migrations.Down(version); ok {
|
||||
return s.open(m)
|
||||
}
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func (s *s3Driver) open(m *source.Migration) (io.ReadCloser, string, error) {
|
||||
key := path.Join(s.config.Prefix, m.Raw)
|
||||
object, err := s.s3client.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(s.config.Bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return object.Body, m.Identifier, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package awss3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
s3Client := fakeS3{
|
||||
bucket: "some-bucket",
|
||||
objects: map[string]string{
|
||||
"staging/migrations/1_foobar.up.sql": "1 up",
|
||||
"staging/migrations/1_foobar.down.sql": "1 down",
|
||||
"prod/migrations/1_foobar.up.sql": "1 up",
|
||||
"prod/migrations/1_foobar.down.sql": "1 down",
|
||||
"prod/migrations/3_foobar.up.sql": "3 up",
|
||||
"prod/migrations/4_foobar.up.sql": "4 up",
|
||||
"prod/migrations/4_foobar.down.sql": "4 down",
|
||||
"prod/migrations/5_foobar.down.sql": "5 down",
|
||||
"prod/migrations/7_foobar.up.sql": "7 up",
|
||||
"prod/migrations/7_foobar.down.sql": "7 down",
|
||||
"prod/migrations/not-a-migration.txt": "",
|
||||
"prod/migrations/0-random-stuff/whatever.txt": "",
|
||||
},
|
||||
}
|
||||
driver, err := WithInstance(&s3Client, &Config{
|
||||
Bucket: "some-bucket",
|
||||
Prefix: "prod/migrations/",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st.Test(t, driver)
|
||||
}
|
||||
|
||||
func TestParseURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
config *Config
|
||||
}{
|
||||
{
|
||||
"with prefix, no trailing slash",
|
||||
"s3://migration-bucket/production",
|
||||
&Config{
|
||||
Bucket: "migration-bucket",
|
||||
Prefix: "production/",
|
||||
},
|
||||
},
|
||||
{
|
||||
"without prefix, no trailing slash",
|
||||
"s3://migration-bucket",
|
||||
&Config{
|
||||
Bucket: "migration-bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
"with prefix, trailing slash",
|
||||
"s3://migration-bucket/production/",
|
||||
&Config{
|
||||
Bucket: "migration-bucket",
|
||||
Prefix: "production/",
|
||||
},
|
||||
},
|
||||
{
|
||||
"without prefix, trailing slash",
|
||||
"s3://migration-bucket/",
|
||||
&Config{
|
||||
Bucket: "migration-bucket",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual, err := parseURI(test.uri)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, test.config, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeS3 struct {
|
||||
s3.S3
|
||||
bucket string
|
||||
objects map[string]string
|
||||
}
|
||||
|
||||
func (s *fakeS3) ListObjects(input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
|
||||
bucket := aws.StringValue(input.Bucket)
|
||||
if bucket != s.bucket {
|
||||
return nil, errors.New("bucket not found")
|
||||
}
|
||||
prefix := aws.StringValue(input.Prefix)
|
||||
delimiter := aws.StringValue(input.Delimiter)
|
||||
var output s3.ListObjectsOutput
|
||||
for name := range s.objects {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
if delimiter == "" || !strings.Contains(strings.Replace(name, prefix, "", 1), delimiter) {
|
||||
output.Contents = append(output.Contents, &s3.Object{
|
||||
Key: aws.String(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &output, nil
|
||||
}
|
||||
|
||||
func (s *fakeS3) GetObject(input *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
|
||||
bucket := aws.StringValue(input.Bucket)
|
||||
if bucket != s.bucket {
|
||||
return nil, errors.New("bucket not found")
|
||||
}
|
||||
if data, ok := s.objects[aws.StringValue(input.Key)]; ok {
|
||||
body := io.NopCloser(strings.NewReader(data))
|
||||
return &s3.GetObjectOutput{Body: body}, nil
|
||||
}
|
||||
return nil, errors.New("object not found")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.bitbucket_test_secrets
|
||||
@@ -0,0 +1,14 @@
|
||||
# bitbucket
|
||||
|
||||
This driver is catered for those that want to source migrations from bitbucket cloud(https://bitbucket.com).
|
||||
|
||||
`bitbucket://user:password@owner/repo/path#ref`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| user | | The username of the user connecting |
|
||||
| password | | User's password or an app password with repo read permission |
|
||||
| owner | | the repo owner |
|
||||
| repo | | the name of the repository |
|
||||
| path | | path in repo to migrations |
|
||||
| ref | | (optional) can be a SHA, branch, or tag |
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/ktrysmt/go-bitbucket"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("bitbucket", &Bitbucket{})
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoUserInfo = fmt.Errorf("no username:password provided")
|
||||
ErrNoAccessToken = fmt.Errorf("no password/app password")
|
||||
ErrInvalidRepo = fmt.Errorf("invalid repo")
|
||||
ErrInvalidBitbucketClient = fmt.Errorf("expected *bitbucket.Client")
|
||||
ErrNoDir = fmt.Errorf("no directory")
|
||||
)
|
||||
|
||||
type Bitbucket struct {
|
||||
config *Config
|
||||
client *bitbucket.Client
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Owner string
|
||||
Repo string
|
||||
Path string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (b *Bitbucket) Open(url string) (source.Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.User == nil {
|
||||
return nil, ErrNoUserInfo
|
||||
}
|
||||
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
return nil, ErrNoAccessToken
|
||||
}
|
||||
|
||||
cl := bitbucket.NewBasicAuth(u.User.Username(), password)
|
||||
|
||||
cfg := &Config{}
|
||||
// set owner, repo and path in repo
|
||||
cfg.Owner = u.Host
|
||||
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(pe) < 1 {
|
||||
return nil, ErrInvalidRepo
|
||||
}
|
||||
cfg.Repo = pe[0]
|
||||
if len(pe) > 1 {
|
||||
cfg.Path = strings.Join(pe[1:], "/")
|
||||
}
|
||||
cfg.Ref = u.Fragment
|
||||
|
||||
bi, err := WithInstance(cl, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func WithInstance(client *bitbucket.Client, config *Config) (source.Driver, error) {
|
||||
bi := &Bitbucket{
|
||||
client: client,
|
||||
config: config,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
if err := bi.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (b *Bitbucket) readDirectory() error {
|
||||
b.ensureFields()
|
||||
|
||||
fOpt := &bitbucket.RepositoryFilesOptions{
|
||||
Owner: b.config.Owner,
|
||||
RepoSlug: b.config.Repo,
|
||||
Ref: b.config.Ref,
|
||||
Path: b.config.Path,
|
||||
}
|
||||
|
||||
dirContents, err := b.client.Repositories.Repository.ListFiles(fOpt)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, fi := range dirContents {
|
||||
|
||||
m, err := source.DefaultParse(filepath.Base(fi.Path))
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
if !b.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", fi.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bitbucket) ensureFields() {
|
||||
if b.config == nil {
|
||||
b.config = &Config{}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bitbucket) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bitbucket) First() (version uint, er error) {
|
||||
b.ensureFields()
|
||||
|
||||
if v, ok := b.migrations.First(); !ok {
|
||||
return 0, &os.PathError{Op: "first", Path: b.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bitbucket) Prev(version uint) (prevVersion uint, err error) {
|
||||
b.ensureFields()
|
||||
|
||||
if v, ok := b.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bitbucket) Next(version uint) (nextVersion uint, err error) {
|
||||
b.ensureFields()
|
||||
|
||||
if v, ok := b.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bitbucket) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
b.ensureFields()
|
||||
|
||||
if m, ok := b.migrations.Up(version); ok {
|
||||
fBlobOpt := &bitbucket.RepositoryBlobOptions{
|
||||
Owner: b.config.Owner,
|
||||
RepoSlug: b.config.Repo,
|
||||
Ref: b.config.Ref,
|
||||
Path: path.Join(b.config.Path, m.Raw),
|
||||
}
|
||||
file, err := b.client.Repositories.Repository.GetFileBlob(fBlobOpt)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if file != nil {
|
||||
r := file.Content
|
||||
return io.NopCloser(strings.NewReader(string(r))), m.Identifier, nil
|
||||
}
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (b *Bitbucket) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
b.ensureFields()
|
||||
|
||||
if m, ok := b.migrations.Down(version); ok {
|
||||
fBlobOpt := &bitbucket.RepositoryBlobOptions{
|
||||
Owner: b.config.Owner,
|
||||
RepoSlug: b.config.Repo,
|
||||
Ref: b.config.Ref,
|
||||
Path: path.Join(b.config.Path, m.Raw),
|
||||
}
|
||||
file, err := b.client.Repositories.Repository.GetFileBlob(fBlobOpt)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if file != nil {
|
||||
r := file.Content
|
||||
|
||||
return io.NopCloser(strings.NewReader(string(r))), m.Identifier, nil
|
||||
}
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
var BitbucketTestSecret = "" // username:password
|
||||
|
||||
func init() {
|
||||
secrets, err := os.ReadFile(".bitbucket_test_secrets")
|
||||
if err == nil {
|
||||
BitbucketTestSecret = string(bytes.TrimSpace(secrets)[:])
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
if len(BitbucketTestSecret) == 0 {
|
||||
t.Skip("test requires .bitbucket_test_secrets")
|
||||
}
|
||||
|
||||
b := &Bitbucket{}
|
||||
|
||||
d, err := b.Open("bitbucket://" + BitbucketTestSecret + "@abhishekbipp/test-migration/migrations/test#master")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Package source provides the Source interface.
|
||||
// All source drivers must implement this interface, register themselves,
|
||||
// optionally provide a `WithInstance` function and pass the tests
|
||||
// in package source/testing.
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var driversMu sync.RWMutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
// Driver is the interface every source driver must implement.
|
||||
//
|
||||
// How to implement a source driver?
|
||||
// 1. Implement this interface.
|
||||
// 2. Optionally, add a function named `WithInstance`.
|
||||
// This function should accept an existing source instance and a Config{} struct
|
||||
// and return a driver instance.
|
||||
// 3. Add a test that calls source/testing.go:Test()
|
||||
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
|
||||
// All other functions are tested by tests in source/testing.
|
||||
// Saves you some time and makes sure all source drivers behave the same way.
|
||||
// 5. Call Register in init().
|
||||
//
|
||||
// Guidelines:
|
||||
// - All configuration input must come from the URL string in func Open()
|
||||
// or the Config{} struct in WithInstance. Don't os.Getenv().
|
||||
// - Drivers are supposed to be read only.
|
||||
// - Ideally don't load any contents (into memory) in Open or WithInstance.
|
||||
type Driver interface {
|
||||
// Open returns a new driver instance configured with parameters
|
||||
// coming from the URL string. Migrate will call this function
|
||||
// only once per instance.
|
||||
Open(url string) (Driver, error)
|
||||
|
||||
// Close closes the underlying source instance managed by the driver.
|
||||
// Migrate will call this function only once per instance.
|
||||
Close() error
|
||||
|
||||
// First returns the very first migration version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no version available, it must return os.ErrNotExist.
|
||||
First() (version uint, err error)
|
||||
|
||||
// Prev returns the previous version for a given version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no previous version available, it must return os.ErrNotExist.
|
||||
Prev(version uint) (prevVersion uint, err error)
|
||||
|
||||
// Next returns the next version for a given version available to the driver.
|
||||
// Migrate will call this function multiple times.
|
||||
// If there is no next version available, it must return os.ErrNotExist.
|
||||
Next(version uint) (nextVersion uint, err error)
|
||||
|
||||
// ReadUp returns the UP migration body and an identifier that helps
|
||||
// finding this migration in the source for a given version.
|
||||
// If there is no up migration available for this version,
|
||||
// it must return os.ErrNotExist.
|
||||
// Do not start reading, just return the ReadCloser!
|
||||
ReadUp(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
|
||||
// ReadDown returns the DOWN migration body and an identifier that helps
|
||||
// finding this migration in the source for a given version.
|
||||
// If there is no down migration available for this version,
|
||||
// it must return os.ErrNotExist.
|
||||
// Do not start reading, just return the ReadCloser!
|
||||
ReadDown(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
}
|
||||
|
||||
// Open returns a new driver instance.
|
||||
func Open(url string) (Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil, fmt.Errorf("source driver: invalid URL scheme")
|
||||
}
|
||||
|
||||
driversMu.RLock()
|
||||
d, ok := drivers[u.Scheme]
|
||||
driversMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("source driver: unknown driver '%s' (forgotten import?)", u.Scheme)
|
||||
}
|
||||
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
// Register globally registers a driver.
|
||||
func Register(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// List lists the registered drivers
|
||||
func List() []string {
|
||||
driversMu.RLock()
|
||||
defer driversMu.RUnlock()
|
||||
names := make([]string, 0, len(drivers))
|
||||
for n := range drivers {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package source
|
||||
|
||||
func ExampleDriver() {
|
||||
// see source/stub for an example
|
||||
|
||||
// source/stub/stub.go has the driver implementation
|
||||
// source/stub/stub_test.go runs source/testing/test.go:Test
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package source
|
||||
|
||||
import "os"
|
||||
|
||||
// ErrDuplicateMigration is an error type for reporting duplicate migration
|
||||
// files.
|
||||
type ErrDuplicateMigration struct {
|
||||
Migration
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e ErrDuplicateMigration) Error() string {
|
||||
return "duplicate migration file: " + e.Name()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# file
|
||||
|
||||
`file:///absolute/path`
|
||||
`file://relative/path`
|
||||
@@ -0,0 +1,66 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("file", &File{})
|
||||
}
|
||||
|
||||
type File struct {
|
||||
iofs.PartialDriver
|
||||
url string
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *File) Open(url string) (source.Driver, error) {
|
||||
p, err := parseURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nf := &File{
|
||||
url: url,
|
||||
path: p,
|
||||
}
|
||||
if err := nf.Init(os.DirFS(p), "."); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nf, nil
|
||||
}
|
||||
|
||||
func parseURL(url string) (string, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// concat host and path to restore full path
|
||||
// host might be `.`
|
||||
p := u.Opaque
|
||||
if len(p) == 0 {
|
||||
p = u.Host + u.Path
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
// default to current directory if no path
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p = wd
|
||||
|
||||
} else if p[0:1] == "." || p[0:1] != "/" {
|
||||
// make path absolute if relative
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p = abs
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
const scheme = "file://"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// write files that meet driver test requirements
|
||||
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "1 up")
|
||||
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "1 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "3_foobar.up.sql", "3 up")
|
||||
|
||||
mustWriteFile(t, tmpDir, "4_foobar.up.sql", "4 up")
|
||||
mustWriteFile(t, tmpDir, "4_foobar.down.sql", "4 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "5_foobar.down.sql", "5 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "7_foobar.up.sql", "7 up")
|
||||
mustWriteFile(t, tmpDir, "7_foobar.down.sql", "7 down")
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open(scheme + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "")
|
||||
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "")
|
||||
|
||||
if !filepath.IsAbs(tmpDir) {
|
||||
t.Fatal("expected tmpDir to be absolute path")
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
_, err := f.Open(scheme + tmpDir) // absolute path
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenWithRelativePath(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
// rescue working dir after we are done
|
||||
if err := os.Chdir(wd); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(filepath.Join(tmpDir, "foo"), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mustWriteFile(t, filepath.Join(tmpDir, "foo"), "1_foobar.up.sql", "")
|
||||
|
||||
f := &File{}
|
||||
|
||||
// dir: foo
|
||||
d, err := f.Open("file://foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = d.First()
|
||||
if err != nil {
|
||||
t.Fatalf("expected first file in working dir %v for foo", tmpDir)
|
||||
}
|
||||
|
||||
// dir: ./foo
|
||||
d, err = f.Open("file://./foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = d.First()
|
||||
if err != nil {
|
||||
t.Fatalf("expected first file in working dir %v for ./foo", tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDefaultsToCurrentDirectory(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if d.(*File).path != wd {
|
||||
t.Fatal("expected driver to default to current directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenWithDuplicateVersion(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
mustWriteFile(t, tmpDir, "1_foo.up.sql", "") // 1 up
|
||||
mustWriteFile(t, tmpDir, "1_bar.up.sql", "") // 1 up
|
||||
|
||||
f := &File{}
|
||||
_, err := f.Open(scheme + tmpDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open(scheme + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if d.Close() != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFile(t testing.TB, dir, file string, body string) {
|
||||
if err := os.WriteFile(path.Join(dir, file), []byte(body), 06444); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustCreateBenchmarkDir(t *testing.B) (dir string) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.up.sql", i), "")
|
||||
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.down.sql", i), "")
|
||||
}
|
||||
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
func BenchmarkOpen(b *testing.B) {
|
||||
dir := mustCreateBenchmarkDir(b)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
f := &File{}
|
||||
_, err := f.Open(scheme + dir)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
func BenchmarkNext(b *testing.B) {
|
||||
dir := mustCreateBenchmarkDir(b)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}()
|
||||
f := &File{}
|
||||
d, _ := f.Open(scheme + dir)
|
||||
b.ResetTimer()
|
||||
v, err := d.First()
|
||||
for n := 0; n < b.N; n++ {
|
||||
for !errors.Is(err, os.ErrNotExist) {
|
||||
v, err = d.Next(v)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.github_test_secrets
|
||||
@@ -0,0 +1,16 @@
|
||||
# github
|
||||
|
||||
This driver is catered for those that want to source migrations from [github.com](https://github.com). The URL scheme doesn't require a hostname, as it just simply defaults to `github.com`.
|
||||
|
||||
Authenticated client: `github://user:personal-access-token@owner/repo/path#ref`
|
||||
|
||||
Unauthenticated client: `github://owner/repo/path#ref`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| user | | (optional) The username of the user connecting |
|
||||
| personal-access-token | | (optional) An access token from GitHub (https://github.com/settings/tokens) |
|
||||
| owner | | the repo owner |
|
||||
| repo | | the name of the repository |
|
||||
| path | | path in repo to migrations |
|
||||
| ref | | (optional) can be a SHA, branch, or tag |
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
|
||||
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1,215 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/google/go-github/v39/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("github", &Github{})
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoUserInfo = fmt.Errorf("no username:token provided")
|
||||
ErrNoAccessToken = fmt.Errorf("no access token")
|
||||
ErrInvalidRepo = fmt.Errorf("invalid repo")
|
||||
ErrInvalidGithubClient = fmt.Errorf("expected *github.Client")
|
||||
ErrNoDir = fmt.Errorf("no directory")
|
||||
)
|
||||
|
||||
type Github struct {
|
||||
config *Config
|
||||
client *github.Client
|
||||
options *github.RepositoryContentGetOptions
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Owner string
|
||||
Repo string
|
||||
Path string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (g *Github) Open(url string) (source.Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// client defaults to http.DefaultClient
|
||||
var client *http.Client
|
||||
if u.User != nil {
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
return nil, ErrNoUserInfo
|
||||
}
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: password},
|
||||
)
|
||||
client = oauth2.NewClient(context.Background(), ts)
|
||||
|
||||
}
|
||||
|
||||
gn := &Github{
|
||||
client: github.NewClient(client),
|
||||
migrations: source.NewMigrations(),
|
||||
options: &github.RepositoryContentGetOptions{Ref: u.Fragment},
|
||||
}
|
||||
|
||||
gn.ensureFields()
|
||||
|
||||
// set owner, repo and path in repo
|
||||
gn.config.Owner = u.Host
|
||||
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(pe) < 1 {
|
||||
return nil, ErrInvalidRepo
|
||||
}
|
||||
gn.config.Repo = pe[0]
|
||||
if len(pe) > 1 {
|
||||
gn.config.Path = strings.Join(pe[1:], "/")
|
||||
}
|
||||
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func WithInstance(client *github.Client, config *Config) (source.Driver, error) {
|
||||
gn := &Github{
|
||||
client: client,
|
||||
config: config,
|
||||
migrations: source.NewMigrations(),
|
||||
options: &github.RepositoryContentGetOptions{Ref: config.Ref},
|
||||
}
|
||||
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func (g *Github) readDirectory() error {
|
||||
g.ensureFields()
|
||||
|
||||
fileContent, dirContents, _, err := g.client.Repositories.GetContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
g.config.Path,
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileContent != nil {
|
||||
return ErrNoDir
|
||||
}
|
||||
|
||||
for _, fi := range dirContents {
|
||||
m, err := source.DefaultParse(*fi.Name)
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
if !g.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", *fi.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Github) ensureFields() {
|
||||
if g.config == nil {
|
||||
g.config = &Config{}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Github) First() (version uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.First(); !ok {
|
||||
return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Prev(version uint) (prevVersion uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Next(version uint) (nextVersion uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Up(version); ok {
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
path.Join(g.config.Path, m.Raw),
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Down(version); ok {
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
path.Join(g.config.Path, m.Raw),
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var GithubTestSecret = "" // username:token
|
||||
|
||||
func init() {
|
||||
secrets, err := os.ReadFile(".github_test_secrets")
|
||||
if err == nil {
|
||||
GithubTestSecret = string(bytes.TrimSpace(secrets)[:])
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
if len(GithubTestSecret) == 0 {
|
||||
t.Skip("test requires .github_test_secrets")
|
||||
}
|
||||
|
||||
g := &Github{}
|
||||
d, err := g.Open("github://" + GithubTestSecret + "@mattes/migrate_test_tmp/test#452b8003e7")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestDefaultClient(t *testing.T) {
|
||||
g := &Github{}
|
||||
owner := "golang-migrate"
|
||||
repo := "migrate"
|
||||
path := "source/github/examples/migrations"
|
||||
|
||||
url := fmt.Sprintf("github://%s/%s/%s", owner, repo, path)
|
||||
d, err := g.Open(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ver, err := d.First()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, uint(1085649617), ver)
|
||||
|
||||
ver, err = d.Next(ver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, uint(1185749658), ver)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.github_test_secrets
|
||||
@@ -0,0 +1,21 @@
|
||||
# github ee
|
||||
|
||||
## GitHub Enterprise Edition
|
||||
|
||||
This driver is catered for those who run GitHub Enterprise under private infrastructure.
|
||||
|
||||
The below URL scheme illustrates how to source migration files from GitHub Enterprise.
|
||||
|
||||
GitHub client for Go requires API and Uploads endpoint hosts in order to create an instance of GitHub Enterprise Client. We're making an assumption that the API and Uploads are available under `https://api.*` and `https://uploads.*` respectively. [GitHub Enterprise Installation Guide](https://help.github.com/en/enterprise/2.15/admin/installation/enabling-subdomain-isolation) recommends that you enable Subdomain isolation feature.
|
||||
|
||||
`github-ee://user:personal-access-token@host/owner/repo/path?verify-tls=true#ref`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| user | | The username of the user connecting |
|
||||
| personal-access-token | | Personal access token from your GitHub Enterprise instance |
|
||||
| owner | | the repo owner |
|
||||
| repo | | the name of the repository |
|
||||
| path | | path in repo to migrations |
|
||||
| ref | | (optional) can be a SHA, branch, or tag |
|
||||
| verify-tls | | (optional) defaults to `true`. This option sets `tls.Config.InsecureSkipVerify` accordingly |
|
||||
@@ -0,0 +1,97 @@
|
||||
package github_ee
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
gh "github.com/golang-migrate/migrate/v4/source/github"
|
||||
|
||||
"github.com/google/go-github/v39/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("github-ee", &GithubEE{})
|
||||
}
|
||||
|
||||
type GithubEE struct {
|
||||
source.Driver
|
||||
}
|
||||
|
||||
func (g *GithubEE) Open(url string) (source.Driver, error) {
|
||||
verifyTLS := true
|
||||
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if o := u.Query().Get("verify-tls"); o != "" {
|
||||
verifyTLS = parseBool(o, verifyTLS)
|
||||
}
|
||||
|
||||
if u.User == nil {
|
||||
return nil, gh.ErrNoUserInfo
|
||||
}
|
||||
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
return nil, gh.ErrNoUserInfo
|
||||
}
|
||||
|
||||
ghc, err := g.createGithubClient(u.Host, u.User.Username(), password, verifyTLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
|
||||
if len(pe) < 1 {
|
||||
return nil, gh.ErrInvalidRepo
|
||||
}
|
||||
|
||||
cfg := &gh.Config{
|
||||
Owner: pe[0],
|
||||
Repo: pe[1],
|
||||
Ref: u.Fragment,
|
||||
}
|
||||
|
||||
if len(pe) > 2 {
|
||||
cfg.Path = strings.Join(pe[2:], "/")
|
||||
}
|
||||
|
||||
i, err := gh.WithInstance(ghc, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GithubEE{Driver: i}, nil
|
||||
}
|
||||
|
||||
func (g *GithubEE) createGithubClient(host, username, password string, verifyTLS bool) (*github.Client, error) {
|
||||
tr := &github.BasicAuthTransport{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifyTLS},
|
||||
},
|
||||
}
|
||||
|
||||
apiHost := fmt.Sprintf("https://%s/api/v3", host)
|
||||
uploadHost := fmt.Sprintf("https://uploads.%s", host)
|
||||
|
||||
return github.NewEnterpriseClient(apiHost, uploadHost, tr.Client())
|
||||
}
|
||||
|
||||
func parseBool(val string, fallback bool) bool {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package github_ee
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
nurl "net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v3/repos/mattes/migrate_test_tmp/contents/test" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if ref := r.URL.Query().Get("ref"); ref != "452b8003e7" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
_, err := w.Write([]byte("[]"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
u, err := nurl.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
g := &GithubEE{}
|
||||
_, err = g.Open("github-ee://foo:bar@" + u.Host + "/mattes/migrate_test_tmp/test?verify-tls=false#452b8003e7")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.gitlab_test_secrets
|
||||
@@ -0,0 +1,12 @@
|
||||
# gitlab
|
||||
|
||||
`gitlab://user:personal-access-token@gitlab_url/project_id/path#ref`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| user | | The username of the user connecting |
|
||||
| personal-access-token | | An access token from Gitlab (https://<gitlab_url>/profile/personal_access_tokens) |
|
||||
| gitlab_url | | url of the gitlab server |
|
||||
| project_id | | id of the repository |
|
||||
| path | | path in repo to migrations |
|
||||
| ref | | (optional) can be a SHA, branch, or tag |
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
|
||||
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1,237 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("gitlab", &Gitlab{})
|
||||
}
|
||||
|
||||
const DefaultMaxItemsPerPage = 100
|
||||
|
||||
var (
|
||||
ErrNoUserInfo = fmt.Errorf("no username:token provided")
|
||||
ErrNoAccessToken = fmt.Errorf("no access token")
|
||||
ErrInvalidHost = fmt.Errorf("invalid host")
|
||||
ErrInvalidProjectID = fmt.Errorf("invalid project id")
|
||||
ErrInvalidResponse = fmt.Errorf("invalid response")
|
||||
)
|
||||
|
||||
type Gitlab struct {
|
||||
client *gitlab.Client
|
||||
url string
|
||||
|
||||
projectID string
|
||||
path string
|
||||
listOptions *gitlab.ListTreeOptions
|
||||
getOptions *gitlab.GetFileOptions
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
||||
func (g *Gitlab) Open(url string) (source.Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.User == nil {
|
||||
return nil, ErrNoUserInfo
|
||||
}
|
||||
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
return nil, ErrNoAccessToken
|
||||
}
|
||||
|
||||
gn := &Gitlab{
|
||||
client: gitlab.NewClient(nil, password),
|
||||
url: url,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
if u.Host != "" {
|
||||
uri := nurl.URL{
|
||||
Scheme: "https",
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
err = gn.client.SetBaseURL(uri.String())
|
||||
if err != nil {
|
||||
return nil, ErrInvalidHost
|
||||
}
|
||||
}
|
||||
|
||||
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(pe) < 1 {
|
||||
return nil, ErrInvalidProjectID
|
||||
}
|
||||
gn.projectID = pe[0]
|
||||
if len(pe) > 1 {
|
||||
gn.path = strings.Join(pe[1:], "/")
|
||||
}
|
||||
|
||||
gn.listOptions = &gitlab.ListTreeOptions{
|
||||
Path: &gn.path,
|
||||
Ref: &u.Fragment,
|
||||
ListOptions: gitlab.ListOptions{
|
||||
PerPage: DefaultMaxItemsPerPage,
|
||||
},
|
||||
}
|
||||
|
||||
gn.getOptions = &gitlab.GetFileOptions{
|
||||
Ref: &u.Fragment,
|
||||
}
|
||||
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func WithInstance(client *gitlab.Client, config *Config) (source.Driver, error) {
|
||||
gn := &Gitlab{
|
||||
client: client,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) readDirectory() error {
|
||||
var nodes []*gitlab.TreeNode
|
||||
for {
|
||||
n, response, err := g.client.Repositories.ListTree(g.projectID, g.listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return ErrInvalidResponse
|
||||
}
|
||||
|
||||
nodes = append(nodes, n...)
|
||||
if response.CurrentPage >= response.TotalPages {
|
||||
break
|
||||
}
|
||||
g.listOptions.ListOptions.Page = response.NextPage
|
||||
}
|
||||
|
||||
for i := range nodes {
|
||||
m, err := g.nodeToMigration(nodes[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !g.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", nodes[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) nodeToMigration(node *gitlab.TreeNode) (*source.Migration, error) {
|
||||
m := source.Regex.FindStringSubmatch(node.Name)
|
||||
if len(m) == 5 {
|
||||
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &source.Migration{
|
||||
Version: uint(versionUint64),
|
||||
Identifier: m[2],
|
||||
Direction: source.Direction(m[3]),
|
||||
Raw: g.path + "/" + node.Name,
|
||||
}, nil
|
||||
}
|
||||
return nil, source.ErrParse
|
||||
}
|
||||
|
||||
func (g *Gitlab) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) First() (version uint, er error) {
|
||||
if v, ok := g.migrations.First(); !ok {
|
||||
return 0, &os.PathError{Op: "first", Path: g.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gitlab) Prev(version uint) (prevVersion uint, err error) {
|
||||
if v, ok := g.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gitlab) Next(version uint) (nextVersion uint, err error) {
|
||||
if v, ok := g.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gitlab) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := g.migrations.Up(version); ok {
|
||||
f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, "", ErrInvalidResponse
|
||||
}
|
||||
|
||||
content, err := base64.StdEncoding.DecodeString(f.Content)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
|
||||
}
|
||||
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (g *Gitlab) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := g.migrations.Down(version); ok {
|
||||
f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, "", ErrInvalidResponse
|
||||
}
|
||||
|
||||
content, err := base64.StdEncoding.DecodeString(f.Content)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
|
||||
}
|
||||
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
var GitlabTestSecret = "" // username:token
|
||||
|
||||
func init() {
|
||||
secrets, err := os.ReadFile(".gitlab_test_secrets")
|
||||
if err == nil {
|
||||
GitlabTestSecret = string(bytes.TrimSpace(secrets)[:])
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
if len(GitlabTestSecret) == 0 {
|
||||
t.Skip("test requires .gitlab_test_secrets")
|
||||
}
|
||||
|
||||
g := &Gitlab{}
|
||||
d, err := g.Open("gitlab://" + GitlabTestSecret + "@gitlab.com/11197284/migrations")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# go_bindata
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
|
||||
### Read bindata with NewWithSourceInstance
|
||||
|
||||
```shell
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
cd examples/migrations && go-bindata -pkg migrations .
|
||||
```
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/source/go_bindata"
|
||||
"github.com/golang-migrate/migrate/v4/source/go_bindata/examples/migrations"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// wrap assets into Resource
|
||||
s := bindata.Resource(migrations.AssetNames(),
|
||||
func(name string) ([]byte, error) {
|
||||
return migrations.Asset(name)
|
||||
})
|
||||
|
||||
d, err := bindata.WithInstance(s)
|
||||
m, err := migrate.NewWithSourceInstance("go-bindata", d, "database://foobar")
|
||||
m.Up() // run your migrations and handle the errors above of course
|
||||
}
|
||||
```
|
||||
|
||||
### Read bindata with URL (todo)
|
||||
|
||||
This will restore the assets in a tmp directory and then
|
||||
proxy to source/file. go-bindata must be in your `$PATH`.
|
||||
|
||||
```
|
||||
migrate -source go-bindata://examples/migrations/bindata.go
|
||||
```
|
||||
|
||||
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
// Code generated by go-bindata.
|
||||
// sources:
|
||||
// 1085649617_create_users_table.down.sql
|
||||
// 1085649617_create_users_table.up.sql
|
||||
// 1185749658_add_city_to_users.down.sql
|
||||
// 1185749658_add_city_to_users.up.sql
|
||||
// DO NOT EDIT!
|
||||
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gz)
|
||||
clErr := gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
if clErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
type bindataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (fi bindataFileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
func (fi bindataFileInfo) Size() int64 {
|
||||
return fi.size
|
||||
}
|
||||
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
func (fi bindataFileInfo) ModTime() time.Time {
|
||||
return fi.modTime
|
||||
}
|
||||
func (fi bindataFileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
func (fi bindataFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var __1085649617_create_users_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xf0\x74\x53\x70\x8d\xf0\x0c\x0e\x09\x56\x28\x2d\x4e\x2d\x2a\xb6\xe6\x02\x04\x00\x00\xff\xff\x2c\x02\x3d\xa7\x1c\x00\x00\x00")
|
||||
|
||||
func _1085649617_create_users_tableDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1085649617_create_users_tableDownSql,
|
||||
"1085649617_create_users_table.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1085649617_create_users_tableDownSql() (*asset, error) {
|
||||
bytes, err := _1085649617_create_users_tableDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1085649617_create_users_table.down.sql", size: 28, mode: os.FileMode(420), modTime: time.Unix(1485750305, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1085649617_create_users_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\x0e\x72\x75\x0c\x71\x55\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\xd0\xe0\x52\x00\xb3\xe2\x33\x53\x14\x32\xf3\x4a\x52\xd3\x53\x8b\x14\x4a\xf3\x32\x0b\x4b\x53\x75\xb8\x14\x14\xf2\x12\x73\x53\x15\x14\x14\x14\xca\x12\x8b\x92\x33\x12\x8b\x34\x4c\x0c\x34\x41\xc2\xa9\xb9\x89\x99\x39\xa8\xc2\x5c\x9a\xd6\x5c\x80\x00\x00\x00\xff\xff\xa3\x57\xbc\x0b\x5f\x00\x00\x00")
|
||||
|
||||
func _1085649617_create_users_tableUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1085649617_create_users_tableUpSql,
|
||||
"1085649617_create_users_table.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1085649617_create_users_tableUpSql() (*asset, error) {
|
||||
bytes, err := _1085649617_create_users_tableUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1085649617_create_users_table.up.sql", size: 95, mode: os.FileMode(420), modTime: time.Unix(1485803085, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1185749658_add_city_to_usersDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\x70\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\xf0\x74\x53\x70\x8d\xf0\x0c\x0e\x09\x56\x48\xce\x2c\xa9\xb4\xe6\x02\x04\x00\x00\xff\xff\xb7\x52\x88\xd7\x2e\x00\x00\x00")
|
||||
|
||||
func _1185749658_add_city_to_usersDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1185749658_add_city_to_usersDownSql,
|
||||
"1185749658_add_city_to_users.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1185749658_add_city_to_usersDownSql() (*asset, error) {
|
||||
bytes, err := _1185749658_add_city_to_usersDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1185749658_add_city_to_users.down.sql", size: 46, mode: os.FileMode(420), modTime: time.Unix(1485750443, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1185749658_add_city_to_usersUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\x70\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x48\xce\x2c\xa9\x54\x28\x4b\x2c\x4a\xce\x48\x2c\xd2\x30\x34\x30\xd0\xb4\xe6\xe2\xe2\x02\x04\x00\x00\xff\xff\xa8\x0f\x49\xc6\x32\x00\x00\x00")
|
||||
|
||||
func _1185749658_add_city_to_usersUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1185749658_add_city_to_usersUpSql,
|
||||
"1185749658_add_city_to_users.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1185749658_add_city_to_usersUpSql() (*asset, error) {
|
||||
bytes, err := _1185749658_add_city_to_usersUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1185749658_add_city_to_users.up.sql", size: 50, mode: os.FileMode(420), modTime: time.Unix(1485843733, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if err != nil {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"1085649617_create_users_table.down.sql": _1085649617_create_users_tableDownSql,
|
||||
"1085649617_create_users_table.up.sql": _1085649617_create_users_tableUpSql,
|
||||
"1185749658_add_city_to_users.down.sql": _1185749658_add_city_to_usersDownSql,
|
||||
"1185749658_add_city_to_users.up.sql": _1185749658_add_city_to_usersUpSql,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
//
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
//
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for childName := range node.Children {
|
||||
rv = append(rv, childName)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type bintree struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*bintree
|
||||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"1085649617_create_users_table.down.sql": &bintree{_1085649617_create_users_tableDownSql, map[string]*bintree{}},
|
||||
"1085649617_create_users_table.up.sql": &bintree{_1085649617_create_users_tableUpSql, map[string]*bintree{}},
|
||||
"1185749658_add_city_to_users.down.sql": &bintree{_1185749658_add_city_to_usersDownSql, map[string]*bintree{}},
|
||||
"1185749658_add_city_to_users.up.sql": &bintree{_1185749658_add_city_to_usersUpSql, map[string]*bintree{}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreAssets restores an asset under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
// File
|
||||
if err != nil {
|
||||
return RestoreAsset(dir, name)
|
||||
}
|
||||
// Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package bindata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
type AssetFunc func(name string) ([]byte, error)
|
||||
|
||||
func Resource(names []string, afn AssetFunc) *AssetSource {
|
||||
return &AssetSource{
|
||||
Names: names,
|
||||
AssetFunc: afn,
|
||||
}
|
||||
}
|
||||
|
||||
type AssetSource struct {
|
||||
Names []string
|
||||
AssetFunc AssetFunc
|
||||
}
|
||||
|
||||
func init() {
|
||||
source.Register("go-bindata", &Bindata{})
|
||||
}
|
||||
|
||||
type Bindata struct {
|
||||
path string
|
||||
assetSource *AssetSource
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
func (b *Bindata) Open(url string) (source.Driver, error) {
|
||||
return nil, fmt.Errorf("not yet implemented")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoAssetSource = fmt.Errorf("expects *AssetSource")
|
||||
)
|
||||
|
||||
func WithInstance(instance interface{}) (source.Driver, error) {
|
||||
if _, ok := instance.(*AssetSource); !ok {
|
||||
return nil, ErrNoAssetSource
|
||||
}
|
||||
as := instance.(*AssetSource)
|
||||
|
||||
bn := &Bindata{
|
||||
path: "<go-bindata>",
|
||||
assetSource: as,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
for _, fi := range as.Names {
|
||||
m, err := source.DefaultParse(fi)
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
|
||||
if !bn.migrations.Append(m) {
|
||||
return nil, fmt.Errorf("unable to parse file %v", fi)
|
||||
}
|
||||
}
|
||||
|
||||
return bn, nil
|
||||
}
|
||||
|
||||
func (b *Bindata) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bindata) First() (version uint, err error) {
|
||||
if v, ok := b.migrations.First(); !ok {
|
||||
return 0, &os.PathError{Op: "first", Path: b.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bindata) Prev(version uint) (prevVersion uint, err error) {
|
||||
if v, ok := b.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: b.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bindata) Next(version uint) (nextVersion uint, err error) {
|
||||
if v, ok := b.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: b.path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bindata) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := b.migrations.Up(version); ok {
|
||||
body, err := b.assetSource.AssetFunc(m.Raw)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (b *Bindata) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := b.migrations.Down(version); ok {
|
||||
body, err := b.assetSource.AssetFunc(m.Raw)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package bindata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source/go_bindata/testdata"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
// wrap assets into Resource first
|
||||
s := Resource(testdata.AssetNames(),
|
||||
func(name string) ([]byte, error) {
|
||||
return testdata.Asset(name)
|
||||
})
|
||||
|
||||
d, err := WithInstance(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestWithInstance(t *testing.T) {
|
||||
// wrap assets into Resource
|
||||
s := Resource(testdata.AssetNames(),
|
||||
func(name string) ([]byte, error) {
|
||||
return testdata.Asset(name)
|
||||
})
|
||||
|
||||
_, err := WithInstance(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
b := &Bindata{}
|
||||
_, err := b.Open("")
|
||||
if err == nil {
|
||||
t.Fatal("expected err, because it's not implemented yet")
|
||||
}
|
||||
}
|
||||
Vendored
+395
@@ -0,0 +1,395 @@
|
||||
// Code generated by go-bindata.
|
||||
// sources:
|
||||
// 1_test.down.sql
|
||||
// 1_test.up.sql
|
||||
// 3_test.up.sql
|
||||
// 4_test.down.sql
|
||||
// 4_test.up.sql
|
||||
// 5_test.down.sql
|
||||
// 7_test.down.sql
|
||||
// 7_test.up.sql
|
||||
// DO NOT EDIT!
|
||||
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gz)
|
||||
clErr := gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
if clErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
type bindataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (fi bindataFileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
func (fi bindataFileInfo) Size() int64 {
|
||||
return fi.size
|
||||
}
|
||||
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
func (fi bindataFileInfo) ModTime() time.Time {
|
||||
return fi.modTime
|
||||
}
|
||||
func (fi bindataFileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
func (fi bindataFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var __1_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _1_testDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1_testDownSql,
|
||||
"1_test.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1_testDownSql() (*asset, error) {
|
||||
bytes, err := _1_testDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440324, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _1_testUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1_testUpSql,
|
||||
"1_test.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1_testUpSql() (*asset, error) {
|
||||
bytes, err := _1_testUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440319, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __3_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _3_testUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__3_testUpSql,
|
||||
"3_test.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _3_testUpSql() (*asset, error) {
|
||||
bytes, err := _3_testUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "3_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440331, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __4_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _4_testDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__4_testDownSql,
|
||||
"4_test.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _4_testDownSql() (*asset, error) {
|
||||
bytes, err := _4_testDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "4_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440337, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __4_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _4_testUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__4_testUpSql,
|
||||
"4_test.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _4_testUpSql() (*asset, error) {
|
||||
bytes, err := _4_testUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "4_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440335, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __5_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _5_testDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__5_testDownSql,
|
||||
"5_test.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _5_testDownSql() (*asset, error) {
|
||||
bytes, err := _5_testDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "5_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440340, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __7_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _7_testDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__7_testDownSql,
|
||||
"7_test.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _7_testDownSql() (*asset, error) {
|
||||
bytes, err := _7_testDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "7_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440343, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __7_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func _7_testUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__7_testUpSql,
|
||||
"7_test.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _7_testUpSql() (*asset, error) {
|
||||
bytes, err := _7_testUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "7_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440347, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if err != nil {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"1_test.down.sql": _1_testDownSql,
|
||||
"1_test.up.sql": _1_testUpSql,
|
||||
"3_test.up.sql": _3_testUpSql,
|
||||
"4_test.down.sql": _4_testDownSql,
|
||||
"4_test.up.sql": _4_testUpSql,
|
||||
"5_test.down.sql": _5_testDownSql,
|
||||
"7_test.down.sql": _7_testDownSql,
|
||||
"7_test.up.sql": _7_testUpSql,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for childName := range node.Children {
|
||||
rv = append(rv, childName)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type bintree struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*bintree
|
||||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"1_test.down.sql": &bintree{_1_testDownSql, map[string]*bintree{}},
|
||||
"1_test.up.sql": &bintree{_1_testUpSql, map[string]*bintree{}},
|
||||
"3_test.up.sql": &bintree{_3_testUpSql, map[string]*bintree{}},
|
||||
"4_test.down.sql": &bintree{_4_testDownSql, map[string]*bintree{}},
|
||||
"4_test.up.sql": &bintree{_4_testUpSql, map[string]*bintree{}},
|
||||
"5_test.down.sql": &bintree{_5_testDownSql, map[string]*bintree{}},
|
||||
"7_test.down.sql": &bintree{_7_testDownSql, map[string]*bintree{}},
|
||||
"7_test.up.sql": &bintree{_7_testUpSql, map[string]*bintree{}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreAssets restores an asset under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
// File
|
||||
if err != nil {
|
||||
return RestoreAsset(dir, name)
|
||||
}
|
||||
// Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Package godoc_vfs contains a driver that reads migrations from a virtual file
|
||||
// system.
|
||||
//
|
||||
// Implementations of the filesystem interface that read from zip files and
|
||||
// maps, as well as the definition of the filesystem interface can be found in
|
||||
// the golang.org/x/tools/godoc/vfs package.
|
||||
package godoc_vfs
|
||||
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
|
||||
"golang.org/x/tools/godoc/vfs"
|
||||
vfs_httpfs "golang.org/x/tools/godoc/vfs/httpfs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("godoc-vfs", &VFS{})
|
||||
}
|
||||
|
||||
// VFS is an implementation of driver that returns migrations from a virtual
|
||||
// file system.
|
||||
type VFS struct {
|
||||
httpfs.PartialDriver
|
||||
fs vfs.FileSystem
|
||||
path string
|
||||
}
|
||||
|
||||
// Open implements the source.Driver interface for VFS.
|
||||
//
|
||||
// Calling this function panics, instead use the WithInstance function.
|
||||
// See the package level documentation for an example.
|
||||
func (b *VFS) Open(url string) (source.Driver, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// WithInstance creates a new driver from a virtual file system.
|
||||
// If a tree named searchPath exists in the virtual filesystem, WithInstance
|
||||
// searches for migration files there.
|
||||
// It defaults to "/".
|
||||
func WithInstance(fs vfs.FileSystem, searchPath string) (source.Driver, error) {
|
||||
if searchPath == "" {
|
||||
searchPath = "/"
|
||||
}
|
||||
|
||||
bn := &VFS{
|
||||
fs: fs,
|
||||
path: searchPath,
|
||||
}
|
||||
|
||||
if err := bn.Init(vfs_httpfs.New(fs), searchPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bn, nil
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package godoc_vfs_test
|
||||
|
||||
import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/source/godoc_vfs"
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func Example_mapfs() {
|
||||
fs := mapfs.New(map[string]string{
|
||||
"1_foobar.up.sql": "1 up",
|
||||
"1_foobar.down.sql": "1 down",
|
||||
"3_foobar.up.sql": "3 up",
|
||||
"4_foobar.up.sql": "4 up",
|
||||
"4_foobar.down.sql": "4 down",
|
||||
"5_foobar.down.sql": "5 down",
|
||||
"7_foobar.up.sql": "7 up",
|
||||
"7_foobar.down.sql": "7 down",
|
||||
})
|
||||
|
||||
d, err := godoc_vfs.WithInstance(fs, "")
|
||||
if err != nil {
|
||||
panic("bad migrations found!")
|
||||
}
|
||||
m, err := migrate.NewWithSourceInstance("godoc-vfs", d, "database://foobar")
|
||||
if err != nil {
|
||||
panic("error creating the migrations")
|
||||
}
|
||||
err = m.Up()
|
||||
if err != nil {
|
||||
panic("up failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package godoc_vfs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source/godoc_vfs"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
"golang.org/x/tools/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func TestVFS(t *testing.T) {
|
||||
fs := mapfs.New(map[string]string{
|
||||
"1_foobar.up.sql": "1 up",
|
||||
"1_foobar.down.sql": "1 down",
|
||||
"3_foobar.up.sql": "3 up",
|
||||
"4_foobar.up.sql": "4 up",
|
||||
"4_foobar.down.sql": "4 down",
|
||||
"5_foobar.down.sql": "5 down",
|
||||
"7_foobar.up.sql": "7 up",
|
||||
"7_foobar.down.sql": "7 down",
|
||||
})
|
||||
|
||||
d, err := godoc_vfs.WithInstance(fs, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("Expected Open to panic")
|
||||
}
|
||||
}()
|
||||
b := &godoc_vfs.VFS{}
|
||||
if _, err := b.Open(""); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Google Cloud Storage
|
||||
|
||||
|
||||
## Import
|
||||
|
||||
```go
|
||||
import (
|
||||
_ "github.com/golang-migrate/migrate/v4/source/google_cloud_storage"
|
||||
)
|
||||
```
|
||||
|
||||
## Connection String
|
||||
|
||||
`gcs://<bucket>/<prefix>`
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package googlecloudstorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"context"
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("gcs", &gcs{})
|
||||
}
|
||||
|
||||
type gcs struct {
|
||||
bucket *storage.BucketHandle
|
||||
prefix string
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
func (g *gcs) Open(folder string) (source.Driver, error) {
|
||||
u, err := url.Parse(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := storage.NewClient(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driver := gcs{
|
||||
bucket: client.Bucket(u.Host),
|
||||
prefix: strings.Trim(u.Path, "/") + "/",
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
err = driver.loadMigrations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &driver, nil
|
||||
}
|
||||
|
||||
func (g *gcs) loadMigrations() error {
|
||||
iter := g.bucket.Objects(context.Background(), &storage.Query{
|
||||
Prefix: g.prefix,
|
||||
Delimiter: "/",
|
||||
})
|
||||
object, err := iter.Next()
|
||||
for ; err == nil; object, err = iter.Next() {
|
||||
_, fileName := path.Split(object.Name)
|
||||
m, parseErr := source.DefaultParse(fileName)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if !g.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", object.Name)
|
||||
}
|
||||
}
|
||||
if err != iterator.Done {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *gcs) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *gcs) First() (uint, error) {
|
||||
v, ok := g.migrations.First()
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (g *gcs) Prev(version uint) (uint, error) {
|
||||
v, ok := g.migrations.Prev(version)
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (g *gcs) Next(version uint) (uint, error) {
|
||||
v, ok := g.migrations.Next(version)
|
||||
if !ok {
|
||||
return 0, os.ErrNotExist
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (g *gcs) ReadUp(version uint) (io.ReadCloser, string, error) {
|
||||
if m, ok := g.migrations.Up(version); ok {
|
||||
return g.open(m)
|
||||
}
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func (g *gcs) ReadDown(version uint) (io.ReadCloser, string, error) {
|
||||
if m, ok := g.migrations.Down(version); ok {
|
||||
return g.open(m)
|
||||
}
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func (g *gcs) open(m *source.Migration) (io.ReadCloser, string, error) {
|
||||
objectPath := path.Join(g.prefix, m.Raw)
|
||||
reader, err := g.bucket.Object(objectPath).NewReader(context.Background())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return reader, m.Identifier, nil
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package googlecloudstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fsouza/fake-gcs-server/fakestorage"
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
server := fakestorage.NewServer([]fakestorage.Object{
|
||||
{BucketName: "some-bucket", Name: "staging/migrations/1_foobar.up.sql", Content: []byte("1 up")},
|
||||
{BucketName: "some-bucket", Name: "staging/migrations/1_foobar.down.sql", Content: []byte("1 down")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/1_foobar.up.sql", Content: []byte("1 up")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/1_foobar.down.sql", Content: []byte("1 down")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/3_foobar.up.sql", Content: []byte("3 up")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/4_foobar.up.sql", Content: []byte("4 up")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/4_foobar.down.sql", Content: []byte("4 down")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/5_foobar.down.sql", Content: []byte("5 down")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/7_foobar.up.sql", Content: []byte("7 up")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/7_foobar.down.sql", Content: []byte("7 down")},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/not-a-migration.txt"},
|
||||
{BucketName: "some-bucket", Name: "prod/migrations/0-random-stuff/whatever.txt"},
|
||||
})
|
||||
defer server.Stop()
|
||||
driver := gcs{
|
||||
bucket: server.Client().Bucket("some-bucket"),
|
||||
prefix: "prod/migrations/",
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
err := driver.loadMigrations()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st.Test(t, &driver)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# httpfs
|
||||
|
||||
## Usage
|
||||
|
||||
This package could be used to create new migration source drivers that uses
|
||||
`http.FileSystem` to read migration files.
|
||||
|
||||
Struct `httpfs.PartialDriver` partly implements the `source.Driver` interface. It has all
|
||||
the methods except for `Open()`. Embedding this struct and adding `Open()` method
|
||||
allows users of this package to create new migration sources. Example:
|
||||
|
||||
```go
|
||||
struct mydriver {
|
||||
httpfs.PartialDriver
|
||||
}
|
||||
|
||||
func (d *mydriver) Open(url string) (source.Driver, error) {
|
||||
var fs http.FileSystem
|
||||
var path string
|
||||
var ds mydriver
|
||||
|
||||
// acquire fs and path from url
|
||||
// set-up ds if necessary
|
||||
|
||||
if err := ds.Init(fs, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ds, nil
|
||||
}
|
||||
```
|
||||
|
||||
This package also provides a simple `source.Driver` implementation that works
|
||||
with `http.FileSystem` provided by the user of this package. It is created with
|
||||
`httpfs.New()` call.
|
||||
|
||||
Example of using `http.Dir()` to read migrations from `sql` directory:
|
||||
|
||||
```go
|
||||
src, err := httpfs.New(http.Dir("sql"))
|
||||
if err != nil {
|
||||
// do something
|
||||
}
|
||||
m, err := migrate.NewWithSourceInstance("httpfs", src, "database://url")
|
||||
if err != nil {
|
||||
// do something
|
||||
}
|
||||
err = m.Up()
|
||||
...
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
package httpfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
// driver is a migration source driver for reading migrations from
|
||||
// http.FileSystem instances. It implements source.Driver interface and can be
|
||||
// used as a migration source for the main migrate library.
|
||||
type driver struct {
|
||||
PartialDriver
|
||||
}
|
||||
|
||||
// New creates a new migrate source driver from a http.FileSystem instance and a
|
||||
// relative path to migration files within the virtual FS.
|
||||
func New(fs http.FileSystem, path string) (source.Driver, error) {
|
||||
var d driver
|
||||
if err := d.Init(fs, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// Open completes the implementetion of source.Driver interface. Other methods
|
||||
// are implemented by the embedded PartialDriver struct.
|
||||
func (d *driver) Open(url string) (source.Driver, error) {
|
||||
return nil, errors.New("Open() cannot be called on the httpfs passthrough driver")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package httpfs_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
func TestNewOK(t *testing.T) {
|
||||
d, err := httpfs.New(http.Dir("testdata"), "sql")
|
||||
if err != nil {
|
||||
t.Errorf("New() expected not error, got: %s", err)
|
||||
}
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestNewErrors(t *testing.T) {
|
||||
d, err := httpfs.New(http.Dir("does-not-exist"), "")
|
||||
if err == nil {
|
||||
t.Errorf("New() expected to return error")
|
||||
}
|
||||
if d != nil {
|
||||
t.Errorf("New() expected to return nil driver")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
d, err := httpfs.New(http.Dir("testdata/sql"), "")
|
||||
if err != nil {
|
||||
t.Error("New() expected no error")
|
||||
return
|
||||
}
|
||||
d, err = d.Open("")
|
||||
if d != nil {
|
||||
t.Error("Open() expected to return nil driver")
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("Open() expected to return error")
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package httpfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
// PartialDriver is a helper service for creating new source drivers working with
|
||||
// http.FileSystem instances. It implements all source.Driver interface methods
|
||||
// except for Open(). New driver could embed this struct and add missing Open()
|
||||
// method.
|
||||
//
|
||||
// To prepare PartialDriver for use Init() function.
|
||||
type PartialDriver struct {
|
||||
migrations *source.Migrations
|
||||
fs http.FileSystem
|
||||
path string
|
||||
}
|
||||
|
||||
// Init prepares not initialized PartialDriver instance to read migrations from a
|
||||
// http.FileSystem instance and a relative path.
|
||||
func (p *PartialDriver) Init(fs http.FileSystem, path string) error {
|
||||
root, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := root.Readdir(0)
|
||||
if err != nil {
|
||||
_ = root.Close()
|
||||
return err
|
||||
}
|
||||
if err = root.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ms := source.NewMigrations()
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
m, err := source.DefaultParse(file.Name())
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
|
||||
if !ms.Append(m) {
|
||||
return source.ErrDuplicateMigration{
|
||||
Migration: *m,
|
||||
FileInfo: file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.fs = fs
|
||||
p.path = path
|
||||
p.migrations = ms
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is part of source.Driver interface implementation. This is a no-op.
|
||||
func (p *PartialDriver) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First is part of source.Driver interface implementation.
|
||||
func (p *PartialDriver) First() (version uint, err error) {
|
||||
if version, ok := p.migrations.First(); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &os.PathError{
|
||||
Op: "first",
|
||||
Path: p.path,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Prev is part of source.Driver interface implementation.
|
||||
func (p *PartialDriver) Prev(version uint) (prevVersion uint, err error) {
|
||||
if version, ok := p.migrations.Prev(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &os.PathError{
|
||||
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: p.path,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Next is part of source.Driver interface implementation.
|
||||
func (p *PartialDriver) Next(version uint) (nextVersion uint, err error) {
|
||||
if version, ok := p.migrations.Next(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &os.PathError{
|
||||
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: p.path,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadUp is part of source.Driver interface implementation.
|
||||
func (p *PartialDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := p.migrations.Up(version); ok {
|
||||
body, err := p.open(path.Join(p.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{
|
||||
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: p.path,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDown is part of source.Driver interface implementation.
|
||||
func (p *PartialDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := p.migrations.Down(version); ok {
|
||||
body, err := p.open(path.Join(p.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{
|
||||
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: p.path,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PartialDriver) open(path string) (http.File, error) {
|
||||
f, err := p.fs.Open(path)
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
// Some non-standard file systems may return errors that don't include the path, that
|
||||
// makes debugging harder.
|
||||
if !errors.As(err, new(*os.PathError)) {
|
||||
err = &os.PathError{
|
||||
Op: "open",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package httpfs_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
type driver struct{ httpfs.PartialDriver }
|
||||
|
||||
func (d *driver) Open(url string) (source.Driver, error) { return nil, errors.New("X") }
|
||||
|
||||
type driverExample struct {
|
||||
httpfs.PartialDriver
|
||||
}
|
||||
|
||||
func (d *driverExample) Open(url string) (source.Driver, error) {
|
||||
parts := strings.Split(url, ":")
|
||||
dir := parts[0]
|
||||
path := ""
|
||||
if len(parts) >= 2 {
|
||||
path = parts[1]
|
||||
}
|
||||
|
||||
var de driverExample
|
||||
return &de, de.Init(http.Dir(dir), path)
|
||||
}
|
||||
|
||||
func TestDriverExample(t *testing.T) {
|
||||
d, err := (*driverExample)(nil).Open("testdata:sql")
|
||||
if err != nil {
|
||||
t.Errorf("Open() returned error: %s", err)
|
||||
}
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestPartialDriverInit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fs http.FileSystem
|
||||
path string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "valid dir and empty path",
|
||||
fs: http.Dir("testdata/sql"),
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "valid dir and non-empty path",
|
||||
fs: http.Dir("testdata"),
|
||||
path: "sql",
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "invalid dir",
|
||||
fs: http.Dir("does-not-exist"),
|
||||
},
|
||||
{
|
||||
name: "file instead of dir",
|
||||
fs: http.Dir("testdata/sql/1_foobar.up.sql"),
|
||||
},
|
||||
{
|
||||
name: "dir with duplicates",
|
||||
fs: http.Dir("testdata/duplicates"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var d driver
|
||||
err := d.Init(test.fs, test.path)
|
||||
if test.ok {
|
||||
if err != nil {
|
||||
t.Errorf("Init() returned error %s", err)
|
||||
}
|
||||
st.Test(t, &d)
|
||||
if err = d.Close(); err != nil {
|
||||
t.Errorf("Init().Close() returned error %s", err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("Init() expected error but did not get one")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFirstWithNoMigrations(t *testing.T) {
|
||||
var d driver
|
||||
fs := http.Dir("testdata/no-migrations")
|
||||
|
||||
if err := d.Init(fs, ""); err != nil {
|
||||
t.Errorf("No error on Init() expected, got: %v", err)
|
||||
}
|
||||
|
||||
if _, err := d.First(); err == nil {
|
||||
t.Errorf("Expected error on First(), got: %v", err)
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
1 up
|
||||
+1
@@ -0,0 +1 @@
|
||||
1 up
|
||||
go/pkg/mod/github.com/golang-migrate/migrate/v4@v4.17.1/source/httpfs/testdata/sql/1_foobar.down.sql
Vendored
+1
@@ -0,0 +1 @@
|
||||
1 down
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
1 up
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
3 up
|
||||
go/pkg/mod/github.com/golang-migrate/migrate/v4@v4.17.1/source/httpfs/testdata/sql/4_foobar.down.sql
Vendored
+1
@@ -0,0 +1 @@
|
||||
4 down
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
4 up
|
||||
go/pkg/mod/github.com/golang-migrate/migrate/v4@v4.17.1/source/httpfs/testdata/sql/5_foobar.down.sql
Vendored
+1
@@ -0,0 +1 @@
|
||||
5 down
|
||||
go/pkg/mod/github.com/golang-migrate/migrate/v4@v4.17.1/source/httpfs/testdata/sql/7_foobar.down.sql
Vendored
+1
@@ -0,0 +1 @@
|
||||
7 down
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
7 up
|
||||
@@ -0,0 +1,3 @@
|
||||
# iofs
|
||||
|
||||
https://pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Package iofs provides the Go 1.16+ io/fs#FS driver.
|
||||
|
||||
It can accept various file systems (like embed.FS, archive/zip#Reader) implementing io/fs#FS.
|
||||
|
||||
This driver cannot be used with Go versions 1.15 and below.
|
||||
|
||||
Also, Opening with a URL scheme is not supported.
|
||||
*/
|
||||
package iofs
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package iofs_test
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
//go:embed testdata/migrations/*.sql
|
||||
var fs embed.FS
|
||||
|
||||
func Example() {
|
||||
d, err := iofs.New(fs, "testdata/migrations")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
m, err := migrate.NewWithSourceInstance("iofs", d, "postgres://postgres@localhost/postgres?sslmode=disable")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = m.Up()
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package iofs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
)
|
||||
|
||||
type driver struct {
|
||||
PartialDriver
|
||||
}
|
||||
|
||||
// New returns a new Driver from io/fs#FS and a relative path.
|
||||
func New(fsys fs.FS, path string) (source.Driver, error) {
|
||||
var i driver
|
||||
if err := i.Init(fsys, path); err != nil {
|
||||
return nil, fmt.Errorf("failed to init driver with path %s: %w", path, err)
|
||||
}
|
||||
return &i, nil
|
||||
}
|
||||
|
||||
// Open is part of source.Driver interface implementation.
|
||||
// Open cannot be called on the iofs passthrough driver.
|
||||
func (d *driver) Open(url string) (source.Driver, error) {
|
||||
return nil, errors.New("Open() cannot be called on the iofs passthrough driver")
|
||||
}
|
||||
|
||||
// PartialDriver is a helper service for creating new source drivers working with
|
||||
// io/fs.FS instances. It implements all source.Driver interface methods
|
||||
// except for Open(). New driver could embed this struct and add missing Open()
|
||||
// method.
|
||||
//
|
||||
// To prepare PartialDriver for use Init() function.
|
||||
type PartialDriver struct {
|
||||
migrations *source.Migrations
|
||||
fsys fs.FS
|
||||
path string
|
||||
}
|
||||
|
||||
// Init prepares not initialized IoFS instance to read migrations from a
|
||||
// io/fs#FS instance and a relative path.
|
||||
func (d *PartialDriver) Init(fsys fs.FS, path string) error {
|
||||
entries, err := fs.ReadDir(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ms := source.NewMigrations()
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
m, err := source.DefaultParse(e.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
file, err := e.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ms.Append(m) {
|
||||
return source.ErrDuplicateMigration{
|
||||
Migration: *m,
|
||||
FileInfo: file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.fsys = fsys
|
||||
d.path = path
|
||||
d.migrations = ms
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is part of source.Driver interface implementation.
|
||||
// Closes the file system if possible.
|
||||
func (d *PartialDriver) Close() error {
|
||||
c, ok := d.fsys.(io.Closer)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
// First is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) First() (version uint, err error) {
|
||||
if version, ok := d.migrations.First(); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "first",
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Prev is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) Prev(version uint) (prevVersion uint, err error) {
|
||||
if version, ok := d.migrations.Prev(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// Next is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) Next(version uint) (nextVersion uint, err error) {
|
||||
if version, ok := d.migrations.Next(version); ok {
|
||||
return version, nil
|
||||
}
|
||||
return 0, &fs.PathError{
|
||||
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadUp is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := d.migrations.Up(version); ok {
|
||||
body, err := d.open(path.Join(d.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &fs.PathError{
|
||||
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDown is part of source.Driver interface implementation.
|
||||
func (d *PartialDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := d.migrations.Down(version); ok {
|
||||
body, err := d.open(path.Join(d.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &fs.PathError{
|
||||
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
|
||||
Path: d.path,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PartialDriver) open(path string) (fs.File, error) {
|
||||
f, err := d.fsys.Open(path)
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
// Some non-standard file systems may return errors that don't include the path, that
|
||||
// makes debugging harder.
|
||||
if !errors.As(err, new(*fs.PathError)) {
|
||||
err = &fs.PathError{
|
||||
Op: "open",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package iofs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
// reuse the embed.FS set in example_test.go
|
||||
d, err := iofs.New(fs, "testdata/migrations")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
1 down
|
||||
+1
@@ -0,0 +1 @@
|
||||
1 up
|
||||
+1
@@ -0,0 +1 @@
|
||||
3 up
|
||||
+1
@@ -0,0 +1 @@
|
||||
4 down
|
||||
+1
@@ -0,0 +1 @@
|
||||
4 up
|
||||
+1
@@ -0,0 +1 @@
|
||||
5 down
|
||||
+1
@@ -0,0 +1 @@
|
||||
7 down
|
||||
+1
@@ -0,0 +1 @@
|
||||
7 up
|
||||
@@ -0,0 +1,133 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Direction is either up or down.
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
Down Direction = "down"
|
||||
Up Direction = "up"
|
||||
)
|
||||
|
||||
// Migration is a helper struct for source drivers that need to
|
||||
// build the full directory tree in memory.
|
||||
// Migration is fully independent from migrate.Migration.
|
||||
type Migration struct {
|
||||
// Version is the version of this migration.
|
||||
Version uint
|
||||
|
||||
// Identifier can be any string that helps identifying
|
||||
// this migration in the source.
|
||||
Identifier string
|
||||
|
||||
// Direction is either Up or Down.
|
||||
Direction Direction
|
||||
|
||||
// Raw holds the raw location path to this migration in source.
|
||||
// ReadUp and ReadDown will use this.
|
||||
Raw string
|
||||
}
|
||||
|
||||
// Migrations wraps Migration and has an internal index
|
||||
// to keep track of Migration order.
|
||||
type Migrations struct {
|
||||
index uintSlice
|
||||
migrations map[uint]map[Direction]*Migration
|
||||
}
|
||||
|
||||
func NewMigrations() *Migrations {
|
||||
return &Migrations{
|
||||
index: make(uintSlice, 0),
|
||||
migrations: make(map[uint]map[Direction]*Migration),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Migrations) Append(m *Migration) (ok bool) {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if i.migrations[m.Version] == nil {
|
||||
i.migrations[m.Version] = make(map[Direction]*Migration)
|
||||
}
|
||||
|
||||
// reject duplicate versions
|
||||
if _, dup := i.migrations[m.Version][m.Direction]; dup {
|
||||
return false
|
||||
}
|
||||
|
||||
i.migrations[m.Version][m.Direction] = m
|
||||
i.buildIndex()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *Migrations) buildIndex() {
|
||||
i.index = make(uintSlice, 0, len(i.migrations))
|
||||
for version := range i.migrations {
|
||||
i.index = append(i.index, version)
|
||||
}
|
||||
sort.Slice(i.index, func(x, y int) bool {
|
||||
return i.index[x] < i.index[y]
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Migrations) First() (version uint, ok bool) {
|
||||
if len(i.index) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return i.index[0], true
|
||||
}
|
||||
|
||||
func (i *Migrations) Prev(version uint) (prevVersion uint, ok bool) {
|
||||
pos := i.findPos(version)
|
||||
if pos >= 1 && len(i.index) > pos-1 {
|
||||
return i.index[pos-1], true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Next(version uint) (nextVersion uint, ok bool) {
|
||||
pos := i.findPos(version)
|
||||
if pos >= 0 && len(i.index) > pos+1 {
|
||||
return i.index[pos+1], true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Up(version uint) (m *Migration, ok bool) {
|
||||
if _, ok := i.migrations[version]; ok {
|
||||
if mx, ok := i.migrations[version][Up]; ok {
|
||||
return mx, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *Migrations) Down(version uint) (m *Migration, ok bool) {
|
||||
if _, ok := i.migrations[version]; ok {
|
||||
if mx, ok := i.migrations[version][Down]; ok {
|
||||
return mx, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *Migrations) findPos(version uint) int {
|
||||
if len(i.index) > 0 {
|
||||
ix := i.index.Search(version)
|
||||
if ix < len(i.index) && i.index[ix] == version {
|
||||
return ix
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type uintSlice []uint
|
||||
|
||||
func (s uintSlice) Search(x uint) int {
|
||||
return sort.Search(len(s), func(i int) bool { return s[i] >= x })
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewMigrations(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestBuildIndex(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestPrev(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestUp(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestDown(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestFindPos(t *testing.T) {
|
||||
m := Migrations{index: uintSlice{1, 2, 3}}
|
||||
if p := m.findPos(0); p != -1 {
|
||||
t.Errorf("expected -1, got %v", p)
|
||||
}
|
||||
if p := m.findPos(1); p != 0 {
|
||||
t.Errorf("expected 0, got %v", p)
|
||||
}
|
||||
if p := m.findPos(3); p != 2 {
|
||||
t.Errorf("expected 2, got %v", p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrParse = fmt.Errorf("no match")
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultParse = Parse
|
||||
DefaultRegex = Regex
|
||||
)
|
||||
|
||||
// Regex matches the following pattern:
|
||||
//
|
||||
// 123_name.up.ext
|
||||
// 123_name.down.ext
|
||||
var Regex = regexp.MustCompile(`^([0-9]+)_(.*)\.(` + string(Down) + `|` + string(Up) + `)\.(.*)$`)
|
||||
|
||||
// Parse returns Migration for matching Regex pattern.
|
||||
func Parse(raw string) (*Migration, error) {
|
||||
m := Regex.FindStringSubmatch(raw)
|
||||
if len(m) == 5 {
|
||||
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Migration{
|
||||
Version: uint(versionUint64),
|
||||
Identifier: m[2],
|
||||
Direction: Direction(m[3]),
|
||||
Raw: raw,
|
||||
}, nil
|
||||
}
|
||||
return nil, ErrParse
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectErr error
|
||||
expectMigration *Migration
|
||||
}{
|
||||
{
|
||||
name: "1_foobar.up.sql",
|
||||
expectErr: nil,
|
||||
expectMigration: &Migration{
|
||||
Version: 1,
|
||||
Identifier: "foobar",
|
||||
Direction: Up,
|
||||
Raw: "1_foobar.up.sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1_foobar.down.sql",
|
||||
expectErr: nil,
|
||||
expectMigration: &Migration{
|
||||
Version: 1,
|
||||
Identifier: "foobar",
|
||||
Direction: Down,
|
||||
Raw: "1_foobar.down.sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1_f-o_ob+ar.up.sql",
|
||||
expectErr: nil,
|
||||
expectMigration: &Migration{
|
||||
Version: 1,
|
||||
Identifier: "f-o_ob+ar",
|
||||
Direction: Up,
|
||||
Raw: "1_f-o_ob+ar.up.sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1485385885_foobar.up.sql",
|
||||
expectErr: nil,
|
||||
expectMigration: &Migration{
|
||||
Version: 1485385885,
|
||||
Identifier: "foobar",
|
||||
Direction: Up,
|
||||
Raw: "1485385885_foobar.up.sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "20170412214116_date_foobar.up.sql",
|
||||
expectErr: nil,
|
||||
expectMigration: &Migration{
|
||||
Version: 20170412214116,
|
||||
Identifier: "date_foobar",
|
||||
Direction: Up,
|
||||
Raw: "20170412214116_date_foobar.up.sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "-1_foobar.up.sql",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
{
|
||||
name: "foobar.up.sql",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
{
|
||||
name: "1.up.sql",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
{
|
||||
name: "1_foobar.sql",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
{
|
||||
name: "1_foobar.up",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
{
|
||||
name: "1_foobar.down",
|
||||
expectErr: ErrParse,
|
||||
expectMigration: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
f, err := Parse(v.name)
|
||||
|
||||
if err != v.expectErr {
|
||||
t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i)
|
||||
}
|
||||
|
||||
if v.expectMigration != nil && *f != *v.expectMigration {
|
||||
t.Errorf("expected %+v, got %+v, in %v", *v.expectMigration, *f, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# pkger
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/markbates/pkger"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/pkger"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pkger.Include("/module/path/to/migrations")
|
||||
m, err := migrate.New("pkger:///module/path/to/migrations", "postgres://postgres@localhost/postgres?sslmode=disable")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if err := m.Up(); errors.Is(err, migrate.ErrNoChange) {
|
||||
log.Println(err)
|
||||
} else if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user