Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
6.1 KiB
265 lines
6.1 KiB
// 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) |
|
} |
|
}
|
|
|