 684b7a999f
			
		
	
	
		684b7a999f
		
			
		
	
	
	
	
		
			
			* Dump: Use mholt/archive/v3 to support tar including many compressions Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Dump: Allow dump output to stdout Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Dump: Fixed bug present since #6677 where SessionConfig.Provider is never "file" Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Dump: never pack RepoRootPath, LFS.ContentPath and LogRootPath when they are below AppDataPath Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Dump: also dump LFS (fixes #10058) Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Dump: never dump CustomPath if CustomPath is a subdir of or equal to AppDataPath (fixes #10365) Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * Use log.Info instead of fmt.Fprintf Signed-off-by: Philipp Homann <homann.philipp@googlemail.com> * import ordering * make fmt Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Matti R <matti@mdranta.net>
		
			
				
	
	
		
			265 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			265 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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)
 | |
| 	}
 | |
| }
 |