diff --git a/.gitignore b/.gitignore index daf913b..775c25d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,5 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so +*/Godeps/* +!*/Godeps/Godeps.json -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof +coverage.out +dump.rdb \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f0b61b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: go +sudo: false + +go: + - 1.5.4 + - 1.6.4 + - 1.7.4 + - tip + +script: + - go test -v -covermode=atomic -coverprofile=coverage.out + +after_success: + - bash <(curl -s https://codecov.io/bash) + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/acc2c57482e94b44f557 + on_success: change + on_failure: always + on_start: false diff --git a/README.md b/README.md index 7a4a61e..fd28577 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# static +# static middleware + +[![Build Status](https://travis-ci.org/gin-contrib/static.svg)](https://travis-ci.org/gin-contrib/static) +[![codecov](https://codecov.io/gh/gin-contrib/static/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/static) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/static)](https://goreportcard.com/report/github.com/gin-contrib/static) +[![GoDoc](https://godoc.org/github.com/gin-contrib/static?status.svg)](https://godoc.org/github.com/gin-contrib/static) + Static middleware + +## Usage + +### Start using it + +Download and install it: + +```sh +$ go get github.com/gin-contrib/static +``` + +Import it in your code: + +```go +import "github.com/gin-contrib/static" +``` diff --git a/example/bindata/bindata.go b/example/bindata/bindata.go new file mode 100644 index 0000000..f3b3e2d --- /dev/null +++ b/example/bindata/bindata.go @@ -0,0 +1,121 @@ +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "os" + "strings" +) + +func bindata_read(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) + gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + return buf.Bytes(), nil +} + +func data_index_html() ([]byte, error) { + return bindata_read([]byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x44, 0xce, + 0xbd, 0xae, 0xc2, 0x30, 0x0c, 0x05, 0xe0, 0xbd, 0x4f, 0xe1, 0x9b, 0xbd, + 0xaa, 0xba, 0xdd, 0x21, 0xcd, 0xc2, 0xef, 0x06, 0x43, 0x19, 0x18, 0x5d, + 0x62, 0x35, 0x41, 0x4e, 0x22, 0x15, 0x4b, 0x88, 0xb7, 0x27, 0x21, 0x45, + 0x4c, 0x39, 0xb1, 0xf5, 0x1d, 0x59, 0xff, 0x6d, 0x4f, 0x9b, 0xf1, 0x7a, + 0xde, 0x81, 0x93, 0xc0, 0xa6, 0xd1, 0xe5, 0x01, 0xc6, 0x38, 0x0f, 0xea, + 0x8e, 0xca, 0x34, 0x00, 0xda, 0x11, 0xda, 0x12, 0x72, 0x0c, 0x24, 0x08, + 0x37, 0x87, 0xcb, 0x83, 0x64, 0x50, 0x97, 0x71, 0xdf, 0xfe, 0x2b, 0xe8, + 0xd6, 0xa5, 0x78, 0x61, 0x32, 0x73, 0x6a, 0x27, 0x1f, 0x2d, 0x0a, 0xea, + 0xae, 0x4e, 0x4a, 0x47, 0xf7, 0x2d, 0xd1, 0x53, 0xb2, 0xaf, 0x15, 0xb8, + 0xde, 0x1c, 0x89, 0x39, 0xc1, 0xc1, 0x47, 0xf8, 0x39, 0x08, 0xde, 0x5a, + 0xa6, 0x27, 0x2e, 0x94, 0x5d, 0x5f, 0x7d, 0x65, 0xf9, 0xff, 0x39, 0xf3, + 0x1d, 0x00, 0x00, 0xff, 0xff, 0x51, 0x69, 0x85, 0x27, 0xb7, 0x00, 0x00, + 0x00, + }, + "data/index.html", + ) +} + +// 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 { + return f() + } + return nil, fmt.Errorf("Asset %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() ([]byte, error){ + "data/index.html": data_index_html, +} + +// 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 +func AssetDir(name string) ([]string, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + node := _bintree + 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 name := range node.Children { + rv = append(rv, name) + } + return rv, nil +} + +type _bintree_t struct { + Func func() ([]byte, error) + Children map[string]*_bintree_t +} + +var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ + "data": &_bintree_t{nil, map[string]*_bintree_t{ + "index.html": &_bintree_t{data_index_html, map[string]*_bintree_t{}}, + }}, +}} + +// AssetInfo returns file info of given path +func AssetInfo(path string) (os.FileInfo, error) { + return os.Stat(path) +} diff --git a/example/bindata/data/index.html b/example/bindata/data/index.html new file mode 100644 index 0000000..e22796e --- /dev/null +++ b/example/bindata/data/index.html @@ -0,0 +1,10 @@ + + + + + go-bindata + + +

Hello Gin go-bindata middleware

+ + diff --git a/example/bindata/example.go b/example/bindata/example.go new file mode 100644 index 0000000..c0beceb --- /dev/null +++ b/example/bindata/example.go @@ -0,0 +1,51 @@ +package main + +import ( + "net/http" + "strings" + + assetfs "github.com/elazarl/go-bindata-assetfs" + "github.com/gin-gonic/contrib/static" + "github.com/gin-gonic/gin" +) + +type binaryFileSystem struct { + fs http.FileSystem +} + +func (b *binaryFileSystem) Open(name string) (http.File, error) { + return b.fs.Open(name) +} + +func (b *binaryFileSystem) Exists(prefix string, filepath string) bool { + + if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { + if _, err := b.fs.Open(p); err != nil { + return false + } + return true + } + return false +} + +func BinaryFileSystem(root string) *binaryFileSystem { + fs := &assetfs.AssetFS{Asset, AssetDir, AssetInfo, root} + return &binaryFileSystem{ + fs, + } +} + +// Usage +// $ go-bindata data/ +// $ go build && ./bindata +// +func main() { + r := gin.Default() + + r.Use(static.Serve("/static", BinaryFileSystem("data"))) + r.GET("/ping", func(c *gin.Context) { + c.String(200, "test") + }) + // Listen and Server in 0.0.0.0:8080 + r.Run(":8080") +} diff --git a/example/simple/example.go b/example/simple/example.go new file mode 100644 index 0000000..ef5e895 --- /dev/null +++ b/example/simple/example.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/gin-gonic/contrib/static" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // if Allow DirectoryIndex + //r.Use(static.Serve("/", static.LocalFile("/tmp", true))) + // set prefix + //r.Use(static.Serve("/static", static.LocalFile("/tmp", true))) + + r.Use(static.Serve("/", static.LocalFile("/tmp", false))) + r.GET("/ping", func(c *gin.Context) { + c.String(200, "test") + }) + // Listen and Server in 0.0.0.0:8080 + r.Run(":8080") +} diff --git a/static.go b/static.go new file mode 100644 index 0000000..6e4050e --- /dev/null +++ b/static.go @@ -0,0 +1,62 @@ +package static + +import ( + "net/http" + "os" + "path" + "strings" + + "github.com/gin-gonic/gin" +) + +type ServeFileSystem interface { + http.FileSystem + Exists(prefix string, path string) bool +} + +type localFileSystem struct { + http.FileSystem + root string + indexes bool +} + +func LocalFile(root string, indexes bool) *localFileSystem { + return &localFileSystem{ + FileSystem: gin.Dir(root, indexes), + root: root, + indexes: indexes, + } +} + +func (l *localFileSystem) Exists(prefix string, filepath string) bool { + if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { + name := path.Join(l.root, p) + stats, err := os.Stat(name) + if err != nil { + return false + } + if !l.indexes && stats.IsDir() { + return false + } + return true + } + return false +} + +func ServeRoot(urlPrefix, root string) gin.HandlerFunc { + return Serve(urlPrefix, LocalFile(root, false)) +} + +// Static returns a middleware handler that serves static files in the given directory. +func Serve(urlPrefix string, fs ServeFileSystem) gin.HandlerFunc { + fileserver := http.FileServer(fs) + if urlPrefix != "" { + fileserver = http.StripPrefix(urlPrefix, fileserver) + } + return func(c *gin.Context) { + if fs.Exists(urlPrefix, c.Request.URL.Path) { + fileserver.ServeHTTP(c.Writer, c.Request) + c.Abort() + } + } +} diff --git a/static_test.go b/static_test.go new file mode 100644 index 0000000..f66176b --- /dev/null +++ b/static_test.go @@ -0,0 +1,86 @@ +package static + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { + req, _ := http.NewRequest(method, path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func TestEmptyDirectory(t *testing.T) { + // SETUP file + testRoot, _ := os.Getwd() + f, err := ioutil.TempFile(testRoot, "") + if err != nil { + t.Error(err) + } + defer os.Remove(f.Name()) + f.WriteString("Gin Web Framework") + f.Close() + + dir, filename := filepath.Split(f.Name()) + + router := gin.New() + router.Use(ServeRoot("/", dir)) + router.GET("/", func(c *gin.Context) { + c.String(200, "index") + }) + router.GET("/a", func(c *gin.Context) { + c.String(200, "a") + }) + router.GET("/"+filename, func(c *gin.Context) { + c.String(200, "this is not printed") + }) + w := performRequest(router, "GET", "/") + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "index") + + w = performRequest(router, "GET", "/"+filename) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "Gin Web Framework") + + w = performRequest(router, "GET", "/"+filename+"a") + assert.Equal(t, w.Code, 404) + + w = performRequest(router, "GET", "/a") + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "a") + + router2 := gin.New() + router2.Use(ServeRoot("/static", dir)) + router2.GET("/"+filename, func(c *gin.Context) { + c.String(200, "this is printed") + }) + + w = performRequest(router2, "GET", "/") + assert.Equal(t, w.Code, 404) + + w = performRequest(router2, "GET", "/static") + assert.Equal(t, w.Code, 404) + router2.GET("/static", func(c *gin.Context) { + c.String(200, "index") + }) + + w = performRequest(router2, "GET", "/static") + assert.Equal(t, w.Code, 200) + + w = performRequest(router2, "GET", "/"+filename) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "this is printed") + + w = performRequest(router2, "GET", "/static/"+filename) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "Gin Web Framework") +}