/web/static_file_server.go

https://github.com/blend/go-sdk · Go · 258 lines · 193 code · 30 blank · 35 comment · 55 complexity · 35545d2a3c8e0a1e18ab5fb84d2171bb MD5 · raw file

  1. /*
  2. Copyright (c) 2021 - Present. Blend Labs, Inc. All rights reserved
  3. Use of this source code is governed by a MIT license that can be found in the LICENSE file.
  4. */
  5. package web
  6. import (
  7. "bytes"
  8. "io/ioutil"
  9. "net/http"
  10. "os"
  11. "regexp"
  12. "sync"
  13. "github.com/blend/go-sdk/logger"
  14. "github.com/blend/go-sdk/webutil"
  15. )
  16. // NewStaticFileServer returns a new static file cache.
  17. func NewStaticFileServer(options ...StaticFileserverOption) *StaticFileServer {
  18. var sfs StaticFileServer
  19. for _, opt := range options {
  20. opt(&sfs)
  21. }
  22. return &sfs
  23. }
  24. // StaticFileserverOption are options for static fileservers.
  25. type StaticFileserverOption func(*StaticFileServer)
  26. // OptStaticFileServerSearchPaths sets the static fileserver search paths.
  27. func OptStaticFileServerSearchPaths(searchPaths ...http.FileSystem) StaticFileserverOption {
  28. return func(sfs *StaticFileServer) {
  29. sfs.SearchPaths = searchPaths
  30. }
  31. }
  32. // OptStaticFileServerHeaders sets the static fileserver default headers..
  33. func OptStaticFileServerHeaders(headers http.Header) StaticFileserverOption {
  34. return func(sfs *StaticFileServer) {
  35. sfs.Headers = headers
  36. }
  37. }
  38. // OptStaticFileServerCacheDisabled sets the static fileserver should read from disk for each request.
  39. func OptStaticFileServerCacheDisabled(cacheDisabled bool) StaticFileserverOption {
  40. return func(sfs *StaticFileServer) {
  41. sfs.CacheDisabled = cacheDisabled
  42. }
  43. }
  44. // StaticFileServer is a cache of static files.
  45. // It can operate in cached mode, or with `CacheDisabled` set to `true`
  46. // it will read from disk for each request.
  47. // In cached mode, it automatically adds etags for files it caches.
  48. type StaticFileServer struct {
  49. sync.RWMutex
  50. SearchPaths []http.FileSystem
  51. RewriteRules []RewriteRule
  52. Headers http.Header
  53. CacheDisabled bool
  54. Cache map[string]*CachedStaticFile
  55. }
  56. // AddHeader adds a header to the static cache results.
  57. func (sc *StaticFileServer) AddHeader(key, value string) {
  58. if sc.Headers == nil {
  59. sc.Headers = http.Header{}
  60. }
  61. sc.Headers[key] = append(sc.Headers[key], value)
  62. }
  63. // AddRewriteRule adds a static re-write rule.
  64. // This is meant to modify the path of a file from what is requested by the browser
  65. // to how a file may actually be accessed on disk.
  66. // Typically re-write rules are used to enforce caching semantics.
  67. func (sc *StaticFileServer) AddRewriteRule(match string, action RewriteAction) error {
  68. expr, err := regexp.Compile(match)
  69. if err != nil {
  70. return err
  71. }
  72. sc.RewriteRules = append(sc.RewriteRules, RewriteRule{
  73. MatchExpression: match,
  74. expr: expr,
  75. Action: action,
  76. })
  77. return nil
  78. }
  79. // Action is the entrypoint for the static server.
  80. // It adds default headers if specified, and then serves the file from disk
  81. // or from a pull-through cache if enabled.
  82. func (sc *StaticFileServer) Action(r *Ctx) Result {
  83. filePath, err := r.RouteParam("filepath")
  84. if err != nil {
  85. if r.DefaultProvider != nil {
  86. return r.DefaultProvider.BadRequest(err)
  87. }
  88. http.Error(r.Response, err.Error(), http.StatusBadRequest)
  89. return nil
  90. }
  91. for key, values := range sc.Headers {
  92. for _, value := range values {
  93. r.Response.Header().Set(key, value)
  94. }
  95. }
  96. if sc.CacheDisabled {
  97. return sc.ServeFile(r, filePath)
  98. }
  99. return sc.ServeCachedFile(r, filePath)
  100. }
  101. // ServeFile writes the file to the response by reading from disk
  102. // for each request (i.e. skipping the cache)
  103. func (sc *StaticFileServer) ServeFile(r *Ctx, filePath string) Result {
  104. f, finalPath, err := sc.ResolveFile(filePath)
  105. if err != nil {
  106. return sc.fileError(r, err)
  107. }
  108. defer f.Close()
  109. finfo, err := f.Stat()
  110. if err != nil {
  111. return sc.fileError(r, err)
  112. }
  113. if finfo.IsDir() {
  114. return r.DefaultProvider.NotFound()
  115. }
  116. r.WithContext(logger.WithLabel(r.Context(), "web.static_file", finalPath))
  117. http.ServeContent(r.Response, r.Request, filePath, finfo.ModTime(), f)
  118. return nil
  119. }
  120. // ServeCachedFile writes the file to the response, potentially
  121. // serving a cached instance of the file.
  122. func (sc *StaticFileServer) ServeCachedFile(r *Ctx, filepath string) Result {
  123. file, err := sc.ResolveCachedFile(filepath)
  124. if err != nil {
  125. return sc.fileError(r, err)
  126. }
  127. if file == nil {
  128. return r.DefaultProvider.NotFound()
  129. }
  130. _ = file.Render(r)
  131. return nil
  132. }
  133. // ResolveFile resolves a file from rewrite rules and search paths.
  134. // First the file path is modified according to the rewrite rules.
  135. // Then each search path is checked for the resolved file path.
  136. func (sc *StaticFileServer) ResolveFile(filePath string) (f http.File, finalPath string, err error) {
  137. for _, rule := range sc.RewriteRules {
  138. if matched, newFilePath := rule.Apply(filePath); matched {
  139. filePath = newFilePath
  140. }
  141. }
  142. for _, searchPath := range sc.SearchPaths {
  143. f, err = searchPath.Open(filePath)
  144. if typed, ok := f.(*os.File); ok && typed != nil {
  145. finalPath = typed.Name()
  146. }
  147. if err != nil {
  148. if os.IsNotExist(err) {
  149. continue
  150. }
  151. return
  152. }
  153. if f != nil {
  154. return
  155. }
  156. }
  157. return
  158. }
  159. // ResolveCachedFile returns a cached file at a given path.
  160. // It returns the cached instance of a file if it exists, and adds it to the cache if there is a miss.
  161. func (sc *StaticFileServer) ResolveCachedFile(filepath string) (*CachedStaticFile, error) {
  162. // start in read shared mode
  163. sc.RLock()
  164. if sc.Cache != nil {
  165. if file, ok := sc.Cache[filepath]; ok {
  166. sc.RUnlock()
  167. return file, nil
  168. }
  169. }
  170. sc.RUnlock()
  171. // transition to exclusive write mode
  172. sc.Lock()
  173. defer sc.Unlock()
  174. if sc.Cache == nil {
  175. sc.Cache = make(map[string]*CachedStaticFile)
  176. }
  177. // double check ftw
  178. if file, ok := sc.Cache[filepath]; ok {
  179. return file, nil
  180. }
  181. diskFile, _, err := sc.ResolveFile(filepath)
  182. if err != nil {
  183. return nil, err
  184. }
  185. if diskFile == nil {
  186. sc.Cache[filepath] = nil
  187. return nil, nil
  188. }
  189. finfo, err := diskFile.Stat()
  190. if err != nil {
  191. if os.IsNotExist(err) {
  192. return nil, nil
  193. }
  194. return nil, err
  195. }
  196. if finfo.IsDir() {
  197. return nil, nil
  198. }
  199. contents, err := ioutil.ReadAll(diskFile)
  200. if err != nil {
  201. return nil, err
  202. }
  203. file := &CachedStaticFile{
  204. Path: filepath,
  205. Contents: bytes.NewReader(contents),
  206. ModTime: finfo.ModTime(),
  207. ETag: webutil.ETag(contents),
  208. Size: len(contents),
  209. }
  210. sc.Cache[filepath] = file
  211. return file, nil
  212. }
  213. func (sc *StaticFileServer) fileError(r *Ctx, err error) Result {
  214. if os.IsNotExist(err) {
  215. if r.DefaultProvider != nil {
  216. return r.DefaultProvider.NotFound()
  217. }
  218. http.NotFound(r.Response, r.Request)
  219. return nil
  220. }
  221. if r.DefaultProvider != nil {
  222. return r.DefaultProvider.InternalError(err)
  223. }
  224. http.Error(r.Response, err.Error(), http.StatusInternalServerError)
  225. return nil
  226. }