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 2 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"
)
9 changes: 6 additions & 3 deletions source/errors.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package source

import "os"

// ErrDuplicateMigration is an error type for reporting duplicate migration
// files.
type ErrDuplicateMigration struct {
Migration
os.FileInfo
FileInfo
Copy link
Member

Choose a reason for hiding this comment

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

Why not use os.FileInfo or fs.FileInfo?

You can use fs.DirEntry.Info() to get the fs.FileInfo

}

// Error implements error interface.
func (e ErrDuplicateMigration) Error() string {
return "duplicate migration file: " + e.Name()
}

// FileInfo is the interface that extracts the minimum required function from os.FileInfo by ErrDuplicateMigration.
type FileInfo interface {
Name() string
}
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()
// ...
}
```
160 changes: 160 additions & 0 deletions source/iofs/iofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// +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 {
Copy link
Member

Choose a reason for hiding this comment

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

Rename to IoFS since abbreviations should be in all caps.

migrations *source.Migrations
fsys fs.ReadDirFS
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.ReadDirFS, 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.ReadDirFS instance and a relative path.
func (p *Iofs) Init(fsys fs.ReadDirFS, 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.

Why use fs.ReadDirFS instead of fs.FS? Does using ReadDir directly through the FS save iops? e.g. don't need to open a file, stat it, and finally readdir?

Does this optimization reduce compatibility with different filesystems?

We're probably better off using fs.ReadDir

entries, err := fsys.ReadDir(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
}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
FileInfo: e,
}
}
}

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