assets.go 2.9 KB

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