summaryrefslogtreecommitdiffstats
path: root/lib/assets/assets.go
blob: eca39e467c5221b3870195f6f8ebda35a04e4f92 (plain)
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)
	}
}