assets.go 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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. // Asset is the type of arguments to Serve.
  23. type Asset struct {
  24. ContentGz string // gzipped contents of asset.
  25. Filename string // Original filename, determines Content-Type.
  26. Modified time.Time // Determines ETag and Last-Modified.
  27. }
  28. // Serve writes a gzipped asset to w.
  29. func Serve(w http.ResponseWriter, r *http.Request, asset Asset) {
  30. header := w.Header()
  31. mtype := MimeTypeForFile(asset.Filename)
  32. if mtype != "" {
  33. header.Set("Content-Type", mtype)
  34. }
  35. etag := fmt.Sprintf(`"%x"`, asset.Modified.Unix())
  36. header.Set("ETag", etag)
  37. header.Set("Last-Modified", asset.Modified.Format(http.TimeFormat))
  38. t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since"))
  39. if err == nil && !asset.Modified.After(t) {
  40. w.WriteHeader(http.StatusNotModified)
  41. return
  42. }
  43. if r.Header.Get("If-None-Match") == etag {
  44. w.WriteHeader(http.StatusNotModified)
  45. return
  46. }
  47. if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
  48. header.Set("Content-Encoding", "gzip")
  49. header.Set("Content-Length", strconv.Itoa(len(asset.ContentGz)))
  50. io.WriteString(w, asset.ContentGz)
  51. } else {
  52. // gunzip for browsers that don't want gzip.
  53. var gr *gzip.Reader
  54. gr, _ = gzip.NewReader(strings.NewReader(asset.ContentGz))
  55. io.Copy(w, gr)
  56. gr.Close()
  57. }
  58. }
  59. // MimeTypeForFile returns the appropriate MIME type for an asset,
  60. // based on the filename.
  61. //
  62. // We use a built in table of the common types since the system
  63. // TypeByExtension might be unreliable. But if we don't know, we delegate
  64. // to the system. All our text files are in UTF-8.
  65. func MimeTypeForFile(file string) string {
  66. ext := filepath.Ext(file)
  67. switch ext {
  68. case ".htm", ".html":
  69. return "text/html; charset=utf-8"
  70. case ".css":
  71. return "text/css; charset=utf-8"
  72. case ".js":
  73. return "application/javascript; charset=utf-8"
  74. case ".json":
  75. return "application/json; charset=utf-8"
  76. case ".png":
  77. return "image/png"
  78. case ".ttf":
  79. return "application/x-font-ttf"
  80. case ".woff":
  81. return "application/x-font-woff"
  82. case ".svg":
  83. return "image/svg+xml; charset=utf-8"
  84. default:
  85. return mime.TypeByExtension(ext)
  86. }
  87. }