Skip to content

Commit

Permalink
Add http.FileSystem migration source driver support
Browse files Browse the repository at this point in the history
This PR adds a new migration source driver capable of reading files from
any source that implements http.FileSystem interface.

Notable http.FileSystem interface implementations:

* http.Dir() - wrapper over local file-system
* github.com/shurcooL/vfsgen

Because user of this package is responsible for getting an
implementation of http.FileSystem, this driver does not support creating
instances from driver URLs.
  • Loading branch information
fln authored and Julius Kriukas committed Dec 6, 2019
1 parent 41b578a commit b5bd00d
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 0 deletions.
16 changes: 16 additions & 0 deletions source/httpfs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# httpfs

## Usage

To create migration data source from `http.FileSystem` instance use
`WithInstance()` function. Users of this package are responsible for getting
`http.FileSystem` instance. It is not possible to create httpfs instance from
URL.

Example of using `http.Dir()` to read migrations from `sql` directory:

```go
src, err := httpfs.WithInstance(http.Dir("sql"), "")
m, err := migrate.NewWithSourceInstance("httpfs", src, "database://url")
err = m.Up()
```
139 changes: 139 additions & 0 deletions source/httpfs/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package httpfs

import (
"fmt"
"io"
"net/http"
"os"
"path"

"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 {
migrations *source.Migrations
fs http.FileSystem
path string
}

// WithInstance creates new migrate source driver from a http.FileSystem
// instance and a relative path to migration files within the virtual FS.
func WithInstance(fs http.FileSystem, path string) (source.Driver, error) {
root, err := fs.Open(path)
if err != nil {
return nil, err
}
defer root.Close()

files, err := root.Readdir(0)
if err != nil {
return nil, 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 nil, fmt.Errorf("duplicate migration file: %v", file.Name())
}
}

return &driver{
fs: fs,
path: path,
migrations: ms,
}, nil
}

// Open is part of source.Driver interface implementation. It always returns
// error because http.FileSystem must be provided by the user of this package
// and created using WithInstance() function.
func (h *driver) Open(url string) (source.Driver, error) {
return nil, fmt.Errorf("not implemented")
}

// Close is part of source.Driver interface implementation. This is a no-op.
func (h *driver) Close() error {
return nil
}

// First is part of source.Driver interface implementation.
func (h *driver) First() (version uint, err error) {
if version, ok := h.migrations.First(); ok {
return version, nil
}
return 0, &os.PathError{
Op: "first",
Path: h.path,
Err: os.ErrNotExist,
}
}

// Prev is part of source.Driver interface implementation.
func (h *driver) Prev(version uint) (prevVersion uint, err error) {
if version, ok := h.migrations.Prev(version); ok {
return version, nil
}
return 0, &os.PathError{
Op: fmt.Sprintf("prev for version %v", version),
Path: h.path,
Err: os.ErrNotExist,
}
}

// Next is part of source.Driver interface implementation.
func (h *driver) Next(version uint) (nextVersion uint, err error) {
if version, ok := h.migrations.Next(version); ok {
return version, nil
}
return 0, &os.PathError{
Op: fmt.Sprintf("next for version %v", version),
Path: h.path,
Err: os.ErrNotExist,
}
}

// ReadUp is part of source.Driver interface implementation.
func (h *driver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
m, ok := h.migrations.Up(version)
if !ok {
return nil, "", &os.PathError{
Op: fmt.Sprintf("read version %v", version),
Path: h.path,
Err: os.ErrNotExist,
}
}
body, err := h.fs.Open(path.Join(h.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}

// ReadDown is part of source.Driver interface implementation.
func (h *driver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
m, ok := h.migrations.Down(version)
if !ok {
return nil, "", &os.PathError{
Op: fmt.Sprintf("read version %v", version),
Path: h.path,
Err: os.ErrNotExist,
}
}
body, err := h.fs.Open(path.Join(h.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}
38 changes: 38 additions & 0 deletions source/httpfs/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package httpfs

import (
"net/http"
"testing"

st "github.com/golang-migrate/migrate/v4/source/testing"
)

func TestWithInstance(t *testing.T) {
t.Run("empty path", func(t *testing.T) {
d, err := WithInstance(http.Dir("testdata/sql"), "")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
})

t.Run("with path", func(t *testing.T) {
d, err := WithInstance(http.Dir("testdata"), "sql")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
})

}

func TestOpen(t *testing.T) {
b := &driver{}
d, err := b.Open("")
if d != nil {
t.Error("Expected Open to return nil driver")
}
if err == nil {
t.Error("Expected Open to return error")
}
}
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/1_foobar.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 down
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/1_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 up
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/3_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3 up
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/4_foobar.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4 down
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/4_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4 up
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/5_foobar.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5 down
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/7_foobar.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7 down
1 change: 1 addition & 0 deletions source/httpfs/testdata/sql/7_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7 up

0 comments on commit b5bd00d

Please sign in to comment.