Parcourir la source

Content encoding negotiation added to archive request.

Signed-off-by: Emil Davtyan <emil2k@gmail.com>
Emil Davtyan il y a 7 ans
Parent
commit
117cd7ff64

+ 27 - 3
api/server/router/container/copy.go

@@ -1,6 +1,8 @@
 package container // import "github.com/docker/docker/api/server/router/container"
 
 import (
+	"compress/flate"
+	"compress/gzip"
 	"encoding/base64"
 	"encoding/json"
 	"io"
@@ -9,6 +11,7 @@ import (
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/versions"
+	gddohttputil "github.com/golang/gddo/httputil"
 	"golang.org/x/net/context"
 )
 
@@ -81,6 +84,29 @@ func (s *containerRouter) headContainersArchive(ctx context.Context, w http.Resp
 	return setContainerPathStatHeader(stat, w.Header())
 }
 
+func writeCompressedResponse(w http.ResponseWriter, r *http.Request, body io.Reader) error {
+	var cw io.Writer
+	switch gddohttputil.NegotiateContentEncoding(r, []string{"gzip", "deflate"}) {
+	case "gzip":
+		gw := gzip.NewWriter(w)
+		defer gw.Close()
+		cw = gw
+		w.Header().Set("Content-Encoding", "gzip")
+	case "deflate":
+		fw, err := flate.NewWriter(w, flate.DefaultCompression)
+		if err != nil {
+			return err
+		}
+		defer fw.Close()
+		cw = fw
+		w.Header().Set("Content-Encoding", "deflate")
+	default:
+		cw = w
+	}
+	_, err := io.Copy(cw, body)
+	return err
+}
+
 func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	v, err := httputils.ArchiveFormValues(r, vars)
 	if err != nil {
@@ -98,9 +124,7 @@ func (s *containerRouter) getContainersArchive(ctx context.Context, w http.Respo
 	}
 
 	w.Header().Set("Content-Type", "application/x-tar")
-	_, err = io.Copy(w, tarArchive)
-
-	return err
+	return writeCompressedResponse(w, r, tarArchive)
 }
 
 func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

+ 1 - 0
vendor.conf

@@ -5,6 +5,7 @@ github.com/Microsoft/go-winio v0.4.6
 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
 github.com/go-check/check 4ed411733c5785b40214c70bce814c3a3a689609 https://github.com/cpuguy83/check.git
+github.com/golang/gddo 9b12a26f3fbd7397dee4e20939ddca719d840d2a
 github.com/gorilla/context v1.1
 github.com/gorilla/mux v1.1
 github.com/Microsoft/opengcs v0.3.6

+ 27 - 0
vendor/github.com/golang/gddo/LICENSE

@@ -0,0 +1,27 @@
+Copyright (c) 2013 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 44 - 0
vendor/github.com/golang/gddo/README.markdown

@@ -0,0 +1,44 @@
+This project is the source for http://godoc.org/
+
+[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](http://godoc.org/github.com/golang/gddo)
+[![Build
+Status](https://travis-ci.org/golang/gddo.svg?branch=master)](https://travis-ci.org/golang/gddo)
+
+The code in this project is designed to be used by godoc.org. Send mail to
+golang-dev@googlegroups.com if you want to discuss other uses of the code.
+
+## Feedback
+
+Send ideas and questions to golang-dev@googlegroups.com. Request features and
+report bugs using the [GitHub Issue
+Tracker](https://github.com/golang/gddo/issues/new).
+
+## Contributions
+
+Contributions to this project are welcome, though please [file an
+issue](https://github.com/golang/gddo/issues/new). before starting work on
+anything major.
+
+**We do not accept GitHub pull requests**
+
+Please refer to the [Contribution
+Guidelines](https://golang.org/doc/contribute.html) on how to submit changes.
+
+We use https://go-review.googlesource.com to review change submissions.
+
+## Getting the Source
+
+To get started contributing to this project, clone the repository from its
+canonical location
+
+```
+git clone https://go.googlesource.com/gddo $GOPATH/src/github.com/golang/gddo
+```
+
+Information on how to set up a local environment is available at
+https://github.com/golang/gddo/wiki/Development-Environment-Setup.
+
+## More Documentation
+
+More documentation about this project is available on the
+[wiki](https://github.com/golang/gddo/wiki).

+ 95 - 0
vendor/github.com/golang/gddo/httputil/buster.go

@@ -0,0 +1,95 @@
+// 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 (
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+)
+
+type busterWriter struct {
+	headerMap http.Header
+	status    int
+	io.Writer
+}
+
+func (bw *busterWriter) Header() http.Header {
+	return bw.headerMap
+}
+
+func (bw *busterWriter) WriteHeader(status int) {
+	bw.status = status
+}
+
+// CacheBusters maintains a cache of cache busting tokens for static resources served by Handler.
+type CacheBusters struct {
+	Handler http.Handler
+
+	mu     sync.Mutex
+	tokens map[string]string
+}
+
+func sanitizeTokenRune(r rune) rune {
+	if r <= ' ' || r >= 127 {
+		return -1
+	}
+	// Convert percent encoding reserved characters to '-'.
+	if strings.ContainsRune("!#$&'()*+,/:;=?@[]", r) {
+		return '-'
+	}
+	return r
+}
+
+// Get returns the cache busting token for path. If the token is not already
+// cached, Get issues a HEAD request on handler and uses the response ETag and
+// Last-Modified headers to compute a token.
+func (cb *CacheBusters) Get(path string) string {
+	cb.mu.Lock()
+	if cb.tokens == nil {
+		cb.tokens = make(map[string]string)
+	}
+	token, ok := cb.tokens[path]
+	cb.mu.Unlock()
+	if ok {
+		return token
+	}
+
+	w := busterWriter{
+		Writer:    ioutil.Discard,
+		headerMap: make(http.Header),
+	}
+	r := &http.Request{URL: &url.URL{Path: path}, Method: "HEAD"}
+	cb.Handler.ServeHTTP(&w, r)
+
+	if w.status == 200 {
+		token = w.headerMap.Get("Etag")
+		if token == "" {
+			token = w.headerMap.Get("Last-Modified")
+		}
+		token = strings.Trim(token, `" `)
+		token = strings.Map(sanitizeTokenRune, token)
+	}
+
+	cb.mu.Lock()
+	cb.tokens[path] = token
+	cb.mu.Unlock()
+
+	return token
+}
+
+// AppendQueryParam appends the token as a query parameter to path.
+func (cb *CacheBusters) AppendQueryParam(path string, name string) string {
+	token := cb.Get(path)
+	if token == "" {
+		return path
+	}
+	return path + "?" + name + "=" + token
+}

+ 298 - 0
vendor/github.com/golang/gddo/httputil/header/header.go

@@ -0,0 +1,298 @@
+// 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 header provides functions for parsing HTTP headers.
+package header
+
+import (
+	"net/http"
+	"strings"
+	"time"
+)
+
+// Octet types from RFC 2616.
+var octetTypes [256]octetType
+
+type octetType byte
+
+const (
+	isToken octetType = 1 << iota
+	isSpace
+)
+
+func init() {
+	// OCTET      = <any 8-bit sequence of data>
+	// CHAR       = <any US-ASCII character (octets 0 - 127)>
+	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
+	// CR         = <US-ASCII CR, carriage return (13)>
+	// LF         = <US-ASCII LF, linefeed (10)>
+	// SP         = <US-ASCII SP, space (32)>
+	// HT         = <US-ASCII HT, horizontal-tab (9)>
+	// <">        = <US-ASCII double-quote mark (34)>
+	// CRLF       = CR LF
+	// LWS        = [CRLF] 1*( SP | HT )
+	// TEXT       = <any OCTET except CTLs, but including LWS>
+	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
+	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
+	// token      = 1*<any CHAR except CTLs or separators>
+	// qdtext     = <any TEXT except <">>
+
+	for c := 0; c < 256; c++ {
+		var t octetType
+		isCtl := c <= 31 || c == 127
+		isChar := 0 <= c && c <= 127
+		isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
+		if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
+			t |= isSpace
+		}
+		if isChar && !isCtl && !isSeparator {
+			t |= isToken
+		}
+		octetTypes[c] = t
+	}
+}
+
+// Copy returns a shallow copy of the header.
+func Copy(header http.Header) http.Header {
+	h := make(http.Header)
+	for k, vs := range header {
+		h[k] = vs
+	}
+	return h
+}
+
+var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
+
+// ParseTime parses the header as time. The zero value is returned if the
+// header is not present or there is an error parsing the
+// header.
+func ParseTime(header http.Header, key string) time.Time {
+	if s := header.Get(key); s != "" {
+		for _, layout := range timeLayouts {
+			if t, err := time.Parse(layout, s); err == nil {
+				return t.UTC()
+			}
+		}
+	}
+	return time.Time{}
+}
+
+// ParseList parses a comma separated list of values. Commas are ignored in
+// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
+// trimmed.
+func ParseList(header http.Header, key string) []string {
+	var result []string
+	for _, s := range header[http.CanonicalHeaderKey(key)] {
+		begin := 0
+		end := 0
+		escape := false
+		quote := false
+		for i := 0; i < len(s); i++ {
+			b := s[i]
+			switch {
+			case escape:
+				escape = false
+				end = i + 1
+			case quote:
+				switch b {
+				case '\\':
+					escape = true
+				case '"':
+					quote = false
+				}
+				end = i + 1
+			case b == '"':
+				quote = true
+				end = i + 1
+			case octetTypes[b]&isSpace != 0:
+				if begin == end {
+					begin = i + 1
+					end = begin
+				}
+			case b == ',':
+				if begin < end {
+					result = append(result, s[begin:end])
+				}
+				begin = i + 1
+				end = begin
+			default:
+				end = i + 1
+			}
+		}
+		if begin < end {
+			result = append(result, s[begin:end])
+		}
+	}
+	return result
+}
+
+// ParseValueAndParams parses a comma separated list of values with optional
+// semicolon separated name-value pairs. Content-Type and Content-Disposition
+// headers are in this format.
+func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
+	params = make(map[string]string)
+	s := header.Get(key)
+	value, s = expectTokenSlash(s)
+	if value == "" {
+		return
+	}
+	value = strings.ToLower(value)
+	s = skipSpace(s)
+	for strings.HasPrefix(s, ";") {
+		var pkey string
+		pkey, s = expectToken(skipSpace(s[1:]))
+		if pkey == "" {
+			return
+		}
+		if !strings.HasPrefix(s, "=") {
+			return
+		}
+		var pvalue string
+		pvalue, s = expectTokenOrQuoted(s[1:])
+		if pvalue == "" {
+			return
+		}
+		pkey = strings.ToLower(pkey)
+		params[pkey] = pvalue
+		s = skipSpace(s)
+	}
+	return
+}
+
+// AcceptSpec describes an Accept* header.
+type AcceptSpec struct {
+	Value string
+	Q     float64
+}
+
+// ParseAccept parses Accept* headers.
+func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
+loop:
+	for _, s := range header[key] {
+		for {
+			var spec AcceptSpec
+			spec.Value, s = expectTokenSlash(s)
+			if spec.Value == "" {
+				continue loop
+			}
+			spec.Q = 1.0
+			s = skipSpace(s)
+			if strings.HasPrefix(s, ";") {
+				s = skipSpace(s[1:])
+				if !strings.HasPrefix(s, "q=") {
+					continue loop
+				}
+				spec.Q, s = expectQuality(s[2:])
+				if spec.Q < 0.0 {
+					continue loop
+				}
+			}
+			specs = append(specs, spec)
+			s = skipSpace(s)
+			if !strings.HasPrefix(s, ",") {
+				continue loop
+			}
+			s = skipSpace(s[1:])
+		}
+	}
+	return
+}
+
+func skipSpace(s string) (rest string) {
+	i := 0
+	for ; i < len(s); i++ {
+		if octetTypes[s[i]]&isSpace == 0 {
+			break
+		}
+	}
+	return s[i:]
+}
+
+func expectToken(s string) (token, rest string) {
+	i := 0
+	for ; i < len(s); i++ {
+		if octetTypes[s[i]]&isToken == 0 {
+			break
+		}
+	}
+	return s[:i], s[i:]
+}
+
+func expectTokenSlash(s string) (token, rest string) {
+	i := 0
+	for ; i < len(s); i++ {
+		b := s[i]
+		if (octetTypes[b]&isToken == 0) && b != '/' {
+			break
+		}
+	}
+	return s[:i], s[i:]
+}
+
+func expectQuality(s string) (q float64, rest string) {
+	switch {
+	case len(s) == 0:
+		return -1, ""
+	case s[0] == '0':
+		q = 0
+	case s[0] == '1':
+		q = 1
+	default:
+		return -1, ""
+	}
+	s = s[1:]
+	if !strings.HasPrefix(s, ".") {
+		return q, s
+	}
+	s = s[1:]
+	i := 0
+	n := 0
+	d := 1
+	for ; i < len(s); i++ {
+		b := s[i]
+		if b < '0' || b > '9' {
+			break
+		}
+		n = n*10 + int(b) - '0'
+		d *= 10
+	}
+	return q + float64(n)/float64(d), s[i:]
+}
+
+func expectTokenOrQuoted(s string) (value string, rest string) {
+	if !strings.HasPrefix(s, "\"") {
+		return expectToken(s)
+	}
+	s = s[1:]
+	for i := 0; i < len(s); i++ {
+		switch s[i] {
+		case '"':
+			return s[:i], s[i+1:]
+		case '\\':
+			p := make([]byte, len(s)-1)
+			j := copy(p, s[:i])
+			escape := true
+			for i = i + 1; i < len(s); i++ {
+				b := s[i]
+				switch {
+				case escape:
+					escape = false
+					p[j] = b
+					j++
+				case b == '\\':
+					escape = true
+				case b == '"':
+					return string(p[:j]), s[i+1:]
+				default:
+					p[j] = b
+					j++
+				}
+			}
+			return "", ""
+		}
+	}
+	return "", ""
+}

+ 25 - 0
vendor/github.com/golang/gddo/httputil/httputil.go

@@ -0,0 +1,25 @@
+// 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 is a toolkit for the Go net/http package.
+package httputil
+
+import (
+	"net"
+	"net/http"
+)
+
+// StripPort removes the port specification from an address.
+func StripPort(s string) string {
+	if h, _, err := net.SplitHostPort(s); err == nil {
+		s = h
+	}
+	return s
+}
+
+// Error defines a type for a function that accepts a ResponseWriter for
+// a Request with the HTTP status code and error.
+type Error func(w http.ResponseWriter, r *http.Request, status int, err error)

+ 79 - 0
vendor/github.com/golang/gddo/httputil/negotiate.go

@@ -0,0 +1,79 @@
+// 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 (
+	"github.com/golang/gddo/httputil/header"
+	"net/http"
+	"strings"
+)
+
+// NegotiateContentEncoding returns the best offered content encoding for the
+// request's Accept-Encoding header. If two offers match with equal weight and
+// then the offer earlier in the list is preferred. If no offers are
+// acceptable, then "" is returned.
+func NegotiateContentEncoding(r *http.Request, offers []string) string {
+	bestOffer := "identity"
+	bestQ := -1.0
+	specs := header.ParseAccept(r.Header, "Accept-Encoding")
+	for _, offer := range offers {
+		for _, spec := range specs {
+			if spec.Q > bestQ &&
+				(spec.Value == "*" || spec.Value == offer) {
+				bestQ = spec.Q
+				bestOffer = offer
+			}
+		}
+	}
+	if bestQ == 0 {
+		bestOffer = ""
+	}
+	return bestOffer
+}
+
+// NegotiateContentType returns the best offered content type for the request's
+// Accept header. If two offers match with equal weight, then the more specific
+// offer is preferred.  For example, text/* trumps */*. If two offers match
+// with equal weight and specificity, then the offer earlier in the list is
+// preferred. If no offers match, then defaultOffer is returned.
+func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
+	bestOffer := defaultOffer
+	bestQ := -1.0
+	bestWild := 3
+	specs := header.ParseAccept(r.Header, "Accept")
+	for _, offer := range offers {
+		for _, spec := range specs {
+			switch {
+			case spec.Q == 0.0:
+				// ignore
+			case spec.Q < bestQ:
+				// better match found
+			case spec.Value == "*/*":
+				if spec.Q > bestQ || bestWild > 2 {
+					bestQ = spec.Q
+					bestWild = 2
+					bestOffer = offer
+				}
+			case strings.HasSuffix(spec.Value, "/*"):
+				if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
+					(spec.Q > bestQ || bestWild > 1) {
+					bestQ = spec.Q
+					bestWild = 1
+					bestOffer = offer
+				}
+			default:
+				if spec.Value == offer &&
+					(spec.Q > bestQ || bestWild > 0) {
+					bestQ = spec.Q
+					bestWild = 0
+					bestOffer = offer
+				}
+			}
+		}
+	}
+	return bestOffer
+}

+ 58 - 0
vendor/github.com/golang/gddo/httputil/respbuf.go

@@ -0,0 +1,58 @@
+// 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"
+	"net/http"
+	"strconv"
+)
+
+// ResponseBuffer is the current response being composed by its owner.
+// It implements http.ResponseWriter and io.WriterTo.
+type ResponseBuffer struct {
+	buf    bytes.Buffer
+	status int
+	header http.Header
+}
+
+// Write implements the http.ResponseWriter interface.
+func (rb *ResponseBuffer) Write(p []byte) (int, error) {
+	return rb.buf.Write(p)
+}
+
+// WriteHeader implements the http.ResponseWriter interface.
+func (rb *ResponseBuffer) WriteHeader(status int) {
+	rb.status = status
+}
+
+// Header implements the http.ResponseWriter interface.
+func (rb *ResponseBuffer) Header() http.Header {
+	if rb.header == nil {
+		rb.header = make(http.Header)
+	}
+	return rb.header
+}
+
+// WriteTo implements the io.WriterTo interface.
+func (rb *ResponseBuffer) WriteTo(w http.ResponseWriter) error {
+	for k, v := range rb.header {
+		w.Header()[k] = v
+	}
+	if rb.buf.Len() > 0 {
+		w.Header().Set("Content-Length", strconv.Itoa(rb.buf.Len()))
+	}
+	if rb.status != 0 {
+		w.WriteHeader(rb.status)
+	}
+	if rb.buf.Len() > 0 {
+		if _, err := w.Write(rb.buf.Bytes()); err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 265 - 0
vendor/github.com/golang/gddo/httputil/static.go

@@ -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)
+	}
+}

