|
@@ -0,0 +1,265 @@
|
|
|
+// Copyright 2013 The Go Authors. All rights reserved.
|
|
|
+//
|
|
|
+// Use of this source code is governed by a BSD-style
|
|
|
+// license that can be found in the LICENSE file or at
|
|
|
+// https://developers.google.com/open-source/licenses/bsd.
|
|
|
+
|
|
|
+package httputil
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "crypto/sha1"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "github.com/golang/gddo/httputil/header"
|
|
|
+ "io"
|
|
|
+ "io/ioutil"
|
|
|
+ "mime"
|
|
|
+ "net/http"
|
|
|
+ "os"
|
|
|
+ "path"
|
|
|
+ "path/filepath"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+// StaticServer serves static files.
|
|
|
+type StaticServer struct {
|
|
|
+ // Dir specifies the location of the directory containing the files to serve.
|
|
|
+ Dir string
|
|
|
+
|
|
|
+ // MaxAge specifies the maximum age for the cache control and expiration
|
|
|
+ // headers.
|
|
|
+ MaxAge time.Duration
|
|
|
+
|
|
|
+ // Error specifies the function used to generate error responses. If Error
|
|
|
+ // is nil, then http.Error is used to generate error responses.
|
|
|
+ Error Error
|
|
|
+
|
|
|
+ // MIMETypes is a map from file extensions to MIME types.
|
|
|
+ MIMETypes map[string]string
|
|
|
+
|
|
|
+ mu sync.Mutex
|
|
|
+ etags map[string]string
|
|
|
+}
|
|
|
+
|
|
|
+func (ss *StaticServer) resolve(fname string) string {
|
|
|
+ if path.IsAbs(fname) {
|
|
|
+ panic("Absolute path not allowed when creating a StaticServer handler")
|
|
|
+ }
|
|
|
+ dir := ss.Dir
|
|
|
+ if dir == "" {
|
|
|
+ dir = "."
|
|
|
+ }
|
|
|
+ fname = filepath.FromSlash(fname)
|
|
|
+ return filepath.Join(dir, fname)
|
|
|
+}
|
|
|
+
|
|
|
+func (ss *StaticServer) mimeType(fname string) string {
|
|
|
+ ext := path.Ext(fname)
|
|
|
+ var mimeType string
|
|
|
+ if ss.MIMETypes != nil {
|
|
|
+ mimeType = ss.MIMETypes[ext]
|
|
|
+ }
|
|
|
+ if mimeType == "" {
|
|
|
+ mimeType = mime.TypeByExtension(ext)
|
|
|
+ }
|
|
|
+ if mimeType == "" {
|
|
|
+ mimeType = "application/octet-stream"
|
|
|
+ }
|
|
|
+ return mimeType
|
|
|
+}
|
|
|
+
|
|
|
+func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
|
|
|
+ f, err := os.Open(fname)
|
|
|
+ if err != nil {
|
|
|
+ return nil, 0, "", err
|
|
|
+ }
|
|
|
+ fi, err := f.Stat()
|
|
|
+ if err != nil {
|
|
|
+ f.Close()
|
|
|
+ return nil, 0, "", err
|
|
|
+ }
|
|
|
+ const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
|
|
|
+ if fi.Mode()&modeType != 0 {
|
|
|
+ f.Close()
|
|
|
+ return nil, 0, "", errors.New("not a regular file")
|
|
|
+ }
|
|
|
+ return f, fi.Size(), ss.mimeType(fname), nil
|
|
|
+}
|
|
|
+
|
|
|
+// FileHandler returns a handler that serves a single file. The file is
|
|
|
+// specified by a slash separated path relative to the static server's Dir
|
|
|
+// field.
|
|
|
+func (ss *StaticServer) FileHandler(fileName string) http.Handler {
|
|
|
+ id := fileName
|
|
|
+ fileName = ss.resolve(fileName)
|
|
|
+ return &staticHandler{
|
|
|
+ ss: ss,
|
|
|
+ id: func(_ string) string { return id },
|
|
|
+ open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// DirectoryHandler returns a handler that serves files from a directory tree.
|
|
|
+// The directory is specified by a slash separated path relative to the static
|
|
|
+// server's Dir field.
|
|
|
+func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
|
|
|
+ if !strings.HasSuffix(prefix, "/") {
|
|
|
+ prefix += "/"
|
|
|
+ }
|
|
|
+ idBase := dirName
|
|
|
+ dirName = ss.resolve(dirName)
|
|
|
+ return &staticHandler{
|
|
|
+ ss: ss,
|
|
|
+ id: func(p string) string {
|
|
|
+ if !strings.HasPrefix(p, prefix) {
|
|
|
+ return "."
|
|
|
+ }
|
|
|
+ return path.Join(idBase, p[len(prefix):])
|
|
|
+ },
|
|
|
+ open: func(p string) (io.ReadCloser, int64, string, error) {
|
|
|
+ if !strings.HasPrefix(p, prefix) {
|
|
|
+ return nil, 0, "", errors.New("request url does not match directory prefix")
|
|
|
+ }
|
|
|
+ p = p[len(prefix):]
|
|
|
+ return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// FilesHandler returns a handler that serves the concatentation of the
|
|
|
+// specified files. The files are specified by slash separated paths relative
|
|
|
+// to the static server's Dir field.
|
|
|
+func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
|
|
|
+
|
|
|
+ // todo: cache concatenated files on disk and serve from there.
|
|
|
+
|
|
|
+ mimeType := ss.mimeType(fileNames[0])
|
|
|
+ var buf []byte
|
|
|
+ var openErr error
|
|
|
+
|
|
|
+ for _, fileName := range fileNames {
|
|
|
+ p, err := ioutil.ReadFile(ss.resolve(fileName))
|
|
|
+ if err != nil {
|
|
|
+ openErr = err
|
|
|
+ buf = nil
|
|
|
+ break
|
|
|
+ }
|
|
|
+ buf = append(buf, p...)
|
|
|
+ }
|
|
|
+
|
|
|
+ id := strings.Join(fileNames, " ")
|
|
|
+
|
|
|
+ return &staticHandler{
|
|
|
+ ss: ss,
|
|
|
+ id: func(_ string) string { return id },
|
|
|
+ open: func(p string) (io.ReadCloser, int64, string, error) {
|
|
|
+ return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type staticHandler struct {
|
|
|
+ id func(fname string) string
|
|
|
+ open func(p string) (io.ReadCloser, int64, string, error)
|
|
|
+ ss *StaticServer
|
|
|
+}
|
|
|
+
|
|
|
+func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
|
|
|
+ http.Error(w, http.StatusText(status), status)
|
|
|
+}
|
|
|
+
|
|
|
+func (h *staticHandler) etag(p string) (string, error) {
|
|
|
+ id := h.id(p)
|
|
|
+
|
|
|
+ h.ss.mu.Lock()
|
|
|
+ if h.ss.etags == nil {
|
|
|
+ h.ss.etags = make(map[string]string)
|
|
|
+ }
|
|
|
+ etag := h.ss.etags[id]
|
|
|
+ h.ss.mu.Unlock()
|
|
|
+
|
|
|
+ if etag != "" {
|
|
|
+ return etag, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // todo: if a concurrent goroutine is calculating the hash, then wait for
|
|
|
+ // it instead of computing it again here.
|
|
|
+
|
|
|
+ rc, _, _, err := h.open(p)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ defer rc.Close()
|
|
|
+
|
|
|
+ w := sha1.New()
|
|
|
+ _, err = io.Copy(w, rc)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
|
|
|
+
|
|
|
+ h.ss.mu.Lock()
|
|
|
+ h.ss.etags[id] = etag
|
|
|
+ h.ss.mu.Unlock()
|
|
|
+
|
|
|
+ return etag, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
+ p := path.Clean(r.URL.Path)
|
|
|
+ if p != r.URL.Path {
|
|
|
+ http.Redirect(w, r, p, 301)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ etag, err := h.etag(p)
|
|
|
+ if err != nil {
|
|
|
+ h.error(w, r, http.StatusNotFound, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ maxAge := h.ss.MaxAge
|
|
|
+ if maxAge == 0 {
|
|
|
+ maxAge = 24 * time.Hour
|
|
|
+ }
|
|
|
+ if r.FormValue("v") != "" {
|
|
|
+ maxAge = 365 * 24 * time.Hour
|
|
|
+ }
|
|
|
+
|
|
|
+ cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
|
|
|
+
|
|
|
+ for _, e := range header.ParseList(r.Header, "If-None-Match") {
|
|
|
+ if e == etag {
|
|
|
+ w.Header().Set("Cache-Control", cacheControl)
|
|
|
+ w.Header().Set("Etag", etag)
|
|
|
+ w.WriteHeader(http.StatusNotModified)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ rc, cl, ct, err := h.open(p)
|
|
|
+ if err != nil {
|
|
|
+ h.error(w, r, http.StatusNotFound, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer rc.Close()
|
|
|
+
|
|
|
+ w.Header().Set("Cache-Control", cacheControl)
|
|
|
+ w.Header().Set("Etag", etag)
|
|
|
+ if ct != "" {
|
|
|
+ w.Header().Set("Content-Type", ct)
|
|
|
+ }
|
|
|
+ if cl != 0 {
|
|
|
+ w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
|
|
|
+ }
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ if r.Method != "HEAD" {
|
|
|
+ io.Copy(w, rc)
|
|
|
+ }
|
|
|
+}
|