Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add io/fs#FS Driver #471 #472

Merged
merged 15 commits into from
Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab iofs
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver firebird neo4j
DATABASE_TEST ?= $(DATABASE) sqlite sqlcipher
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
Expand Down
8 changes: 8 additions & 0 deletions internal/cli/build_iofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// +build go1.16
// +build iofs

package cli

import (
_ "github.com/golang-migrate/migrate/v4/source/iofs"
)
38 changes: 38 additions & 0 deletions source/iofs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# iofs

Driver with file system interface (`io/fs#FS`) supported from Go 1.16.

This Driver cannot be used with Go versions 1.15 and below.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Reword this to something like just:

Package iofs provides a Go 1.16+ io/fs driver which is compatible with Go 1.16+ file //embed directives.

Side note: I think all the documentation in this file would be better placed into a package-level godoc documentation comment, so that on https://pkg.go.dev this package shows up with package docs and usage examples similar to this and this

Feel free to ignore if this is not helpful :)


## Usage

Directory embedding example

```go
package main

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 migrations
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//go:embed migrations
//go:embed migrations/*.sql

May be useful to describe this to avoid people from accidentally embedding their own README.md, .gitignore, .DS_Store, etc.

var fs embed.FS

func main() {
d, err := iofs.WithInstance(fs, "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()
// ...
}
```
164 changes: 164 additions & 0 deletions source/iofs/iofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// +build go1.16

package iofs

import (
"errors"
"fmt"
"io"
"io/fs"
"path"
"strconv"

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

func init() {
source.Register("iofs", &IoFS{})
}

// IoFS is a source driver for io/fs#FS.
type IoFS struct {
migrations *source.Migrations
fsys fs.FS
path string
}

// Open by url does not supported with IoFS.
func (i *IoFS) Open(url string) (source.Driver, error) {
return nil, errors.New("iofs driver does not support open by url")
}

// WithInstance wraps io/fs#FS as source.Driver.
func WithInstance(fsys fs.FS, path string) (source.Driver, error) {
var i IoFS
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
}

// Init prepares not initialized IoFS instance to read migrations from a
// fs.FS instance and a relative path.
func (p *IoFS) Init(fsys fs.FS, path string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts about moving most of this implementation to a PartialDriver, similar to httpfs? We'd also need to rename WithInstance() to New() and changeIoFS to driver. That way other drivers can embed the partial driver and implement their own Open() receiver function.

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 {
continue
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is continue OK like any other error case?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://tip.golang.org/pkg/io/fs/#DirEntry an error in this case may mean:

If the file has been removed or renamed since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist).

But I think it can also just represent any other IO error, in e.g. the case the FS interface is backed by a real FS and not embed. I think ignoring the error and continueing is wrong here, instead any IO error should be reported back to the caller of the function I think (also in case of DefaultParse error above)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that errors should be returned and not ignored. Ignored errors will make bugs harder to debug.

}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
FileInfo: file,
}
}
}

p.fsys = fsys
p.path = path
p.migrations = ms
return nil
}

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

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

// Prev is part of source.Driver interface implementation.
func (p *IoFS) Prev(version uint) (prevVersion uint, err error) {
if version, ok := p.migrations.Prev(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: fs.ErrNotExist,
}
}

// Next is part of source.Driver interface implementation.
func (p *IoFS) Next(version uint) (nextVersion uint, err error) {
if version, ok := p.migrations.Next(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: fs.ErrNotExist,
}
}

// ReadUp is part of source.Driver interface implementation.
func (p *IoFS) 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, "", &fs.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: fs.ErrNotExist,
}
}

// ReadDown is part of source.Driver interface implementation.
func (p *IoFS) 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, "", &fs.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: fs.ErrNotExist,
}
}

func (p *IoFS) open(path string) (fs.File, error) {
f, err := p.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
}
31 changes: 31 additions & 0 deletions source/iofs/iofs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// +build go1.16

package iofs_test

import (
"embed"
"testing"

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

//go:embed testdata
var fs embed.FS

func Test(t *testing.T) {
d, err := iofs.WithInstance(fs, "testdata")
if err != nil {
t.Fatal(err)
}

st.Test(t, d)
}

func TestOpen(t *testing.T) {
i := new(iofs.IoFS)
_, err := i.Open("")
if err == nil {
t.Fatal("iofs driver does not support open by url")
}
}
1 change: 1 addition & 0 deletions source/iofs/testdata/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/iofs/testdata/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/iofs/testdata/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/iofs/testdata/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/iofs/testdata/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/iofs/testdata/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/iofs/testdata/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/iofs/testdata/7_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7 up