+ 87 - 0
vendor/github.com/golang/gddo/httputil/transport.go

@@ -0,0 +1,87 @@
+// Copyright 2015 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.
+
+// This file implements a http.RoundTripper that authenticates
+// requests issued against api.github.com endpoint.
+
+package httputil
+
+import (
+	"net/http"
+	"net/url"
+)
+
+// AuthTransport is an implementation of http.RoundTripper that authenticates
+// with the GitHub API.
+//
+// When both a token and client credentials are set, the latter is preferred.
+type AuthTransport struct {
+	UserAgent          string
+	GithubToken        string
+	GithubClientID     string
+	GithubClientSecret string
+	Base               http.RoundTripper
+}
+
+// RoundTrip implements the http.RoundTripper interface.
+func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	var reqCopy *http.Request
+	if t.UserAgent != "" {
+		reqCopy = copyRequest(req)
+		reqCopy.Header.Set("User-Agent", t.UserAgent)
+	}
+	if req.URL.Host == "api.github.com" && req.URL.Scheme == "https" {
+		switch {
+		case t.GithubClientID != "" && t.GithubClientSecret != "":
+			if reqCopy == nil {
+				reqCopy = copyRequest(req)
+			}
+			if reqCopy.URL.RawQuery == "" {
+				reqCopy.URL.RawQuery = "client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
+			} else {
+				reqCopy.URL.RawQuery += "&client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
+			}
+		case t.GithubToken != "":
+			if reqCopy == nil {
+				reqCopy = copyRequest(req)
+			}
+			reqCopy.Header.Set("Authorization", "token "+t.GithubToken)
+		}
+	}
+	if reqCopy != nil {
+		return t.base().RoundTrip(reqCopy)
+	}
+	return t.base().RoundTrip(req)
+}
+
+// CancelRequest cancels an in-flight request by closing its connection.
+func (t *AuthTransport) CancelRequest(req *http.Request) {
+	type canceler interface {
+		CancelRequest(req *http.Request)
+	}
+	if cr, ok := t.base().(canceler); ok {
+		cr.CancelRequest(req)
+	}
+}
+
+func (t *AuthTransport) base() http.RoundTripper {
+	if t.Base != nil {
+		return t.Base
+	}
+	return http.DefaultTransport
+}
+
+func copyRequest(req *http.Request) *http.Request {
+	req2 := new(http.Request)
+	*req2 = *req
+	req2.URL = new(url.URL)
+	*req2.URL = *req.URL
+	req2.Header = make(http.Header, len(req.Header))
+	for k, s := range req.Header {
+		req2.Header[k] = append([]string(nil), s...)
+	}
+	return req2
+}