aboutsummaryrefslogtreecommitdiff
path: root/httputil/static.go
diff options
context:
space:
mode:
Diffstat (limited to 'httputil/static.go')
-rw-r--r--httputil/static.go265
1 files changed, 265 insertions, 0 deletions
diff --git a/httputil/static.go b/httputil/static.go
new file mode 100644
index 0000000..6610dde
--- /dev/null
+++ b/httputil/static.go
@@ -0,0 +1,265 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "bytes"
11 "crypto/sha1"
12 "errors"
13 "fmt"
14 "github.com/golang/gddo/httputil/header"
15 "io"
16 "io/ioutil"
17 "mime"
18 "net/http"
19 "os"
20 "path"
21 "path/filepath"
22 "strconv"
23 "strings"
24 "sync"
25 "time"
26)
27
28// StaticServer serves static files.
29type StaticServer struct {
30 // Dir specifies the location of the directory containing the files to serve.
31 Dir string
32
33 // MaxAge specifies the maximum age for the cache control and expiration
34 // headers.
35 MaxAge time.Duration
36
37 // Error specifies the function used to generate error responses. If Error
38 // is nil, then http.Error is used to generate error responses.
39 Error Error
40
41 // MIMETypes is a map from file extensions to MIME types.
42 MIMETypes map[string]string
43
44 mu sync.Mutex
45 etags map[string]string
46}
47
48func (ss *StaticServer) resolve(fname string) string {
49 if path.IsAbs(fname) {
50 panic("Absolute path not allowed when creating a StaticServer handler")
51 }
52 dir := ss.Dir
53 if dir == "" {
54 dir = "."
55 }
56 fname = filepath.FromSlash(fname)
57 return filepath.Join(dir, fname)
58}
59
60func (ss *StaticServer) mimeType(fname string) string {
61 ext := path.Ext(fname)
62 var mimeType string
63 if ss.MIMETypes != nil {
64 mimeType = ss.MIMETypes[ext]
65 }
66 if mimeType == "" {
67 mimeType = mime.TypeByExtension(ext)
68 }
69 if mimeType == "" {
70 mimeType = "application/octet-stream"
71 }
72 return mimeType
73}
74
75func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
76 f, err := os.Open(fname)
77 if err != nil {
78 return nil, 0, "", err
79 }
80 fi, err := f.Stat()
81 if err != nil {
82 f.Close()
83 return nil, 0, "", err
84 }
85 const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
86 if fi.Mode()&modeType != 0 {
87 f.Close()
88 return nil, 0, "", errors.New("not a regular file")
89 }
90 return f, fi.Size(), ss.mimeType(fname), nil
91}
92
93// FileHandler returns a handler that serves a single file. The file is
94// specified by a slash separated path relative to the static server's Dir
95// field.
96func (ss *StaticServer) FileHandler(fileName string) http.Handler {
97 id := fileName
98 fileName = ss.resolve(fileName)
99 return &staticHandler{
100 ss: ss,
101 id: func(_ string) string { return id },
102 open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
103 }
104}
105
106// DirectoryHandler returns a handler that serves files from a directory tree.
107// The directory is specified by a slash separated path relative to the static
108// server's Dir field.
109func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
110 if !strings.HasSuffix(prefix, "/") {
111 prefix += "/"
112 }
113 idBase := dirName
114 dirName = ss.resolve(dirName)
115 return &staticHandler{
116 ss: ss,
117 id: func(p string) string {
118 if !strings.HasPrefix(p, prefix) {
119 return "."
120 }
121 return path.Join(idBase, p[len(prefix):])
122 },
123 open: func(p string) (io.ReadCloser, int64, string, error) {
124 if !strings.HasPrefix(p, prefix) {
125 return nil, 0, "", errors.New("request url does not match directory prefix")
126 }
127 p = p[len(prefix):]
128 return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
129 },
130 }
131}
132
133// FilesHandler returns a handler that serves the concatentation of the
134// specified files. The files are specified by slash separated paths relative
135// to the static server's Dir field.
136func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
137
138 // todo: cache concatenated files on disk and serve from there.
139
140 mimeType := ss.mimeType(fileNames[0])
141 var buf []byte
142 var openErr error
143
144 for _, fileName := range fileNames {
145 p, err := ioutil.ReadFile(ss.resolve(fileName))
146 if err != nil {
147 openErr = err
148 buf = nil
149 break
150 }
151 buf = append(buf, p...)
152 }
153
154 id := strings.Join(fileNames, " ")
155
156 return &staticHandler{
157 ss: ss,
158 id: func(_ string) string { return id },
159 open: func(p string) (io.ReadCloser, int64, string, error) {
160 return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
161 },
162 }
163}
164
165type staticHandler struct {
166 id func(fname string) string
167 open func(p string) (io.ReadCloser, int64, string, error)
168 ss *StaticServer
169}
170
171func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
172 http.Error(w, http.StatusText(status), status)
173}
174
175func (h *staticHandler) etag(p string) (string, error) {
176 id := h.id(p)
177
178 h.ss.mu.Lock()
179 if h.ss.etags == nil {
180 h.ss.etags = make(map[string]string)
181 }
182 etag := h.ss.etags[id]
183 h.ss.mu.Unlock()
184
185 if etag != "" {
186 return etag, nil
187 }
188
189 // todo: if a concurrent goroutine is calculating the hash, then wait for
190 // it instead of computing it again here.
191
192 rc, _, _, err := h.open(p)
193 if err != nil {
194 return "", err
195 }
196
197 defer rc.Close()
198
199 w := sha1.New()
200 _, err = io.Copy(w, rc)
201 if err != nil {
202 return "", err
203 }
204
205 etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
206
207 h.ss.mu.Lock()
208 h.ss.etags[id] = etag
209 h.ss.mu.Unlock()
210
211 return etag, nil
212}
213
214func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
215 p := path.Clean(r.URL.Path)
216 if p != r.URL.Path {
217 http.Redirect(w, r, p, 301)
218 return
219 }
220
221 etag, err := h.etag(p)
222 if err != nil {
223 h.error(w, r, http.StatusNotFound, err)
224 return
225 }
226
227 maxAge := h.ss.MaxAge
228 if maxAge == 0 {
229 maxAge = 24 * time.Hour
230 }
231 if r.FormValue("v") != "" {
232 maxAge = 365 * 24 * time.Hour
233 }
234
235 cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
236
237 for _, e := range header.ParseList(r.Header, "If-None-Match") {
238 if e == etag {
239 w.Header().Set("Cache-Control", cacheControl)
240 w.Header().Set("Etag", etag)
241 w.WriteHeader(http.StatusNotModified)
242 return
243 }
244 }
245
246 rc, cl, ct, err := h.open(p)
247 if err != nil {
248 h.error(w, r, http.StatusNotFound, err)
249 return
250 }
251 defer rc.Close()
252
253 w.Header().Set("Cache-Control", cacheControl)
254 w.Header().Set("Etag", etag)
255 if ct != "" {
256 w.Header().Set("Content-Type", ct)
257 }
258 if cl != 0 {
259 w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
260 }
261 w.WriteHeader(http.StatusOK)
262 if r.Method != "HEAD" {
263 io.Copy(w, rc)
264 }
265}