├── .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 [](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
--------------------------------------------------------------------------------