├── .gitignore ├── LICENSE ├── README.md ├── mix.go ├── mix_test.go └── test ├── one.js ├── three.js └── two.js /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mat Ryer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mix [![GoDoc](https://godoc.org/github.com/matryer/mix?status.svg)](https://godoc.org/github.com/matryer/mix) 2 | 3 | Go http.Handler that mixes many files into one request. 4 | 5 | * Trivial to use 6 | * Each file will only be included once, despite how many times it might match a pattern 7 | * Uses [Glob (from go-zglob)](http://github.com/mattn/go-zglob) providing familiar filepath patterns 8 | * Uses `http.ServeContent` so all headers are managed nicely 9 | 10 | Go from this: 11 | 12 | ``` 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | to this: 21 | 22 | ``` 23 | 24 | ``` 25 | 26 | ## Usage 27 | 28 | ``` 29 | go get gopkg.in/matryer/mix.v2 30 | ``` 31 | 32 | If you have a directory containing many JavaScript files: 33 | 34 | ``` 35 | files/ 36 | js/ 37 | one.js 38 | two.js 39 | three.js 40 | lib/ 41 | four.js 42 | ``` 43 | 44 | You can use `mix.Handler` to specify filepath patterns to serve them all in a single request. 45 | 46 | ``` 47 | http.Handle("/mix/all.js", mix.New("./files/js/*.js", "./files/lib/*.js")) 48 | ``` 49 | 50 | * The `Content-Type` will be taken from the request path. 51 | 52 | ### Notes 53 | 54 | #### App engine 55 | 56 | It's important to remember that files marked as static with `static_dir` or `static_file` in App Engine are *not* available to your Go code. So mix cannot work on those files. Instead, you should structure your app so that mixable content lives in a different directory to your static files. 57 | -------------------------------------------------------------------------------- /mix.go: -------------------------------------------------------------------------------- 1 | package mix 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/mattn/go-zglob" 13 | ) 14 | 15 | // Handler is a http.Handler that mixes files. 16 | type Handler struct { 17 | files []string 18 | err error 19 | 20 | // Header allows you to set common response headers that will be 21 | // send with requests handled by this Handler. 22 | Header http.Header 23 | } 24 | 25 | var _ http.Handler = (*Handler)(nil) 26 | 27 | // New makes a new mix handler with the specified files or 28 | // patterns. 29 | // Patterns powered by https://github.com/mattn/go-zglob. 30 | func New(patterns ...string) *Handler { 31 | files, err := glob(patterns...) 32 | h := (&Handler{ 33 | files: files, 34 | err: err, 35 | }) 36 | return h 37 | } 38 | 39 | // serveFiles serves all specified files. 40 | // Content-Type (if not set) will be inferred from the extension in the 41 | // request. 42 | // Uses http.ServeContent to serve the content. 43 | func serveFiles(w http.ResponseWriter, r *http.Request, files ...string) { 44 | 45 | var recentMod time.Time 46 | var buf bytes.Buffer 47 | for _, f := range files { 48 | 49 | stat, err := os.Stat(f) 50 | if err != nil { 51 | http.Error(w, err.Error(), http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | // keep track of latest modtime 56 | if stat.ModTime().After(recentMod) { 57 | recentMod = stat.ModTime() 58 | } 59 | 60 | file, err := os.Open(f) 61 | if err != nil { 62 | http.Error(w, err.Error(), http.StatusInternalServerError) 63 | return 64 | } 65 | _, err = io.Copy(&buf, file) 66 | file.Close() 67 | if err != nil { 68 | http.Error(w, err.Error(), http.StatusInternalServerError) 69 | return 70 | } 71 | 72 | // write linefeed 73 | if _, err := buf.WriteRune('\n'); err != nil { 74 | http.Error(w, err.Error(), http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | } 79 | 80 | http.ServeContent(w, r, path.Base(r.URL.Path), recentMod, sizable(buf)) 81 | 82 | } 83 | 84 | // ServeHTTP serves the request. 85 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 86 | 87 | // set headers 88 | for k, vs := range h.Header { 89 | for _, v := range vs { 90 | w.Header().Add(k, v) 91 | } 92 | } 93 | 94 | // return error if something went wrong 95 | if h.err != nil { 96 | http.Error(w, h.err.Error(), http.StatusInternalServerError) 97 | return 98 | } 99 | 100 | serveFiles(w, r, h.files...) 101 | 102 | } 103 | 104 | // glob takes a range of patterns and produces a unique list 105 | // of matching files. 106 | // Files are added in pattern and alphabetical order. 107 | // Like filepath.Glob, but you can pass in many patterns. 108 | // Uses https://github.com/mattn/go-zglob under the hood. 109 | func glob(patterns ...string) ([]string, error) { 110 | seen := make(map[string]struct{}) 111 | var files []string 112 | for _, g := range patterns { 113 | matches, err := zglob.Glob(g) 114 | if err != nil { 115 | return nil, err 116 | } 117 | for _, match := range matches { 118 | match = filepath.Clean(match) 119 | if _, alreadySeen := seen[match]; !alreadySeen { 120 | files = append(files, match) 121 | seen[match] = struct{}{} 122 | } 123 | } 124 | } 125 | return files, nil 126 | } 127 | 128 | // sizableBuffer is a wrapper around a bytes.Buffer that allows 129 | // http.ServeContent to get the content length. 130 | // Buffers can't normally seek, so this just simulates the behaviour 131 | // and returns buf.Len() when os.SEEK_END is requested. 132 | type sizableBuffer struct { 133 | buf bytes.Buffer 134 | } 135 | 136 | var _ io.ReadSeeker = (*sizableBuffer)(nil) 137 | 138 | func (s *sizableBuffer) Seek(offset int64, whence int) (int64, error) { 139 | if whence == os.SEEK_END { 140 | return int64(s.buf.Len()) + offset, nil 141 | } 142 | return 0, nil 143 | } 144 | 145 | func (s *sizableBuffer) Read(p []byte) (int, error) { 146 | return s.buf.Read(p) 147 | } 148 | 149 | func sizable(buf bytes.Buffer) *sizableBuffer { 150 | return &sizableBuffer{buf: buf} 151 | } 152 | -------------------------------------------------------------------------------- /mix_test.go: -------------------------------------------------------------------------------- 1 | package mix_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/cheekybits/is" 10 | "github.com/matryer/mix" 11 | ) 12 | 13 | func TestMixHandler(t *testing.T) { 14 | is := is.New(t) 15 | 16 | h := mix.New("./test/one.js", "./test/*.js") 17 | 18 | w := httptest.NewRecorder() 19 | r, err := http.NewRequest("GET", "/assets/all.js", nil) 20 | is.NoErr(err) 21 | 22 | h.ServeHTTP(w, r) 23 | 24 | is.Equal(w.Code, http.StatusOK) 25 | is.True(strings.Contains(w.Body.String(), "one\n")) 26 | is.True(strings.Contains(w.Body.String(), "two\n")) 27 | is.True(strings.Contains(w.Body.String(), "three\n")) 28 | is.Equal(w.HeaderMap.Get("Content-Type"), "application/javascript") 29 | is.Equal(w.HeaderMap.Get("Content-Length"), "14") 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/one.js: -------------------------------------------------------------------------------- 1 | one -------------------------------------------------------------------------------- /test/three.js: -------------------------------------------------------------------------------- 1 | three -------------------------------------------------------------------------------- /test/two.js: -------------------------------------------------------------------------------- 1 | two --------------------------------------------------------------------------------