mirror of https://github.com/status-im/fathom.git
use packr to embed migration files & web assets into binary. closes #8
This commit is contained in:
parent
61753a713c
commit
0683cac2bf
4
Makefile
4
Makefile
|
@ -56,7 +56,3 @@ $(EXECUTABLE): $(SOURCES)
|
|||
docker:
|
||||
docker build -t metalmatze/ana:latest .
|
||||
|
||||
.PHONY: assets
|
||||
assets:
|
||||
if [ ! -d "node_modules" ]; then npm install; fi
|
||||
gulp
|
||||
|
|
|
@ -17,7 +17,7 @@ For getting a development version of Fathom up & running, please go through the
|
|||
1. compile into binary & prepare assets: `make all`
|
||||
1. Rename `.env.example` to `.env` and set your database credentials.
|
||||
1. Create your user account: `fathom register <email> <password>`
|
||||
1. Start the webserver: `fathom server --webroot=./build --port=8080` & visit **localhost:8080** to access your analytics dashboard.
|
||||
1. Start the webserver: `fathom server --port=8080` & visit **localhost:8080** to access your analytics dashboard.
|
||||
|
||||
To start tracking, include the following JavaScript on your site and replace `yourfathom.com` with the URL to your Fathom instance.
|
||||
|
||||
|
|
17
fathom.go
17
fathom.go
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/usefathom/fathom/pkg/commands"
|
||||
"github.com/usefathom/fathom/pkg/counter"
|
||||
"github.com/usefathom/fathom/pkg/datastore"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
|
@ -27,8 +26,6 @@ var (
|
|||
registerPassword = register.Arg("password", "Password for user.").Required().String()
|
||||
server = app.Command("server", "Start webserver.").Default()
|
||||
serverPort = server.Flag("port", "Port to listen on.").Default("8080").Int()
|
||||
serverWebRoot = server.Flag("webroot", "Root directory of static assets").Default("./").String()
|
||||
archive = app.Command("archive", "Process unarchived data.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -46,15 +43,8 @@ func main() {
|
|||
commands.Register(*registerEmail, *registerPassword)
|
||||
|
||||
case "server":
|
||||
commands.Server(*serverPort, *serverWebRoot)
|
||||
|
||||
case "archive":
|
||||
err := counter.Aggregate()
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
commands.Server(*serverPort)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func parseConfig() *Config {
|
||||
|
@ -65,6 +55,11 @@ func parseConfig() *Config {
|
|||
log.Fatalf("Error parsing Fathom config from environment: %s", err)
|
||||
}
|
||||
|
||||
// alias sqlite to sqlite3
|
||||
if cfg.Database.Driver == "sqlite" {
|
||||
cfg.Database.Driver = "sqlite3"
|
||||
}
|
||||
|
||||
// if secret key is empty, use a randomly generated one to ease first-time installation
|
||||
if cfg.Secret == "" {
|
||||
cfg.Secret = randomString(40)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Routes(webroot string) *mux.Router {
|
||||
func Routes() *mux.Router {
|
||||
// register routes
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/collect", NewCollectHandler()).Methods(http.MethodGet)
|
||||
|
@ -21,7 +22,7 @@ func Routes(webroot string) *mux.Router {
|
|||
r.Handle("/api/stats/pages", Authorize(GetPageStatsHandler)).Methods(http.MethodGet)
|
||||
r.Handle("/api/stats/referrers", Authorize(GetReferrerStatsHandler)).Methods(http.MethodGet)
|
||||
|
||||
r.Path("/tracker.js").Handler(http.FileServer(http.Dir(webroot + "/js/")))
|
||||
r.PathPrefix("/").Handler(http.FileServer(http.Dir(webroot)))
|
||||
r.Path("/tracker.js").Handler(http.FileServer(packr.NewBox("./../../build/js")))
|
||||
r.PathPrefix("/").Handler(http.FileServer(packr.NewBox("./../../build")))
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
)
|
||||
|
||||
// Server starts the HTTP server, listening on the given port
|
||||
func Server(port int, webroot string) {
|
||||
r := api.Routes(webroot)
|
||||
log.Printf("Now serving %s on port %d/\n", webroot, port)
|
||||
func Server(port int) {
|
||||
r := api.Routes()
|
||||
log.Printf("Now serving on port %d/\n", port)
|
||||
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%d", port), handlers.LoggingHandler(os.Stdout, r))
|
||||
if err != nil {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
_ "github.com/go-sql-driver/mysql" // mysql driver
|
||||
"github.com/jmoiron/sqlx"
|
||||
//_ "github.com/lib/pq" // postgresql driver
|
||||
"github.com/gobuffalo/packr"
|
||||
_ "github.com/mattn/go-sqlite3" //sqlite3 driver
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -34,10 +35,9 @@ func New(c *Config) *sqlx.DB {
|
|||
|
||||
// TODO: Move to command (but still auto-run on boot).
|
||||
func runMigrations(driver string) {
|
||||
migrations := migrate.FileMigrationSource{
|
||||
Dir: "pkg/datastore/migrations/" + driver, // TODO: Move to bindata
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: packr.NewBox("./migrations/" + driver),
|
||||
}
|
||||
|
||||
migrate.SetTable("migrations")
|
||||
|
||||
n, err := migrate.Exec(dbx.DB, driver, migrations, migrate.Up)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
The MIT License (MIT)
|
||||
Copyright (c) 2016 Mark Bates
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,145 @@
|
|||
# packr
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/gobuffalo/packr?status.svg)](https://godoc.org/github.com/gobuffalo/packr)
|
||||
|
||||
Packr is a simple solution for bundling static assets inside of Go binaries. Most importantly it does it in a way that is friendly to developers while they are developing.
|
||||
|
||||
## Intro Video
|
||||
|
||||
To get an idea of the what and why of packr, please enjoy this short video: [https://vimeo.com/219863271](https://vimeo.com/219863271).
|
||||
|
||||
## Installation
|
||||
|
||||
```text
|
||||
$ go get -u github.com/gobuffalo/packr/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### In Code
|
||||
|
||||
The first step in using Packr is to create a new box. A box represents a folder on disk. Once you have a box you can get `string` or `[]byte` representations of the file.
|
||||
|
||||
```go
|
||||
// set up a new box by giving it a (relative) path to a folder on disk:
|
||||
box := packr.NewBox("./templates")
|
||||
|
||||
// Get the string representation of a file:
|
||||
html := box.String("index.html")
|
||||
|
||||
// Get the string representation of a file, or an error if it doesn't exist:
|
||||
html, err := box.MustString("index.html")
|
||||
|
||||
// Get the []byte representation of a file:
|
||||
html := box.Bytes("index.html")
|
||||
|
||||
// Get the []byte representation of a file, or an error if it doesn't exist:
|
||||
html, err := box.MustBytes("index.html")
|
||||
```
|
||||
|
||||
### What is a Box?
|
||||
|
||||
A box represents a folder, and any sub-folders, on disk that you want to have access to in your binary. When compiling a binary using the `packr` CLI the contents of the folder will be converted into Go files that can be compiled inside of a "standard" go binary. Inside of the compiled binary the files will be read from memory. When working locally the files will be read directly off of disk. This is a seamless switch that doesn't require any special attention on your part.
|
||||
|
||||
#### Example
|
||||
|
||||
Assume the follow directory structure:
|
||||
|
||||
```
|
||||
├── main.go
|
||||
└── templates
|
||||
├── admin
|
||||
│ └── index.html
|
||||
└── index.html
|
||||
```
|
||||
|
||||
The following program will read the `./templates/admin/index.html` file and print it out.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
box := packr.NewBox("./templates")
|
||||
|
||||
s := box.String("admin/index.html")
|
||||
fmt.Println(s)
|
||||
}
|
||||
```
|
||||
|
||||
### Development Made Easy
|
||||
|
||||
In order to get static files into a Go binary, those files must first be converted to Go code. To do that, Packr, ships with a few tools to help build binaries. See below.
|
||||
|
||||
During development, however, it is painful to have to keep running a tool to compile those files.
|
||||
|
||||
Packr uses the following resolution rules when looking for a file:
|
||||
|
||||
1. Look for the file in-memory (inside a Go binary)
|
||||
1. Look for the file on disk (during development)
|
||||
|
||||
Because Packr knows how to fall through to the file system, developers don't need to worry about constantly compiling their static files into a binary. They can work unimpeded.
|
||||
|
||||
Packr takes file resolution a step further. When declaring a new box you use a relative path, `./templates`. When Packr recieves this call it calculates out the absolute path to that directory. By doing this it means you can be guaranteed that Packr can find your files correctly, even if you're not running in the directory that the box was created in. This helps with the problem of testing, where Go changes the `pwd` for each package, making relative paths difficult to work with. This is not a problem when using Packr.
|
||||
|
||||
---
|
||||
|
||||
## Usage with HTTP
|
||||
|
||||
A box implements the [`http.FileSystem`](https://golang.org/pkg/net/http/#FileSystemhttps://golang.org/pkg/net/http/#FileSystem) interface, meaning it can be used to serve static files.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
box := packr.NewBox("./templates")
|
||||
|
||||
http.Handle("/", http.FileServer(box))
|
||||
http.ListenAndServe(":3000", nil)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building a Binary (the easy way)
|
||||
|
||||
When it comes time to build, or install, your Go binary, simply use `packr build` or `packr install` just as you would `go build` or `go install`. All flags for the `go` tool are supported and everything works the way you expect, the only difference is your static assets are now bundled in the generated binary. If you want more control over how this happens, looking at the following section on building binaries (the hard way).
|
||||
|
||||
## Building a Binary (the hard way)
|
||||
|
||||
Before you build your Go binary, run the `packr` command first. It will look for all the boxes in your code and then generate `.go` files that pack the static files into bytes that can be bundled into the Go binary.
|
||||
|
||||
```
|
||||
$ packr
|
||||
```
|
||||
|
||||
Then run your `go build command` like normal.
|
||||
|
||||
*NOTE*: It is not recommended to check-in these generated `-packr.go` files. They can be large, and can easily become out of date if not careful. It is recommended that you always run `packr clean` after running the `packr` tool.
|
||||
|
||||
#### Cleaning Up
|
||||
|
||||
When you're done it is recommended that you run the `packr clean` command. This will remove all of the generated files that Packr created for you.
|
||||
|
||||
```
|
||||
$ packr clean
|
||||
```
|
||||
|
||||
Why do you want to do this? Packr first looks to the information stored in these generated files, if the information isn't there it looks to disk. This makes it easy to work with in development.
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
The `packr` command passes all arguments down to the underlying `go` command, this includes the `-v` flag to print out `go build` information. Packr looks for the `-v` flag, and will turn on its own verbose logging. This is very useful for trying to understand what the `packr` command is doing when it is run.
|
|
@ -0,0 +1,241 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResOutsideBox = errors.New("Can't find a resource outside the box")
|
||||
)
|
||||
|
||||
// NewBox returns a Box that can be used to
|
||||
// retrieve files from either disk or the embedded
|
||||
// binary.
|
||||
func NewBox(path string) Box {
|
||||
var cd string
|
||||
if !filepath.IsAbs(path) {
|
||||
_, filename, _, _ := runtime.Caller(1)
|
||||
cd = filepath.Dir(filename)
|
||||
}
|
||||
|
||||
// this little hack courtesy of the `-cover` flag!!
|
||||
cov := filepath.Join("_test", "_obj_test")
|
||||
cd = strings.Replace(cd, string(filepath.Separator)+cov, "", 1)
|
||||
if !filepath.IsAbs(cd) && cd != "" {
|
||||
cd = filepath.Join(GoPath(), "src", cd)
|
||||
}
|
||||
|
||||
return Box{
|
||||
Path: path,
|
||||
callingDir: cd,
|
||||
data: map[string][]byte{},
|
||||
}
|
||||
}
|
||||
|
||||
// Box represent a folder on a disk you want to
|
||||
// have access to in the built Go binary.
|
||||
type Box struct {
|
||||
Path string
|
||||
callingDir string
|
||||
data map[string][]byte
|
||||
directories map[string]bool
|
||||
}
|
||||
|
||||
func (b Box) AddString(path string, t string) {
|
||||
b.AddBytes(path, []byte(t))
|
||||
}
|
||||
|
||||
func (b Box) AddBytes(path string, t []byte) {
|
||||
b.data[path] = t
|
||||
}
|
||||
|
||||
// String of the file asked for or an empty string.
|
||||
func (b Box) String(name string) string {
|
||||
return string(b.Bytes(name))
|
||||
}
|
||||
|
||||
// MustString returns either the string of the requested
|
||||
// file or an error if it can not be found.
|
||||
func (b Box) MustString(name string) (string, error) {
|
||||
bb, err := b.MustBytes(name)
|
||||
return string(bb), err
|
||||
}
|
||||
|
||||
// Bytes of the file asked for or an empty byte slice.
|
||||
func (b Box) Bytes(name string) []byte {
|
||||
bb, _ := b.MustBytes(name)
|
||||
return bb
|
||||
}
|
||||
|
||||
// MustBytes returns either the byte slice of the requested
|
||||
// file or an error if it can not be found.
|
||||
func (b Box) MustBytes(name string) ([]byte, error) {
|
||||
f, err := b.find(name)
|
||||
if err == nil {
|
||||
bb := &bytes.Buffer{}
|
||||
bb.ReadFrom(f)
|
||||
return bb.Bytes(), err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Has returns true if the resource exists in the box
|
||||
func (b Box) Has(name string) bool {
|
||||
_, err := b.find(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b Box) decompress(bb []byte) []byte {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(bb))
|
||||
if err != nil {
|
||||
return bb
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return bb
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (b Box) find(name string) (File, error) {
|
||||
if bb, ok := b.data[name]; ok {
|
||||
return newVirtualFile(name, bb), nil
|
||||
}
|
||||
if b.directories == nil {
|
||||
b.indexDirectories()
|
||||
}
|
||||
|
||||
cleanName := filepath.ToSlash(filepath.Clean(name))
|
||||
// Ensure name is not outside the box
|
||||
if strings.HasPrefix(cleanName, "../") {
|
||||
return nil, ErrResOutsideBox
|
||||
}
|
||||
// Absolute name is considered as relative to the box root
|
||||
cleanName = strings.TrimPrefix(cleanName, "/")
|
||||
|
||||
// Try to get the resource from the box
|
||||
if _, ok := data[b.Path]; ok {
|
||||
if bb, ok := data[b.Path][cleanName]; ok {
|
||||
bb = b.decompress(bb)
|
||||
return newVirtualFile(cleanName, bb), nil
|
||||
}
|
||||
if filepath.Ext(cleanName) != "" {
|
||||
// The Handler created by http.FileSystem checks for those errors and
|
||||
// returns http.StatusNotFound instead of http.StatusInternalServerError.
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
if _, ok := b.directories[cleanName]; ok {
|
||||
return newVirtualDir(cleanName), nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// Not found in the box virtual fs, try to get it from the file system
|
||||
cleanName = filepath.FromSlash(cleanName)
|
||||
p := filepath.Join(b.callingDir, b.Path, cleanName)
|
||||
return fileFor(p, cleanName)
|
||||
}
|
||||
|
||||
type WalkFunc func(string, File) error
|
||||
|
||||
func (b Box) Walk(wf WalkFunc) error {
|
||||
if data[b.Path] == nil {
|
||||
base, err := filepath.EvalSymlinks(filepath.Join(b.callingDir, b.Path))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
|
||||
cleanName, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
cleanName = strings.TrimPrefix(path, base)
|
||||
}
|
||||
cleanName = filepath.ToSlash(filepath.Clean(cleanName))
|
||||
cleanName = strings.TrimPrefix(cleanName, "/")
|
||||
cleanName = filepath.FromSlash(cleanName)
|
||||
if info == nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := fileFor(path, cleanName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wf(cleanName, file)
|
||||
})
|
||||
}
|
||||
for n := range data[b.Path] {
|
||||
f, err := b.find(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wf(n, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open returns a File using the http.File interface
|
||||
func (b Box) Open(name string) (http.File, error) {
|
||||
return b.find(name)
|
||||
}
|
||||
|
||||
// List shows "What's in the box?"
|
||||
func (b Box) List() []string {
|
||||
var keys []string
|
||||
|
||||
if b.data == nil || len(b.data) == 0 {
|
||||
b.Walk(func(path string, info File) error {
|
||||
finfo, _ := info.FileInfo()
|
||||
if !finfo.IsDir() {
|
||||
keys = append(keys, finfo.Name())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
for k := range b.data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (b *Box) indexDirectories() {
|
||||
b.directories = map[string]bool{}
|
||||
if _, ok := data[b.Path]; ok {
|
||||
for name := range data[b.Path] {
|
||||
prefix, _ := path.Split(name)
|
||||
// Even on Windows the suffix appears to be a /
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
b.directories[prefix] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fileFor(p string, name string) (File, error) {
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return newVirtualDir(p), nil
|
||||
}
|
||||
if bb, err := ioutil.ReadFile(p); err == nil {
|
||||
return newVirtualFile(name, bb), nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GoPath returns the current GOPATH env var
|
||||
// or if it's missing, the default.
|
||||
func GoPath() string {
|
||||
go_path := strings.Split(os.Getenv("GOPATH"), string(os.PathListSeparator))
|
||||
if len(go_path) == 0 || go_path[0] == "" {
|
||||
return build.Default.GOPATH
|
||||
}
|
||||
return go_path[0]
|
||||
}
|
||||
|
||||
// GoBin returns the current GO_BIN env var
|
||||
// or if it's missing, a default of "go"
|
||||
func GoBin() string {
|
||||
go_bin := os.Getenv("GO_BIN")
|
||||
if go_bin == "" {
|
||||
return "go"
|
||||
}
|
||||
return go_bin
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type File interface {
|
||||
io.ReadCloser
|
||||
io.Writer
|
||||
FileInfo() (os.FileInfo, error)
|
||||
Readdir(count int) ([]os.FileInfo, error)
|
||||
Seek(offset int64, whence int) (int64, error)
|
||||
Stat() (os.FileInfo, error)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
Path string
|
||||
Contents []byte
|
||||
size int64
|
||||
modTime time.Time
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (f fileInfo) Name() string {
|
||||
return f.Path
|
||||
}
|
||||
|
||||
func (f fileInfo) Size() int64 {
|
||||
return f.size
|
||||
}
|
||||
|
||||
func (f fileInfo) Mode() os.FileMode {
|
||||
return 0444
|
||||
}
|
||||
|
||||
func (f fileInfo) ModTime() time.Time {
|
||||
return f.modTime
|
||||
}
|
||||
|
||||
func (f fileInfo) IsDir() bool {
|
||||
return f.isDir
|
||||
}
|
||||
|
||||
func (f fileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var gil = &sync.Mutex{}
|
||||
var data = map[string]map[string][]byte{}
|
||||
|
||||
// PackBytes packs bytes for a file into a box.
|
||||
func PackBytes(box string, name string, bb []byte) {
|
||||
gil.Lock()
|
||||
defer gil.Unlock()
|
||||
if _, ok := data[box]; !ok {
|
||||
data[box] = map[string][]byte{}
|
||||
}
|
||||
data[box][name] = bb
|
||||
}
|
||||
|
||||
// PackBytesGzip packets the gzipped compressed bytes into a box.
|
||||
func PackBytesGzip(box string, name string, bb []byte) error {
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
_, err := w.Write(bb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
PackBytes(box, name, buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackJSONBytes packs JSON encoded bytes for a file into a box.
|
||||
func PackJSONBytes(box string, name string, jbb string) error {
|
||||
var bb []byte
|
||||
err := json.Unmarshal([]byte(jbb), &bb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
PackBytes(box, name, bb)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package packr
|
||||
|
||||
import "os"
|
||||
|
||||
var _ File = physicalFile{}
|
||||
|
||||
type physicalFile struct {
|
||||
*os.File
|
||||
}
|
||||
|
||||
func (p physicalFile) FileInfo() (os.FileInfo, error) {
|
||||
return os.Stat(p.Name())
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package packr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var virtualFileModTime = time.Now()
|
||||
var _ File = virtualFile{}
|
||||
|
||||
type virtualFile struct {
|
||||
*bytes.Reader
|
||||
Name string
|
||||
info fileInfo
|
||||
}
|
||||
|
||||
func (f virtualFile) FileInfo() (os.FileInfo, error) {
|
||||
return f.info, nil
|
||||
}
|
||||
|
||||
func (f virtualFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f virtualFile) Write(p []byte) (n int, err error) {
|
||||
return 0, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (f virtualFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return []os.FileInfo{f.info}, nil
|
||||
}
|
||||
|
||||
func (f virtualFile) Stat() (os.FileInfo, error) {
|
||||
return f.info, nil
|
||||
}
|
||||
|
||||
func newVirtualFile(name string, b []byte) File {
|
||||
return virtualFile{
|
||||
Reader: bytes.NewReader(b),
|
||||
Name: name,
|
||||
info: fileInfo{
|
||||
Path: name,
|
||||
Contents: b,
|
||||
size: int64(len(b)),
|
||||
modTime: virtualFileModTime,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newVirtualDir(name string) File {
|
||||
var b []byte
|
||||
v := newVirtualFile(name, b).(virtualFile)
|
||||
v.info.isDir = true
|
||||
return v
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,52 @@
|
|||
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
|
||||
|
||||
Package errors provides simple error handling primitives.
|
||||
|
||||
`go get github.com/pkg/errors`
|
||||
|
||||
The traditional error handling idiom in Go is roughly akin to
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||
|
||||
## Adding context to an error
|
||||
|
||||
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||
```go
|
||||
_, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read failed")
|
||||
}
|
||||
```
|
||||
## Retrieving the cause of an error
|
||||
|
||||
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||
```go
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||
```go
|
||||
switch err := errors.Cause(err).(type) {
|
||||
case *MyError:
|
||||
// handle specifically
|
||||
default:
|
||||
// unknown error
|
||||
}
|
||||
```
|
||||
|
||||
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||
|
||||
Before proposing a change, please discuss your change by raising an issue.
|
||||
|
||||
## License
|
||||
|
||||
BSD-2-Clause
|
|
@ -0,0 +1,32 @@
|
|||
version: build-{build}.{branch}
|
||||
|
||||
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||
shallow_clone: true # for startup speed
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
# http://www.appveyor.com/docs/installed-software
|
||||
install:
|
||||
# some helpful output for debugging builds
|
||||
- go version
|
||||
- go env
|
||||
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||
# but MSYS2 at C:\msys64 has mingw64
|
||||
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||
- gcc --version
|
||||
- g++ --version
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- set PATH=C:\gopath\bin;%PATH%
|
||||
- go test -v ./...
|
||||
|
||||
#artifacts:
|
||||
# - path: '%GOPATH%\bin\*.exe'
|
||||
deploy: off
|
|
@ -0,0 +1,269 @@
|
|||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// and the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||
// functions destructure errors.Wrap into its component operations of annotating
|
||||
// an error with a stack trace and an a message, respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error which does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// causer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface.
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// Where errors.StackTrace is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// stackTracer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is call, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s function name and path of source file relative to the compile time
|
||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (C) 2014-2017 by Ruben Vermeersch <ruben@rocketeer.be>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -21,17 +21,20 @@ Using [modl](https://github.com/jmoiron/modl)? Check out [modl-migrate](https://
|
|||
To install the library and command line program, use the following:
|
||||
|
||||
```bash
|
||||
go get github.com/rubenv/sql-migrate/...
|
||||
go get -v github.com/rubenv/sql-migrate/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As a standalone tool
|
||||
|
||||
```
|
||||
$ sql-migrate --help
|
||||
usage: sql-migrate [--version] [--help] <command> [<args>]
|
||||
|
||||
Available commands are:
|
||||
down Undo a database migration
|
||||
new Create a new migration
|
||||
redo Reapply the last migration
|
||||
status Show migration status
|
||||
up Migrates the database to the most recent version available
|
||||
|
@ -72,6 +75,8 @@ Options:
|
|||
-dryrun Don't apply migrations, just print them.
|
||||
```
|
||||
|
||||
The `new` command creates a new empty migration template using the following pattern `<current time>-<name>.sql`.
|
||||
|
||||
The `up` command applies all available migrations. By contrast, `down` will only apply one migration by default. This behavior can be changed for both by using the `-limit` parameter.
|
||||
|
||||
The `redo` command will unapply the last migration and reapply it. This is useful during development, when you're writing migrations.
|
||||
|
@ -88,7 +93,22 @@ $ sql-migrate status
|
|||
+---------------+-----------------------------------------+
|
||||
```
|
||||
|
||||
### MySQL Caveat
|
||||
|
||||
If you are using MySQL, you must append `?parseTime=true` to the `datasource` configuration. For example:
|
||||
|
||||
```yml
|
||||
production:
|
||||
dialect: mysql
|
||||
datasource: root@/dbname?parseTime=true
|
||||
dir: migrations/mysql
|
||||
table: migrations
|
||||
```
|
||||
|
||||
See [here](https://github.com/go-sql-driver/mysql#parsetime) for more information.
|
||||
|
||||
### As a library
|
||||
|
||||
Import sql-migrate into your application:
|
||||
|
||||
```go
|
||||
|
@ -114,6 +134,11 @@ migrations := &migrate.FileMigrationSource{
|
|||
Dir: "db/migrations",
|
||||
}
|
||||
|
||||
// OR: Use migrations from a packr box
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: packr.NewBox("./migrations"),
|
||||
}
|
||||
|
||||
// OR: Use migrations from bindata:
|
||||
migrations := &migrate.AssetMigrationSource{
|
||||
Asset: Asset,
|
||||
|
@ -157,6 +182,10 @@ DROP TABLE people;
|
|||
|
||||
You can put multiple statements in each block, as long as you end them with a semicolon (`;`).
|
||||
|
||||
You can alternatively set up a separator string that matches an entire line by setting `sqlparse.LineSeparator`. This
|
||||
can be used to imitate, for example, MS SQL Query Analyzer functionality where commands can be separated by a line with
|
||||
contents of `GO`. If `sqlparse.LineSeparator` is matched, it will not be included in the resulting migration scripts.
|
||||
|
||||
If you have complex statements which contain semicolons, use `StatementBegin` and `StatementEnd` to indicate boundaries:
|
||||
|
||||
```sql
|
||||
|
@ -192,8 +221,32 @@ CREATE UNIQUE INDEX people_unique_id_idx CONCURRENTLY ON people (id);
|
|||
DROP INDEX people_unique_id_idx;
|
||||
```
|
||||
|
||||
## Embedding migrations with [bindata](https://github.com/jteeuwen/go-bindata)
|
||||
If you like your Go applications self-contained (that is: a single binary): use [bindata](https://github.com/jteeuwen/go-bindata) to embed the migration files.
|
||||
## Embedding migrations with [packr](https://github.com/gobuffalo/packr)
|
||||
|
||||
If you like your Go applications self-contained (that is: a single binary): use [packr](https://github.com/gobuffalo/packr) to embed the migration files.
|
||||
|
||||
Just write your migration files as usual, as a set of SQL files in a folder.
|
||||
|
||||
Use the `PackrMigrationSource` in your application to find the migrations:
|
||||
|
||||
```go
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: packr.NewBox("./migrations"),
|
||||
}
|
||||
```
|
||||
|
||||
If you already have a box and would like to use a subdirectory:
|
||||
|
||||
```go
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: myBox,
|
||||
Dir: "./migrations",
|
||||
}
|
||||
```
|
||||
|
||||
## Embedding migrations with [bindata](https://github.com/shuLhan/go-bindata)
|
||||
|
||||
As an alternative, but slightly less maintained, you can use [bindata](https://github.com/shuLhan/go-bindata) to embed the migration files.
|
||||
|
||||
Just write your migration files as usual, as a set of SQL files in a folder.
|
||||
|
||||
|
@ -220,6 +273,7 @@ Both `Asset` and `AssetDir` are functions provided by bindata.
|
|||
Then proceed as usual.
|
||||
|
||||
## Extending
|
||||
|
||||
Adding a new migration source means implementing `MigrationSource`.
|
||||
|
||||
```go
|
||||
|
@ -232,24 +286,4 @@ The resulting slice of migrations will be executed in the given order, so it sho
|
|||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2014-2016 by Ruben Vermeersch <ruben@rocketeer.be>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
This library is distributed under the [MIT](LICENSE) license.
|
||||
|
|
|
@ -16,7 +16,7 @@ Installation
|
|||
|
||||
To install the library and command line program, use the following:
|
||||
|
||||
go get github.com/rubenv/sql-migrate/...
|
||||
go get -v github.com/rubenv/sql-migrate/...
|
||||
|
||||
Command-line tool
|
||||
|
||||
|
@ -27,6 +27,7 @@ The main command is called sql-migrate.
|
|||
|
||||
Available commands are:
|
||||
down Undo a database migration
|
||||
new Create a new migration
|
||||
redo Reapply the last migration
|
||||
status Show migration status
|
||||
up Migrates the database to the most recent version available
|
||||
|
@ -76,6 +77,18 @@ Use the status command to see the state of the applied migrations:
|
|||
| 2_record.sql | no |
|
||||
+---------------+-----------------------------------------+
|
||||
|
||||
MySQL Caveat
|
||||
|
||||
If you are using MySQL, you must append ?parseTime=true to the datasource configuration. For example:
|
||||
|
||||
production:
|
||||
dialect: mysql
|
||||
datasource: root@/dbname?parseTime=true
|
||||
dir: migrations/mysql
|
||||
table: migrations
|
||||
|
||||
See https://github.com/go-sql-driver/mysql#parsetime for more information.
|
||||
|
||||
Library
|
||||
|
||||
Import sql-migrate into your application:
|
||||
|
@ -170,9 +183,28 @@ Normally each migration is run within a transaction in order to guarantee that i
|
|||
-- +migrate Down
|
||||
DROP INDEX people_unique_id_idx;
|
||||
|
||||
Embedding migrations with packr
|
||||
|
||||
If you like your Go applications self-contained (that is: a single binary): use packr (https://github.com/gobuffalo/packr) to embed the migration files.
|
||||
|
||||
Just write your migration files as usual, as a set of SQL files in a folder.
|
||||
|
||||
Use the PackrMigrationSource in your application to find the migrations:
|
||||
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: packr.NewBox("./migrations"),
|
||||
}
|
||||
|
||||
If you already have a box and would like to use a subdirectory:
|
||||
|
||||
migrations := &migrate.PackrMigrationSource{
|
||||
Box: myBox,
|
||||
Dir: "./migrations",
|
||||
}
|
||||
|
||||
Embedding migrations with bindata
|
||||
|
||||
If you like your Go applications self-contained (that is: a single binary): use bindata (https://github.com/jteeuwen/go-bindata) to embed the migration files.
|
||||
As an alternative, but slightly less maintained, you can use bindata (https://github.com/shuLhan/go-bindata) to embed the migration files.
|
||||
|
||||
Just write your migration files as usual, as a set of SQL files in a folder.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
@ -144,10 +144,25 @@ type MemoryMigrationSource struct {
|
|||
var _ MigrationSource = (*MemoryMigrationSource)(nil)
|
||||
|
||||
func (m MemoryMigrationSource) FindMigrations() ([]*Migration, error) {
|
||||
// Make sure migrations are sorted
|
||||
sort.Sort(byId(m.Migrations))
|
||||
// Make sure migrations are sorted. In order to make the MemoryMigrationSource safe for
|
||||
// concurrent use we should not mutate it in place. So `FindMigrations` would sort a copy
|
||||
// of the m.Migrations.
|
||||
migrations := make([]*Migration, len(m.Migrations))
|
||||
copy(migrations, m.Migrations)
|
||||
sort.Sort(byId(migrations))
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
return m.Migrations, nil
|
||||
// A set of migrations loaded from an http.FileServer
|
||||
|
||||
type HttpFileSystemMigrationSource struct {
|
||||
FileSystem http.FileSystem
|
||||
}
|
||||
|
||||
var _ MigrationSource = (*HttpFileSystemMigrationSource)(nil)
|
||||
|
||||
func (f HttpFileSystemMigrationSource) FindMigrations() ([]*Migration, error) {
|
||||
return findMigrations(f.FileSystem)
|
||||
}
|
||||
|
||||
// A set of migrations loaded from a directory.
|
||||
|
@ -158,9 +173,14 @@ type FileMigrationSource struct {
|
|||
var _ MigrationSource = (*FileMigrationSource)(nil)
|
||||
|
||||
func (f FileMigrationSource) FindMigrations() ([]*Migration, error) {
|
||||
filesystem := http.Dir(f.Dir)
|
||||
return findMigrations(filesystem)
|
||||
}
|
||||
|
||||
func findMigrations(dir http.FileSystem) ([]*Migration, error) {
|
||||
migrations := make([]*Migration, 0)
|
||||
|
||||
file, err := os.Open(f.Dir)
|
||||
file, err := dir.Open("/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -172,14 +192,14 @@ func (f FileMigrationSource) FindMigrations() ([]*Migration, error) {
|
|||
|
||||
for _, info := range files {
|
||||
if strings.HasSuffix(info.Name(), ".sql") {
|
||||
file, err := os.Open(path.Join(f.Dir, info.Name()))
|
||||
file, err := dir.Open(info.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error while opening %s: %s", info.Name(), err)
|
||||
}
|
||||
|
||||
migration, err := ParseMigration(info.Name(), file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error while parsing %s: %s", info.Name(), err)
|
||||
}
|
||||
|
||||
migrations = append(migrations, migration)
|
||||
|
@ -236,6 +256,60 @@ func (a AssetMigrationSource) FindMigrations() ([]*Migration, error) {
|
|||
return migrations, nil
|
||||
}
|
||||
|
||||
// Avoids pulling in the packr library for everyone, mimicks the bits of
|
||||
// packr.Box that we need.
|
||||
type PackrBox interface {
|
||||
List() []string
|
||||
Bytes(name string) []byte
|
||||
}
|
||||
|
||||
// Migrations from a packr box.
|
||||
type PackrMigrationSource struct {
|
||||
Box PackrBox
|
||||
|
||||
// Path in the box to use.
|
||||
Dir string
|
||||
}
|
||||
|
||||
var _ MigrationSource = (*PackrMigrationSource)(nil)
|
||||
|
||||
func (p PackrMigrationSource) FindMigrations() ([]*Migration, error) {
|
||||
migrations := make([]*Migration, 0)
|
||||
items := p.Box.List()
|
||||
|
||||
prefix := ""
|
||||
dir := path.Clean(p.Dir)
|
||||
if dir != "." {
|
||||
prefix = fmt.Sprintf("%s/", dir)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if !strings.HasPrefix(item, prefix) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimPrefix(item, prefix)
|
||||
if strings.Contains(name, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(name, ".sql") {
|
||||
file := p.Box.Bytes(item)
|
||||
|
||||
migration, err := ParseMigration(name, bytes.NewReader(file))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migrations = append(migrations, migration)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure migrations are sorted
|
||||
sort.Sort(byId(migrations))
|
||||
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
// Migration parsing
|
||||
func ParseMigration(id string, r io.ReadSeeker) (*Migration, error) {
|
||||
m := &Migration{
|
||||
|
@ -244,7 +318,7 @@ func ParseMigration(id string, r io.ReadSeeker) (*Migration, error) {
|
|||
|
||||
parsed, err := sqlparse.ParseMigration(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error parsing migration (%s): %s", id, err)
|
||||
}
|
||||
|
||||
m.Up = parsed.UpStatements
|
||||
|
@ -304,22 +378,31 @@ func ExecMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirecti
|
|||
}
|
||||
}
|
||||
|
||||
if dir == Up {
|
||||
switch dir {
|
||||
case Up:
|
||||
err = executor.Insert(&MigrationRecord{
|
||||
Id: migration.Id,
|
||||
AppliedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
if trans, ok := executor.(*gorp.Transaction); ok {
|
||||
trans.Rollback()
|
||||
}
|
||||
|
||||
return applied, newTxError(migration, err)
|
||||
}
|
||||
} else if dir == Down {
|
||||
case Down:
|
||||
_, err := executor.Delete(&MigrationRecord{
|
||||
Id: migration.Id,
|
||||
})
|
||||
if err != nil {
|
||||
if trans, ok := executor.(*gorp.Transaction); ok {
|
||||
trans.Rollback()
|
||||
}
|
||||
|
||||
return applied, newTxError(migration, err)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
panic("Not possible")
|
||||
}
|
||||
|
||||
|
@ -402,6 +485,55 @@ func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationD
|
|||
return result, dbMap, nil
|
||||
}
|
||||
|
||||
// Skip a set of migrations
|
||||
//
|
||||
// Will skip at most `max` migrations. Pass 0 for no limit.
|
||||
//
|
||||
// Returns the number of skipped migrations.
|
||||
func SkipMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) {
|
||||
migrations, dbMap, err := PlanMigration(db, dialect, m, dir, max)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Skip migrations
|
||||
applied := 0
|
||||
for _, migration := range migrations {
|
||||
var executor SqlExecutor
|
||||
|
||||
if migration.DisableTransaction {
|
||||
executor = dbMap
|
||||
} else {
|
||||
executor, err = dbMap.Begin()
|
||||
if err != nil {
|
||||
return applied, newTxError(migration, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = executor.Insert(&MigrationRecord{
|
||||
Id: migration.Id,
|
||||
AppliedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
if trans, ok := executor.(*gorp.Transaction); ok {
|
||||
trans.Rollback()
|
||||
}
|
||||
|
||||
return applied, newTxError(migration, err)
|
||||
}
|
||||
|
||||
if trans, ok := executor.(*gorp.Transaction); ok {
|
||||
if err := trans.Commit(); err != nil {
|
||||
return applied, newTxError(migration, err)
|
||||
}
|
||||
}
|
||||
|
||||
applied++
|
||||
}
|
||||
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// Filter a slice of migrations into ones that should be applied.
|
||||
func ToApply(migrations []*Migration, current string, direction MigrationDirection) []*Migration {
|
||||
var index = -1
|
||||
|
@ -482,7 +614,8 @@ func getMigrationDbMap(db *sql.DB, dialect string) (*gorp.DbMap, error) {
|
|||
var out *time.Time
|
||||
err := db.QueryRow("SELECT NOW()").Scan(&out)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time" {
|
||||
if err.Error() == "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time" ||
|
||||
err.Error() == "sql: Scan error on column index 0: unsupported Scan, storing driver.Value type []uint8 into type *time.Time" {
|
||||
return nil, errors.New(`Cannot parse dates.
|
||||
|
||||
Make sure that the parseTime option is supplied to your database connection.
|
||||
|
|
|
@ -32,6 +32,12 @@
|
|||
"revision": "2e00b5cd70399450106cec6431c2e2ce3cae5034",
|
||||
"revisionTime": "2016-12-24T12:10:19Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "8h+jn5DM8qpamdOYYWCCInJZzyM=",
|
||||
"path": "github.com/gobuffalo/packr",
|
||||
"revision": "7f4074995d431987caaa35088199f13c44b24440",
|
||||
"revisionTime": "2018-04-13T17:35:21Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=",
|
||||
"path": "github.com/gorilla/context",
|
||||
|
@ -111,10 +117,16 @@
|
|||
"revisionTime": "2016-12-08T17:59:04Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Qgwcmaxcms96cseiKeTu73RiKJw=",
|
||||
"checksumSHA1": "ljd3FhYRJ91cLZz3wsH9BQQ2JbA=",
|
||||
"path": "github.com/pkg/errors",
|
||||
"revision": "816c9085562cd7ee03e7f8188a1cfd942858cded",
|
||||
"revisionTime": "2018-03-11T21:45:15Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6nYX6dKRoU5uicZ5wa9nD7dq22Q=",
|
||||
"path": "github.com/rubenv/sql-migrate",
|
||||
"revision": "1ed79968dfca5de79adb13c84523caaa4fc865a9",
|
||||
"revisionTime": "2017-01-04T09:54:22Z"
|
||||
"revision": "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906",
|
||||
"revisionTime": "2018-02-17T20:35:53Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "IQ6NtaJ+F1rtAMIaCetzKb5nrxQ=",
|
||||
|
|
Loading…
Reference in New Issue