diff options
Diffstat (limited to 'httputil/static.go')
-rw-r--r-- | httputil/static.go | 265 |
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 | |||
7 | package httputil | ||
8 | |||
9 | import ( | ||
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. | ||
29 | type 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 | |||
48 | func (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 | |||
60 | func (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 | |||
75 | func (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. | ||
96 | func (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. | ||
109 | func (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. | ||
136 | func (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 | |||
165 | type staticHandler struct { | ||
166 | id func(fname string) string | ||
167 | open func(p string) (io.ReadCloser, int64, string, error) | ||
168 | ss *StaticServer | ||
169 | } | ||
170 | |||
171 | func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) { | ||
172 | http.Error(w, http.StatusText(status), status) | ||
173 | } | ||
174 | |||
175 | func (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 | |||
214 | func (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 | } | ||