├── static ├── test-files │ ├── .gitignore │ ├── symlinkedfile.txt │ ├── teststart │ │ ├── plainfile.txt │ │ ├── symlinkeddir │ │ └── symlinkedfile.txt │ ├── symlinkeddir │ │ ├── realdir │ │ │ ├── realdirfile.txt │ │ │ └── doublesymlinkeddir │ │ └── symlinkeddirfile.txt │ ├── triplesymlinkeddir │ │ └── triplefile.txt │ └── doublesymlinkeddir │ │ ├── doublesymlinkedfile.txt │ │ └── triplesymlinkeddir ├── misc.go ├── dir.go ├── doc.go ├── file.go ├── static.go └── static_test.go ├── .gitignore ├── LICENSE ├── strings.go ├── README.md ├── main_test.go └── main.go /static/test-files/.gitignore: -------------------------------------------------------------------------------- 1 | test.go 2 | -------------------------------------------------------------------------------- /static/test-files/symlinkedfile.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /static/test-files/teststart/plainfile.txt: -------------------------------------------------------------------------------- 1 | palindata 2 | -------------------------------------------------------------------------------- /static/test-files/teststart/symlinkeddir: -------------------------------------------------------------------------------- 1 | ../symlinkeddir/ -------------------------------------------------------------------------------- /static/test-files/symlinkeddir/realdir/realdirfile.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /static/test-files/symlinkeddir/symlinkeddirfile.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /static/test-files/triplesymlinkeddir/triplefile.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /static/test-files/teststart/symlinkedfile.txt: -------------------------------------------------------------------------------- 1 | ../symlinkedfile.txt -------------------------------------------------------------------------------- /static/test-files/doublesymlinkeddir/doublesymlinkedfile.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /static/test-files/doublesymlinkeddir/triplesymlinkeddir: -------------------------------------------------------------------------------- 1 | ../triplesymlinkeddir/ -------------------------------------------------------------------------------- /static/test-files/symlinkeddir/realdir/doublesymlinkeddir: -------------------------------------------------------------------------------- 1 | ../../doublesymlinkeddir/ -------------------------------------------------------------------------------- /static/misc.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import "os" 4 | 5 | // byName implements sort.Interface. 6 | type byName []os.FileInfo 7 | 8 | func (f byName) Len() int { return len(f) } 9 | func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() } 10 | func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Custom Ignores 27 | run -------------------------------------------------------------------------------- /static/dir.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // dir implements the FileSystem interface 11 | type dir struct { 12 | useStaticFiles bool 13 | fallbackToDisk bool 14 | absPkgPath string 15 | files map[string]*file 16 | } 17 | 18 | // Open returns the FileSystem DIR 19 | func (d dir) Open(name string) (http.File, error) { 20 | 21 | if d.useStaticFiles { 22 | f, found := d.files[name] 23 | 24 | if found { 25 | return f.File() 26 | } 27 | 28 | if !d.fallbackToDisk { 29 | return nil, os.ErrNotExist 30 | } 31 | } 32 | 33 | if !strings.HasPrefix(name, d.absPkgPath) { 34 | name = filepath.FromSlash(d.absPkgPath + name) 35 | } else { 36 | name = filepath.FromSlash(name) 37 | } 38 | 39 | return os.Open(name) 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | functionComments = "// NewStatic%s initializes a new static.Files instance for use" 5 | initStartFile = `//go:generate statics -i=%s -o=%s -pkg=%s -group=%s %s %s 6 | 7 | package %s 8 | 9 | import "github.com/go-playground/statics/static" 10 | 11 | // newStatic%s initializes a new *static.Files instance for use 12 | func newStatic%s(config *static.Config) (*static.Files, error) { 13 | 14 | return static.New(config, &static.DirFile{ 15 | ` 16 | initEndfile = `}) 17 | }` 18 | startFile = `//go:generate statics -i=%s -o=%s -pkg=%s -group=%s %s %s 19 | 20 | package %s 21 | 22 | import ( 23 | "os" 24 | 25 | "github.com/go-playground/statics/static" 26 | ) 27 | 28 | // newStatic%s initializes a new *static.Files instance for use 29 | func newStatic%s(config *static.Config) (*static.Files, error) { 30 | 31 | return static.New(config, ` 32 | endfile = `) 33 | }` 34 | 35 | dirFileEnd = `}, 36 | }` 37 | 38 | dirFileEndArray = `}, 39 | }, 40 | ` 41 | ) 42 | 43 | var ( 44 | dirFileStart = `&static.DirFile{ 45 | Path: %q, 46 | Name: "%s", 47 | Size: %d, 48 | Mode: os.FileMode(%d), 49 | ModTime: %v, 50 | IsDir: %t, 51 | Compressed: ` + "`\n%s`" + `, 52 | Files: []*static.DirFile{` 53 | ) 54 | -------------------------------------------------------------------------------- /static/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package static reads package statics generated go files and provides helper methods and objects to retrieve the embeded files and even serve then via http.FileSystem. 3 | 4 | Embedding in Source Control 5 | 6 | statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=\\.gitignore -init=true 7 | 8 | Output: 9 | 10 | //go:generate statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=\.gitignore 11 | 12 | package main 13 | 14 | import "github.com/go-playground/statics/static" 15 | 16 | // newStaticAssets initializes a new *static.Files instance for use 17 | func newStaticAssets(config *static.Config) (*static.Files, error) { 18 | 19 | return static.New(config, &static.DirFile{}) 20 | } 21 | 22 | when using arg init=true statics package generates a minimal configuration with no 23 | files embeded; you can then add it to source control, ignore the file locally using 24 | git update-index --assume-unchanged [filename(s)] and then when ready for generation 25 | just run go generate from the project root and the files will get embedded ready for 26 | compilation. 27 | 28 | Be sure to check out this packages best buddy https://github.com/go-playground/generate 29 | to help get everything generated and ready for compilation. 30 | 31 | 32 | NOTE: when specifying paths or directory name in code always use "/", even for you windows users, 33 | the package handles any conversion to you local filesystem paths; Except for the AbsPkgPath 34 | variable in the config. 35 | 36 | run statics -h to see the options/arguments 37 | 38 | Example Usages 39 | 40 | // generated via command: 41 | // statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=\\.gitignore 42 | 43 | gopath := getGopath() // retrieved from environment variable 44 | pkgPath := "/src/github.com/username/project" 45 | 46 | // get absolute directory path of the -i arguments parent directory + any prefix 47 | // removed, used when UseStaticFiles=false this is so even when referencing this 48 | // package from another project and your PWD is not for this package anymore the 49 | // file paths will still work. 50 | pkg := goapth + pkgPath 51 | 52 | config := &static.Config{ 53 | UseStaticFiles: true, 54 | AbsPkgPath: pkg, 55 | } 56 | 57 | assets, err := newStaticAssets(config) 58 | if err != nil { 59 | log.Println(err) 60 | } 61 | 62 | // when using http 63 | http.Handle("/assets", http.FileServer(assets.FS())) 64 | 65 | // other methods for direct access 66 | assets.GetHTTPFile 67 | assets.ReadFile 68 | assets.ReadDir 69 | assets.ReadFiles 70 | */ 71 | package static 72 | -------------------------------------------------------------------------------- /static/file.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "os" 9 | "time" 10 | ) 11 | 12 | type httpFile struct { 13 | *bytes.Reader 14 | *file 15 | } 16 | 17 | // File contains the static FileInfo 18 | type file struct { 19 | data []byte 20 | path string 21 | name string 22 | size int64 23 | mode os.FileMode 24 | modTime int64 25 | isDir bool 26 | files []*file 27 | lastDirIndex int 28 | } 29 | 30 | // File returns an http.File or error 31 | func (f *file) File() (http.File, error) { 32 | 33 | // if production read filesystem file 34 | return &httpFile{ 35 | bytes.NewReader(f.data), 36 | f, 37 | }, nil 38 | } 39 | 40 | // Close closes the File, rendering it unusable for I/O. It returns an error, if any. 41 | func (f *file) Close() error { 42 | return nil 43 | } 44 | 45 | // Readdir returns nil fileinfo and an error because the static FileSystem does not store directories 46 | func (f *file) Readdir(count int) ([]os.FileInfo, error) { 47 | 48 | if !f.IsDir() { 49 | return nil, errors.New("not a directory") 50 | } 51 | 52 | var files []os.FileInfo 53 | 54 | if count <= 0 { 55 | files = make([]os.FileInfo, len(f.files)) 56 | count = len(f.files) 57 | f.lastDirIndex = 0 58 | } else { 59 | files = make([]os.FileInfo, count) 60 | count += f.lastDirIndex 61 | } 62 | 63 | if f.lastDirIndex >= len(f.files) { 64 | f.lastDirIndex = 0 65 | return nil, io.EOF 66 | } 67 | 68 | if count+f.lastDirIndex >= len(f.files) { 69 | count = len(f.files) 70 | } 71 | 72 | i := f.lastDirIndex 73 | 74 | var j int 75 | 76 | for i = f.lastDirIndex; i < count; i++ { 77 | files[j] = f.files[i] 78 | j++ 79 | } 80 | 81 | if count > 0 { 82 | f.lastDirIndex += j 83 | } 84 | 85 | return files[:j], nil 86 | } 87 | 88 | // Stat returns the FileInfo structure describing file. If there is an error, it will be of type *PathError. 89 | func (f *file) Stat() (os.FileInfo, error) { 90 | return f, nil 91 | } 92 | 93 | // Name returns the name of the file as presented to Open. 94 | func (f *file) Name() string { 95 | return f.name 96 | } 97 | 98 | // Size length in bytes for regular files; system-dependent for others 99 | func (f *file) Size() int64 { 100 | return f.size 101 | } 102 | 103 | // Mode returns file mode bits 104 | func (f *file) Mode() os.FileMode { 105 | return os.FileMode(f.mode) 106 | } 107 | 108 | // ModTime returns the files modification time 109 | func (f *file) ModTime() time.Time { 110 | return time.Unix(f.modTime, 0) 111 | } 112 | 113 | // IsDir reports whether f describes a directory. 114 | func (f *file) IsDir() bool { 115 | return f.isDir 116 | } 117 | 118 | // Sys returns the underlying data source (can return nil) 119 | func (f *file) Sys() interface{} { 120 | return f 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package statics 2 | =============== 3 | 4 | ![Project status](https://img.shields.io/badge/version-1.7.0-green.svg) 5 | [![Build Status](https://semaphoreci.com/api/v1/projects/1b97afa9-77f3-43ff-ad26-749958500745/601363/badge.svg)](https://semaphoreci.com/joeybloggs/statics) 6 | [![Go Report Card](http://goreportcard.com/badge/go-playground/statics)](http://goreportcard.com/report/go-playground/statics) 7 | [![GoDoc](https://godoc.org/github.com/go-playground/statics/static?status.svg)](https://godoc.org/github.com/go-playground/statics/static) 8 | ![License](https://img.shields.io/dub/l/vibe-d.svg) 9 | 10 | Package statics embeds static files into your go applications. It provides helper methods and objects to retrieve embeded files and serve via http. 11 | 12 | It has the following **unique** features: 13 | 14 | - ```Follows Symlinks!``` uses the symlinked file/directory name and path but the contents and other fileinfo of the original file. 15 | - Embeds static generation command for using ```go:generate``` in each static file for easy generation in production mode. 16 | - Handles multiple static go files, even inside of the same package. 17 | - Handles development (aka local files) vs production (aka static files) across packages. 18 | 19 | Installation 20 | ------------ 21 | 22 | ```shell 23 | go get -u github.com/go-playground/statics 24 | ``` 25 | 26 | Then import the statics package into your own code. 27 | 28 | import "github.com/go-playground/statics" 29 | 30 | Usage and documentation 31 | ------ 32 | 33 | Please see https://godoc.org/github.com/go-playground/statics/static for detailed usage docs. 34 | 35 | NOTE: when specifying path or directory name in code always use "/", even for you windows users, 36 | the package handles any conversion to you local filesystem paths; Except for the AbsPkgPath 37 | variable in the config. 38 | 39 | run statics -h to see the options/arguments 40 | 41 | ##### Examples: 42 | 43 | Embedding in Source Control 44 | 45 | statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=\\\\.gitignore -init=true 46 | 47 | Output: 48 | ```go 49 | //go:generate statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=\.gitignore 50 | 51 | package main 52 | 53 | import "github.com/go-playground/statics/static" 54 | 55 | // newStaticAssets initializes a new *static.Files instance for use 56 | func newStaticAssets(config *static.Config) (*static.Files, error) { 57 | 58 | return static.New(config, &static.DirFile{}) 59 | } 60 | ``` 61 | 62 | when using arg init=true statics package generates a minimal configuration with no 63 | files embeded; you can then add it to source control, ignore the file locally using 64 | git update-index --assume-unchanged [filename(s)] and then when ready for generation 65 | just run go generate from the project root and the files will get embedded ready for 66 | compilation. 67 | 68 | Be sure to check out this packages best buddy https://github.com/go-playground/generate 69 | to help get everything generated and ready for compilation. 70 | 71 | Example Usage 72 | ```go 73 | // generated via command: 74 | // statics -i=assets -o=assets.go -pkg=main -group=Assets -ignore=//.gitignore 75 | 76 | gopath := getGopath() // retrieved from environment variable 77 | pkgPath := "/src/github.com/username/project" 78 | 79 | // get absolute directory path of the -i arguments parent directory + any prefix 80 | // removed, used when UseStaticFiles=false this is so even when referencing this 81 | // package from another project and your PWD is not for this package anymore the 82 | // file paths will still work. 83 | pkg := goapth + pkgPath 84 | 85 | config := &static.Config{ 86 | UseStaticFiles: true, 87 | AbsPkgPath: pkg, 88 | } 89 | 90 | // NOTE: Assets in the function name below is the group in the generation command 91 | assets, err := newStaticAssets(config) 92 | if err != nil { 93 | log.Println(err) 94 | } 95 | 96 | // when using http 97 | http.Handle("/assets", http.FileServer(assets.FS())) 98 | 99 | // other methods for direct access 100 | assets.GetHTTPFile 101 | assets.ReadFile 102 | assets.ReadDir 103 | assets.ReadFiles 104 | ``` 105 | 106 | Package Versioning 107 | ---------- 108 | I'm jumping on the vendoring bandwagon, you should vendor this package as I will not 109 | be creating different version with gopkg.in like allot of my other libraries. 110 | 111 | Why? because my time is spread pretty thin maintaining all of the libraries I have + LIFE, 112 | it is so freeing not to worry about it and will help me keep pouring out bigger and better 113 | things for you the community. 114 | 115 | License 116 | ------ 117 | Distributed under MIT License, please see license file in code for more details. 118 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | . "gopkg.in/go-playground/assert.v1" 9 | ) 10 | 11 | // NOTES: 12 | // - Run "go test" to run tests 13 | // - Run "gocov test | gocov report" to report on test converage by file 14 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 15 | // 16 | // or 17 | // 18 | // -- may be a good idea to change to output path to somewherelike /tmp 19 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 20 | // 21 | 22 | func TestMain(m *testing.M) { 23 | 24 | // setup 25 | 26 | os.Exit(m.Run()) 27 | 28 | // teardown 29 | } 30 | 31 | func TestNonExistantStaticDir(t *testing.T) { 32 | 33 | i := "static/test-files/garbagedir" 34 | flagStaticDir = &i 35 | 36 | o := "static/test-files/test.go" 37 | flagOuputFile = &o 38 | 39 | p := "test" 40 | flagPkg = &p 41 | 42 | g := "Assets" 43 | flagGroup = &g 44 | 45 | ignore := "" 46 | flagIgnore = &ignore 47 | 48 | prefix := "" 49 | flagPrefix = &prefix 50 | 51 | init := false 52 | flagInit = &init 53 | 54 | PanicMatches(t, func() { main() }, "stat static/test-files/garbagedir: no such file or directory") 55 | } 56 | 57 | func TestBadPackage(t *testing.T) { 58 | 59 | i := "static/test-files/test-inner" 60 | flagStaticDir = &i 61 | 62 | o := "static/test-files/test.go" 63 | flagOuputFile = &o 64 | 65 | p := "" 66 | flagPkg = &p 67 | 68 | g := "Assets" 69 | flagGroup = &g 70 | 71 | init := false 72 | flagInit = &init 73 | 74 | PanicMatches(t, func() { main() }, "**invalid Package Name") 75 | } 76 | 77 | func TestBadOutputDir(t *testing.T) { 78 | 79 | i := "static/test-files/test-inner" 80 | flagStaticDir = &i 81 | 82 | o := "" 83 | flagOuputFile = &o 84 | 85 | p := "test" 86 | flagPkg = &p 87 | 88 | g := "Assets" 89 | flagGroup = &g 90 | 91 | init := false 92 | flagInit = &init 93 | 94 | PanicMatches(t, func() { main() }, "**invalid Output Directory") 95 | } 96 | 97 | func TestBadStaticDir(t *testing.T) { 98 | 99 | i := "" 100 | flagStaticDir = &i 101 | 102 | o := "static/test-files/test.go" 103 | flagOuputFile = &o 104 | 105 | p := "test" 106 | flagPkg = &p 107 | 108 | g := "Assets" 109 | flagGroup = &g 110 | 111 | init := false 112 | flagInit = &init 113 | 114 | PanicMatches(t, func() { main() }, "**invalid Static File Directoy '.'") 115 | } 116 | 117 | func TestGenerateInitFile(t *testing.T) { 118 | 119 | i := "static/test-files/teststart" 120 | flagStaticDir = &i 121 | 122 | o := "static/test-files/test.go" 123 | flagOuputFile = &o 124 | 125 | p := "test" 126 | flagPkg = &p 127 | 128 | g := "Assets" 129 | flagGroup = &g 130 | 131 | init := true 132 | flagInit = &init 133 | 134 | main() 135 | 136 | b, err := ioutil.ReadFile("static/test-files/test.go") 137 | Equal(t, err, nil) 138 | 139 | expected := `//go:generate statics -i=static/test-files/teststart -o=static/test-files/test.go -pkg=test -group=Assets 140 | 141 | package test 142 | 143 | import "github.com/go-playground/statics/static" 144 | 145 | // newStaticAssets initializes a new *static.Files instance for use 146 | func newStaticAssets(config *static.Config) (*static.Files, error) { 147 | 148 | return static.New(config, &static.DirFile{}) 149 | } 150 | ` 151 | 152 | Equal(t, string(b), expected) 153 | } 154 | 155 | func TestIgnore(t *testing.T) { 156 | 157 | Equal(t, true, true) 158 | 159 | i := "static/test-files/teststart" 160 | flagStaticDir = &i 161 | 162 | o := "static/test-files/test.go" 163 | flagOuputFile = &o 164 | 165 | p := "test" 166 | flagPkg = &p 167 | 168 | g := "Assets" 169 | flagGroup = &g 170 | 171 | ignore := ".*.txt" 172 | flagIgnore = &ignore 173 | 174 | prefix := "" 175 | flagPrefix = &prefix 176 | 177 | init := false 178 | flagInit = &init 179 | 180 | main() 181 | } 182 | 183 | func TestBadIgnore(t *testing.T) { 184 | 185 | Equal(t, true, true) 186 | 187 | i := "static/test-files/teststart" 188 | flagStaticDir = &i 189 | 190 | o := "static/test-files/test.go" 191 | flagOuputFile = &o 192 | 193 | p := "test" 194 | flagPkg = &p 195 | 196 | g := "Assets" 197 | flagGroup = &g 198 | 199 | ignore := "([12.gitignore" 200 | flagIgnore = &ignore 201 | 202 | prefix := "" 203 | flagPrefix = &prefix 204 | 205 | init := false 206 | flagInit = &init 207 | 208 | PanicMatches(t, func() { main() }, "**Error Compiling Regex:error parsing regexp: missing closing ]: `[12.gitignore`") 209 | } 210 | 211 | func TestGenerateFilePrefix(t *testing.T) { 212 | 213 | Equal(t, true, true) 214 | 215 | i := "static/test-files/teststart" 216 | flagStaticDir = &i 217 | 218 | o := "static/test-files/test.go" 219 | flagOuputFile = &o 220 | 221 | p := "test" 222 | flagPkg = &p 223 | 224 | g := "Assets" 225 | flagGroup = &g 226 | 227 | ignore := "" 228 | flagIgnore = &ignore 229 | 230 | prefix := "static/" 231 | flagPrefix = &prefix 232 | 233 | init := false 234 | flagInit = &init 235 | 236 | main() 237 | } 238 | 239 | func TestGenerateFile(t *testing.T) { 240 | 241 | Equal(t, true, true) 242 | 243 | i := "static/test-files/teststart" 244 | flagStaticDir = &i 245 | 246 | o := "static/test-files/test.go" 247 | flagOuputFile = &o 248 | 249 | p := "test" 250 | flagPkg = &p 251 | 252 | g := "Assets" 253 | flagGroup = &g 254 | 255 | ignore := "" 256 | flagIgnore = &ignore 257 | 258 | prefix := "" 259 | flagPrefix = &prefix 260 | 261 | init := false 262 | flagInit = &init 263 | 264 | main() 265 | } 266 | -------------------------------------------------------------------------------- /static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "errors" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | pathSep = "/" 19 | ) 20 | 21 | // DirFile contains the static directory and file content info 22 | type DirFile struct { 23 | Path string 24 | Name string 25 | Size int64 26 | Mode os.FileMode 27 | ModTime int64 28 | IsDir bool 29 | Compressed string 30 | Files []*DirFile 31 | } 32 | 33 | // Files contains a full instance of a static file collection 34 | type Files struct { 35 | dir dir 36 | } 37 | 38 | // Config contains information about how extracting the data should behave 39 | // NOTE: FallbackToDisk falls back to disk when file not found in static assets 40 | // usefull when you have a mixture of static assets and some that need to remain 41 | // on disk i.e. a users avatar image 42 | type Config struct { 43 | UseStaticFiles bool 44 | FallbackToDisk bool // falls back to disk when file not found in static assets 45 | AbsPkgPath string // the Absolute package path used for local file reading when UseStaticFiles is false 46 | } 47 | 48 | // New create a new static file instance. 49 | func New(config *Config, dirFile *DirFile) (*Files, error) { 50 | 51 | files := map[string]*file{} 52 | 53 | if config.UseStaticFiles { 54 | processFiles(files, dirFile) 55 | } else { 56 | 57 | if strings.Contains(config.AbsPkgPath, "$") { 58 | config.AbsPkgPath = os.ExpandEnv(config.AbsPkgPath) 59 | } 60 | 61 | if !filepath.IsAbs(config.AbsPkgPath) { 62 | return nil, errors.New("AbsPkgPath is required when not using static files otherwise the static package has no idea where to grab local files from when your package is used from within another package.") 63 | } 64 | } 65 | 66 | return &Files{ 67 | dir: dir{ 68 | useStaticFiles: config.UseStaticFiles, 69 | fallbackToDisk: config.FallbackToDisk, 70 | files: files, 71 | absPkgPath: filepath.Clean(config.AbsPkgPath), 72 | }, 73 | }, nil 74 | } 75 | 76 | func processFiles(files map[string]*file, dirFile *DirFile) *file { 77 | 78 | f := &file{ 79 | path: dirFile.Path, 80 | name: dirFile.Name, 81 | size: dirFile.Size, 82 | mode: dirFile.Mode, 83 | modTime: dirFile.ModTime, 84 | isDir: dirFile.IsDir, 85 | files: []*file{}, 86 | } 87 | 88 | files[filepath.ToSlash(f.path)] = f 89 | 90 | if dirFile.IsDir { 91 | for _, nestedFile := range dirFile.Files { 92 | resultFile := processFiles(files, nestedFile) 93 | f.files = append(f.files, resultFile) 94 | } 95 | 96 | return f 97 | } 98 | 99 | // decompress file contents 100 | b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(dirFile.Compressed)) 101 | reader, err := gzip.NewReader(b64) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | f.data, err = ioutil.ReadAll(reader) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | return f 112 | } 113 | 114 | // FS returns an http.FileSystem object for serving files over http 115 | func (f *Files) FS() http.FileSystem { 116 | return f.dir 117 | } 118 | 119 | func (f *Files) determinePath(name string) string { 120 | 121 | if f.dir.useStaticFiles { 122 | return name 123 | } 124 | 125 | return f.dir.absPkgPath + name 126 | } 127 | 128 | // GetHTTPFile returns an http.File object 129 | func (f *Files) GetHTTPFile(filename string) (http.File, error) { 130 | return f.dir.Open(f.determinePath(filename)) 131 | } 132 | 133 | // ReadFile returns a files contents as []byte from the filesystem, static or local 134 | func (f *Files) ReadFile(filename string) ([]byte, error) { 135 | 136 | file, err := f.dir.Open(f.determinePath(filename)) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | return ioutil.ReadAll(file) 142 | } 143 | 144 | // ReadDir reads the directory named by dirname and returns 145 | // a list of sorted directory entries. 146 | func (f *Files) ReadDir(dirname string) ([]os.FileInfo, error) { 147 | 148 | file, err := f.dir.Open(f.determinePath(dirname)) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | results, err := file.Readdir(-1) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | sort.Sort(byName(results)) 159 | 160 | return results, nil 161 | } 162 | 163 | // ReadFiles returns a directories file contents as a map[string][]byte from the filesystem, static or local 164 | func (f *Files) ReadFiles(dirname string, recursive bool) (map[string][]byte, error) { 165 | 166 | dirname = f.determinePath(dirname) 167 | 168 | results := map[string][]byte{} 169 | 170 | file, err := f.dir.Open(dirname) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | if err = f.readFilesRecursive(dirname+pathSep, file, results, recursive); err != nil { 176 | return nil, err 177 | } 178 | 179 | return results, nil 180 | } 181 | 182 | func (f *Files) readFilesRecursive(dirname string, file http.File, results map[string][]byte, recursive bool) error { 183 | 184 | files, err := file.Readdir(-1) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | var fpath string 190 | 191 | for _, fi := range files { 192 | 193 | fpath = dirname + fi.Name() 194 | 195 | newFile, err := f.dir.Open(fpath) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if fi.IsDir() { 201 | 202 | if !recursive { 203 | continue 204 | } 205 | 206 | err := f.readFilesRecursive(fpath+pathSep, newFile, results, recursive) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | continue 212 | } 213 | 214 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 215 | 216 | link, err := filepath.EvalSymlinks(fpath) 217 | if err != nil { 218 | log.Panic("Error Resolving Symlink", err) 219 | } 220 | 221 | fi, err := os.Stat(link) 222 | if err != nil { 223 | log.Panic(err) 224 | } 225 | 226 | if fi.IsDir() { 227 | 228 | if !recursive { 229 | continue 230 | } 231 | 232 | err := f.readFilesRecursive(fpath+pathSep, newFile, results, recursive) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | continue 238 | } 239 | } 240 | 241 | if !f.dir.useStaticFiles { 242 | fpath = strings.Replace(fpath, f.dir.absPkgPath, "", 1) 243 | } 244 | 245 | results[fpath], err = ioutil.ReadAll(newFile) 246 | if err != nil { 247 | return err 248 | } 249 | } 250 | 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/base64" 8 | "flag" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "strings" 17 | ) 18 | 19 | var ( 20 | flagStaticDir = flag.String("i", "static", "Static directory to embed") 21 | flagOuputFile = flag.String("o", "", "Output file to write to") 22 | flagPkg = flag.String("pkg", "main", "Package name of the generated static file") 23 | flagGroup = flag.String("group", "Assets", "The group name of the static files i.e. CSS, JS, Assets, HTML. It will be added to the generated static function name.") 24 | flagIgnore = flag.String("ignore", "", "Regexp for files/dirs we should ignore i.e. \\.gitignore") 25 | flagPrefix = flag.String("prefix", "", "Prefix to strip from file paths") 26 | flagInit = flag.Bool("init", false, " determines if only initializing the static file without contents") 27 | 28 | ignoreRegexp *regexp.Regexp 29 | writer *bufio.Writer 30 | ) 31 | 32 | func main() { 33 | parseFlags() 34 | 35 | os.Remove(*flagOuputFile) 36 | 37 | f, err := os.Create(*flagOuputFile) 38 | if err != nil { 39 | log.Panic(err) 40 | } 41 | defer f.Close() 42 | 43 | funcName := strings.ToUpper((*flagGroup)[0:1]) + (*flagGroup)[1:] 44 | 45 | writer = bufio.NewWriter(f) 46 | 47 | ignoreFlag := "" 48 | prefixFlag := "" 49 | 50 | if len(*flagIgnore) > 0 { 51 | ignoreFlag = "-ignore=" + *flagIgnore 52 | } 53 | 54 | if len(*flagPrefix) > 0 { 55 | prefixFlag = "-prefix=" + *flagPrefix 56 | } 57 | 58 | if *flagInit { 59 | writer.WriteString(fmt.Sprintf(initStartFile, *flagStaticDir, *flagOuputFile, *flagPkg, *flagGroup, ignoreFlag, prefixFlag, *flagPkg, funcName, funcName)) 60 | writer.WriteString(initEndfile) 61 | } else { 62 | writer.WriteString(fmt.Sprintf(startFile, *flagStaticDir, *flagOuputFile, *flagPkg, *flagGroup, ignoreFlag, prefixFlag, *flagPkg, funcName, funcName)) 63 | processFiles(filepath.Clean(*flagStaticDir)) 64 | writer.WriteString(endfile) 65 | } 66 | 67 | writer.Flush() 68 | 69 | f.Close() 70 | 71 | // after file written run gofmt on file 72 | cmd := exec.Command("gofmt", "-s", "-w", *flagOuputFile) 73 | if err = cmd.Run(); err != nil { 74 | log.Panic(err) 75 | } 76 | } 77 | 78 | func parseFlags() { 79 | 80 | flag.Parse() 81 | 82 | s := filepath.Clean(*flagStaticDir) 83 | flagStaticDir = &s 84 | 85 | if len(*flagStaticDir) == 0 || *flagStaticDir == "." { 86 | panic("**invalid Static File Directoy '" + *flagStaticDir + "'") 87 | } 88 | 89 | if len(*flagOuputFile) == 0 { 90 | panic("**invalid Output Directory") 91 | } 92 | 93 | if len(*flagPkg) == 0 { 94 | panic("**invalid Package Name") 95 | } 96 | 97 | if len(*flagIgnore) > 0 { 98 | 99 | var err error 100 | 101 | ignoreRegexp, err = regexp.Compile(*flagIgnore) 102 | if err != nil { 103 | panic("**Error Compiling Regex:" + err.Error()) 104 | } 105 | } 106 | } 107 | 108 | func processFiles(dir string) { 109 | 110 | fi, err := os.Stat(dir) 111 | if err != nil { 112 | log.Panic(err) 113 | } 114 | 115 | dPath := applyPathOptions(dir) 116 | 117 | writer.WriteString(fmt.Sprintf(dirFileStart, dPath, fi.Name(), fi.Size(), fi.Mode(), fi.ModTime().Unix(), true, "")) 118 | processFilesRecursive(dir, "", false, "") 119 | writer.WriteString(dirFileEnd) 120 | } 121 | 122 | // need isSymlinkDir variable as it is valid for symlinkDir to be blank 123 | func processFilesRecursive(path string, dir string, isSymlinkDir bool, symlinkDir string) { 124 | 125 | var b64File string 126 | var p string 127 | var tmpPath string 128 | 129 | f, err := os.Open(path) 130 | if err != nil { 131 | log.Panic(err) 132 | } 133 | 134 | files, err := f.Readdir(0) 135 | 136 | for _, file := range files { 137 | 138 | info := file 139 | b64File = "" 140 | p = path + string(os.PathSeparator) + file.Name() 141 | fPath := p 142 | 143 | if isSymlinkDir { 144 | fPath = strings.Replace(p, dir, symlinkDir, 1) 145 | } 146 | 147 | if ignoreRegexp != nil && ignoreRegexp.MatchString(fPath) { 148 | continue 149 | } 150 | 151 | if file.IsDir() { 152 | 153 | tmpPath = applyPathOptions(fPath) 154 | 155 | fmt.Println("Processing:", tmpPath) 156 | 157 | // write out here 158 | writer.WriteString(fmt.Sprintf(dirFileStart, tmpPath, info.Name(), info.Size(), info.Mode(), info.ModTime().Unix(), true, "")) 159 | processFilesRecursive(p, p, isSymlinkDir, symlinkDir+string(os.PathSeparator)+info.Name()) 160 | writer.WriteString(dirFileEndArray) 161 | continue 162 | } 163 | 164 | if file.Mode()&os.ModeSymlink == os.ModeSymlink { 165 | 166 | link, err := filepath.EvalSymlinks(p) 167 | if err != nil { 168 | log.Panic("Error Resolving Symlink", err) 169 | } 170 | 171 | fi, err := os.Stat(link) 172 | if err != nil { 173 | log.Panic(err) 174 | } 175 | 176 | info = fi 177 | 178 | if fi.IsDir() { 179 | 180 | tmpPath = applyPathOptions(fPath) 181 | 182 | fmt.Println("Processing:", tmpPath) 183 | 184 | // write out here 185 | writer.WriteString(fmt.Sprintf(dirFileStart, tmpPath, file.Name(), info.Size(), info.Mode(), info.ModTime().Unix(), true, "")) 186 | processFilesRecursive(link, link, true, fPath) 187 | writer.WriteString(dirFileEndArray) 188 | continue 189 | } 190 | } 191 | 192 | // if we get here it's a file 193 | 194 | // read file 195 | b, err := ioutil.ReadFile(p) 196 | if err != nil { 197 | log.Panic(err) 198 | } 199 | 200 | // gzip 201 | var gzBuff bytes.Buffer 202 | gz := gzip.NewWriter(&gzBuff) 203 | defer gz.Close() 204 | 205 | _, err = gz.Write(b) 206 | if err != nil { 207 | log.Panic(err) 208 | } 209 | 210 | // Flush not quaranteed to flush, must close 211 | // gz.Flush() 212 | gz.Close() 213 | 214 | // turn into chunked base64 string 215 | var bb bytes.Buffer 216 | b64 := base64.NewEncoder(base64.StdEncoding, &bb) 217 | b64.Write(gzBuff.Bytes()) 218 | b64.Close() 219 | // b64File += "\n" 220 | chunk := make([]byte, 80) 221 | 222 | for n, _ := bb.Read(chunk); n > 0; n, _ = bb.Read(chunk) { 223 | b64File += string(chunk[0:n]) + "\n" 224 | } 225 | 226 | fPath = applyPathOptions(fPath) 227 | 228 | fmt.Println("Processing:", fPath) 229 | 230 | // write out here 231 | writer.WriteString(fmt.Sprintf(dirFileStart, fPath, file.Name(), info.Size(), info.Mode(), info.ModTime().Unix(), false, b64File)) 232 | writer.WriteString(dirFileEndArray) 233 | } 234 | } 235 | 236 | func applyPathOptions(path string) string { 237 | path = strings.TrimPrefix(path, *flagPrefix) 238 | path = strings.TrimLeft(path, string(os.PathSeparator)) 239 | return string(os.PathSeparator) + path 240 | } 241 | -------------------------------------------------------------------------------- /static/static_test.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | . "gopkg.in/go-playground/assert.v1" 12 | ) 13 | 14 | // NOTES: 15 | // - Run "go test" to run tests 16 | // - Run "gocov test | gocov report" to report on test converage by file 17 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 18 | // 19 | // or 20 | // 21 | // -- may be a good idea to change to output path to somewherelike /tmp 22 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 23 | // 24 | 25 | var testDirFile *DirFile 26 | 27 | func getGOPATH() string { 28 | gopath := os.Getenv("GOPATH") 29 | 30 | if len(gopath) == 0 { 31 | panic("$GOPATH environment is not setup correctly, ending transmission!") 32 | } 33 | 34 | return gopath 35 | } 36 | 37 | func TestMain(m *testing.M) { 38 | 39 | // setup 40 | testDirFile = &DirFile{ 41 | Path: "/static/test-files/teststart", 42 | Name: "teststart", 43 | Size: 170, 44 | Mode: os.FileMode(2147484141), 45 | ModTime: 1446650128, 46 | IsDir: true, 47 | Compressed: ` 48 | `, 49 | Files: []*DirFile{{ 50 | Path: "/static/test-files/teststart/symlinkeddir", 51 | Name: "symlinkeddir", 52 | Size: 136, 53 | Mode: os.FileMode(2147484141), 54 | ModTime: 1446648191, 55 | IsDir: true, 56 | Compressed: ` 57 | `, 58 | Files: []*DirFile{{ 59 | Path: "/static/test-files/teststart/symlinkeddir/realdir", 60 | Name: "realdir", 61 | Size: 136, 62 | Mode: os.FileMode(2147484141), 63 | ModTime: 1446648584, 64 | IsDir: true, 65 | Compressed: ` 66 | `, 67 | Files: []*DirFile{{ 68 | Path: "/static/test-files/teststart/symlinkeddir/realdir/doublesymlinkeddir", 69 | Name: "doublesymlinkeddir", 70 | Size: 136, 71 | Mode: os.FileMode(2147484141), 72 | ModTime: 1447163695, 73 | IsDir: true, 74 | Compressed: ` 75 | `, 76 | Files: []*DirFile{{ 77 | Path: "/static/test-files/teststart/symlinkeddir/realdir/doublesymlinkeddir/doublesymlinkedfile.txt", 78 | Name: "doublesymlinkedfile.txt", 79 | Size: 5, 80 | Mode: os.FileMode(420), 81 | ModTime: 1446648265, 82 | IsDir: false, 83 | Compressed: ` 84 | H4sIAAAJbogA/0pJLEnkAgQAAP//gsXB5gUAAAA= 85 | `, 86 | Files: []*DirFile{}, 87 | }, 88 | { 89 | Path: "/static/test-files/teststart/symlinkeddir/realdir/doublesymlinkeddir/triplesymlinkeddir", 90 | Name: "triplesymlinkeddir", 91 | Size: 102, 92 | Mode: os.FileMode(2147484141), 93 | ModTime: 1447163709, 94 | IsDir: true, 95 | Compressed: ` 96 | `, 97 | Files: []*DirFile{{ 98 | Path: "/static/test-files/teststart/symlinkeddir/realdir/doublesymlinkeddir/triplesymlinkeddir/triplefile.txt", 99 | Name: "triplefile.txt", 100 | Size: 5, 101 | Mode: os.FileMode(420), 102 | ModTime: 1447163511, 103 | IsDir: false, 104 | Compressed: ` 105 | H4sIAAAJbogA/0pJLEnkAgQAAP//gsXB5gUAAAA= 106 | `, 107 | Files: []*DirFile{}, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | { 114 | Path: "/static/test-files/teststart/symlinkeddir/realdir/realdirfile.txt", 115 | Name: "realdirfile.txt", 116 | Size: 5, 117 | Mode: os.FileMode(420), 118 | ModTime: 1446648207, 119 | IsDir: false, 120 | Compressed: ` 121 | H4sIAAAJbogA/0pJLEnkAgQAAP//gsXB5gUAAAA= 122 | `, 123 | Files: []*DirFile{}, 124 | }, 125 | }, 126 | }, 127 | { 128 | Path: "/static/test-files/teststart/symlinkeddir/symlinkeddirfile.txt", 129 | Name: "symlinkeddirfile.txt", 130 | Size: 5, 131 | Mode: os.FileMode(420), 132 | ModTime: 1446647769, 133 | IsDir: false, 134 | Compressed: ` 135 | H4sIAAAJbogA/0pJLEnkAgQAAP//gsXB5gUAAAA= 136 | `, 137 | Files: []*DirFile{}, 138 | }, 139 | }, 140 | }, 141 | { 142 | Path: "/static/test-files/teststart/plainfile.txt", 143 | Name: "plainfile.txt", 144 | Size: 10, 145 | Mode: os.FileMode(420), 146 | ModTime: 1446650128, 147 | IsDir: false, 148 | Compressed: ` 149 | H4sIAAAJbogA/ypIzMnMS0ksSeQCBAAA//9+mKzVCgAAAA== 150 | `, 151 | Files: []*DirFile{}, 152 | }, 153 | { 154 | Path: "/static/test-files/teststart/symlinkedfile.txt", 155 | Name: "symlinkedfile.txt", 156 | Size: 5, 157 | Mode: os.FileMode(420), 158 | ModTime: 1446647746, 159 | IsDir: false, 160 | Compressed: ` 161 | H4sIAAAJbogA/0pJLEnkAgQAAP//gsXB5gUAAAA= 162 | `, 163 | Files: []*DirFile{}, 164 | }, 165 | }, 166 | } 167 | 168 | os.Exit(m.Run()) 169 | 170 | // teardown 171 | } 172 | 173 | func TestStaticNew(t *testing.T) { 174 | 175 | config := &Config{ 176 | UseStaticFiles: true, 177 | AbsPkgPath: "$GOPATH/src/github.com/go-playground/statics", 178 | } 179 | 180 | staticFiles, err := New(config, testDirFile) 181 | Equal(t, err, nil) 182 | NotEqual(t, staticFiles, nil) 183 | 184 | go func(sf *Files) { 185 | 186 | http.Handle("/static/", http.StripPrefix("/", http.FileServer(sf.FS()))) 187 | http.ListenAndServe("127.0.0.1:3006", nil) 188 | 189 | }(staticFiles) 190 | 191 | time.Sleep(5000) 192 | 193 | f, err := staticFiles.GetHTTPFile("/static/test-files/teststart/plainfile.txt") 194 | Equal(t, err, nil) 195 | 196 | fis, err := f.Readdir(-1) 197 | NotEqual(t, err, nil) 198 | Equal(t, err.Error(), "not a directory") 199 | 200 | fi, err := f.Stat() 201 | Equal(t, err, nil) 202 | Equal(t, fi.Name(), "plainfile.txt") 203 | Equal(t, fi.Size(), int64(10)) 204 | Equal(t, fi.IsDir(), false) 205 | Equal(t, fi.Mode(), os.FileMode(420)) 206 | Equal(t, fi.ModTime(), time.Unix(1446650128, 0)) 207 | NotEqual(t, fi.Sys(), nil) 208 | 209 | err = f.Close() 210 | Equal(t, err, nil) 211 | 212 | f, err = staticFiles.GetHTTPFile("/static/test-files/teststart") 213 | Equal(t, err, nil) 214 | 215 | fi, err = f.Stat() 216 | Equal(t, err, nil) 217 | Equal(t, fi.Name(), "teststart") 218 | Equal(t, fi.Size(), int64(170)) 219 | Equal(t, fi.IsDir(), true) 220 | Equal(t, fi.Mode(), os.FileMode(2147484141)) 221 | Equal(t, fi.ModTime(), time.Unix(1446650128, 0)) 222 | NotEqual(t, fi.Sys(), nil) 223 | 224 | var j int 225 | 226 | for err != io.EOF { 227 | 228 | fis, err = f.Readdir(2) 229 | 230 | switch j { 231 | case 0: 232 | Equal(t, len(fis), 2) 233 | case 1: 234 | Equal(t, len(fis), 1) 235 | case 2: 236 | Equal(t, len(fis), 0) 237 | } 238 | 239 | j++ 240 | } 241 | 242 | err = f.Close() 243 | Equal(t, err, nil) 244 | 245 | b, err := staticFiles.ReadFile("/static/test-files/teststart/plainfile.txt") 246 | Equal(t, err, nil) 247 | Equal(t, string(b), "palindata\n") 248 | 249 | b, err = staticFiles.ReadFile("nonexistantfile") 250 | NotEqual(t, err, nil) 251 | 252 | bs, err := staticFiles.ReadFiles("/static/test-files/teststart", false) 253 | Equal(t, err, nil) 254 | Equal(t, len(bs), 2) 255 | Equal(t, string(bs["/static/test-files/teststart/plainfile.txt"]), "palindata\n") 256 | Equal(t, string(bs["/static/test-files/teststart/symlinkedfile.txt"]), "data\n") 257 | 258 | bs, err = staticFiles.ReadFiles("/static/test-files/teststart", true) 259 | Equal(t, err, nil) 260 | Equal(t, len(bs), 6) 261 | 262 | bs, err = staticFiles.ReadFiles("nonexistantdir", false) 263 | NotEqual(t, err, nil) 264 | 265 | fis, err = staticFiles.ReadDir("/static/test-files/teststart") 266 | Equal(t, err, nil) 267 | Equal(t, len(fis), 3) 268 | Equal(t, fis[0].Name(), "plainfile.txt") 269 | Equal(t, fis[1].Name(), "symlinkeddir") 270 | Equal(t, fis[2].Name(), "symlinkedfile.txt") 271 | 272 | fis, err = staticFiles.ReadDir("nonexistantdir") 273 | NotEqual(t, err, nil) 274 | 275 | client := &http.Client{} 276 | 277 | req, err := http.NewRequest("GET", "http://127.0.0.1:3006/static/test-files/teststart/plainfile.txt", nil) 278 | Equal(t, err, nil) 279 | 280 | resp, err := client.Do(req) 281 | Equal(t, err, nil) 282 | Equal(t, resp.StatusCode, http.StatusOK) 283 | 284 | bytes, err := ioutil.ReadAll(resp.Body) 285 | Equal(t, err, nil) 286 | Equal(t, string(bytes), "palindata\n") 287 | 288 | defer resp.Body.Close() 289 | } 290 | 291 | func TestLocalNew(t *testing.T) { 292 | 293 | config := &Config{ 294 | UseStaticFiles: false, 295 | AbsPkgPath: getGOPATH() + "/src/github.com/go-playground/statics", 296 | } 297 | 298 | staticFiles, err := New(config, testDirFile) 299 | Equal(t, err, nil) 300 | NotEqual(t, staticFiles, nil) 301 | 302 | go func(sf *Files) { 303 | 304 | http.Handle("/static/test-files/", http.StripPrefix("/", http.FileServer(sf.FS()))) 305 | http.ListenAndServe("127.0.0.1:3007", nil) 306 | 307 | }(staticFiles) 308 | 309 | time.Sleep(5000) 310 | 311 | f, err := staticFiles.GetHTTPFile("/static/test-files/teststart/plainfile.txt") 312 | Equal(t, err, nil) 313 | 314 | fis, err := f.Readdir(-1) 315 | NotEqual(t, err, nil) 316 | // Equal(t, err.Error(), "readdirent: invalid argument") // this is "not a directory" in linux but readdirent: invalid argument in osx 317 | 318 | fi, err := f.Stat() 319 | Equal(t, err, nil) 320 | Equal(t, fi.Name(), "plainfile.txt") 321 | // Equal(t, fi.Size(), int64(10)) // commented out as size can differ on different file systems when cloned 322 | Equal(t, fi.IsDir(), false) 323 | // Equal(t, fi.Mode(), os.FileMode(420)) // commented out as permissions can be different based on when & where you cloned 324 | // Equal(t, fi.ModTime(), time.Unix(1446650128, 0)) // commented out as file mod times will be different based on when you cloned 325 | NotEqual(t, fi.Sys(), nil) 326 | 327 | err = f.Close() 328 | Equal(t, err, nil) 329 | 330 | f, err = staticFiles.GetHTTPFile("/static/test-files/teststart") 331 | Equal(t, err, nil) 332 | 333 | fi, err = f.Stat() 334 | Equal(t, err, nil) 335 | Equal(t, fi.Name(), "teststart") 336 | // Equal(t, fi.Size(), int64(170)) // commented out as size can differ on different file systems when cloned 337 | Equal(t, fi.IsDir(), true) 338 | // Equal(t, fi.Mode(), os.FileMode(2147484141)) // commented out as permissions can be different based on when & where you cloned 339 | // Equal(t, fi.ModTime(), time.Unix(1446650128, 0)) // commented out as file mod times will be different based on when you cloned 340 | NotEqual(t, fi.Sys(), nil) 341 | 342 | var j int 343 | 344 | for err != io.EOF { 345 | 346 | fis, err = f.Readdir(2) 347 | 348 | switch j { 349 | case 0: 350 | Equal(t, len(fis), 2) 351 | case 1: 352 | Equal(t, len(fis), 1) 353 | case 2: 354 | Equal(t, len(fis), 0) 355 | } 356 | 357 | j++ 358 | } 359 | 360 | err = f.Close() 361 | Equal(t, err, nil) 362 | 363 | b, err := staticFiles.ReadFile("/static/test-files/teststart/plainfile.txt") 364 | Equal(t, err, nil) 365 | Equal(t, string(b), "palindata\n") 366 | 367 | b, err = staticFiles.ReadFile("nonexistantfile") 368 | NotEqual(t, err, nil) 369 | 370 | bs, err := staticFiles.ReadFiles("/static/test-files/teststart", false) 371 | Equal(t, err, nil) 372 | Equal(t, len(bs), 2) 373 | Equal(t, string(bs["/static/test-files/teststart/plainfile.txt"]), "palindata\n") 374 | Equal(t, string(bs["/static/test-files/teststart/symlinkedfile.txt"]), "data\n") 375 | 376 | bs, err = staticFiles.ReadFiles("/static/test-files/teststart", true) 377 | Equal(t, err, nil) 378 | Equal(t, len(bs), 6) 379 | 380 | bs, err = staticFiles.ReadFiles("nonexistantdir", false) 381 | NotEqual(t, err, nil) 382 | 383 | fis, err = staticFiles.ReadDir("/static/test-files/teststart") 384 | Equal(t, err, nil) 385 | Equal(t, len(fis), 3) 386 | Equal(t, fis[0].Name(), "plainfile.txt") 387 | Equal(t, fis[1].Name(), "symlinkeddir") 388 | Equal(t, fis[2].Name(), "symlinkedfile.txt") 389 | 390 | fis, err = staticFiles.ReadDir("nonexistantdir") 391 | NotEqual(t, err, nil) 392 | 393 | client := &http.Client{} 394 | 395 | req, err := http.NewRequest("GET", "http://127.0.0.1:3007/static/test-files/teststart/plainfile.txt", nil) 396 | Equal(t, err, nil) 397 | 398 | resp, err := client.Do(req) 399 | Equal(t, err, nil) 400 | Equal(t, resp.StatusCode, http.StatusOK) 401 | 402 | bytes, err := ioutil.ReadAll(resp.Body) 403 | Equal(t, err, nil) 404 | Equal(t, string(bytes), "palindata\n") 405 | 406 | defer resp.Body.Close() 407 | } 408 | 409 | func TestBadLocalAbsPath(t *testing.T) { 410 | 411 | config := &Config{ 412 | UseStaticFiles: false, 413 | AbsPkgPath: "../github.com/go-playground/statics", 414 | } 415 | 416 | staticFiles, err := New(config, testDirFile) 417 | NotEqual(t, err, nil) 418 | Equal(t, err.Error(), "AbsPkgPath is required when not using static files otherwise the static package has no idea where to grab local files from when your package is used from within another package.") 419 | Equal(t, staticFiles, nil) 420 | } 421 | --------------------------------------------------------------------------------