1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
// Copyright (C) 2014-2020 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// Package assets hold utilities for serving static assets.
//
// The actual assets live in auto subpackages instead of here,
// because the set of assets varies per program.
package assets
import (
"compress/gzip"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
)
// An Asset is an embedded file to be served over HTTP.
type Asset struct {
Content string // Contents of asset, possibly gzipped.
Gzipped bool
Length int // Length of (decompressed) Content.
Filename string // Original filename, determines Content-Type.
Modified time.Time // Determines ETag and Last-Modified.
}
// Serve writes a gzipped asset to w.
func Serve(w http.ResponseWriter, r *http.Request, asset Asset) {
header := w.Header()
mtype := MimeTypeForFile(asset.Filename)
if mtype != "" {
header.Set("Content-Type", mtype)
}
etag := fmt.Sprintf(`"%x"`, asset.Modified.Unix())
header.Set("ETag", etag)
header.Set("Last-Modified", asset.Modified.Format(http.TimeFormat))
t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since"))
if err == nil && !asset.Modified.After(t) {
w.WriteHeader(http.StatusNotModified)
return
}
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
switch {
case !asset.Gzipped:
header.Set("Content-Length", strconv.Itoa(len(asset.Content)))
io.WriteString(w, asset.Content)
case strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"):
header.Set("Content-Encoding", "gzip")
header.Set("Content-Length", strconv.Itoa(len(asset.Content)))
io.WriteString(w, asset.Content)
default:
header.Set("Content-Length", strconv.Itoa(asset.Length))
// gunzip for browsers that don't want gzip.
var gr *gzip.Reader
gr, _ = gzip.NewReader(strings.NewReader(asset.Content))
io.Copy(w, gr)
gr.Close()
}
}
// MimeTypeForFile returns the appropriate MIME type for an asset,
// based on the filename.
//
// We use a built in table of the common types since the system
// TypeByExtension might be unreliable. But if we don't know, we delegate
// to the system. All our text files are in UTF-8.
func MimeTypeForFile(file string) string {
ext := filepath.Ext(file)
switch ext {
case ".htm", ".html":
return "text/html; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".js":
return "application/javascript; charset=utf-8"
case ".json":
return "application/json; charset=utf-8"
case ".png":
return "image/png"
case ".ttf":
return "application/x-font-ttf"
case ".woff":
return "application/x-font-woff"
case ".svg":
return "image/svg+xml; charset=utf-8"
default:
return mime.TypeByExtension(ext)
}
}
|