filesystem.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package filesystem
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/fs"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "github.com/gofiber/fiber/v2"
  11. "github.com/gofiber/fiber/v2/utils"
  12. )
  13. // Config defines the config for middleware.
  14. type Config struct {
  15. // Next defines a function to skip this middleware when returned true.
  16. //
  17. // Optional. Default: nil
  18. Next func(c *fiber.Ctx) bool
  19. // Root is a FileSystem that provides access
  20. // to a collection of files and directories.
  21. //
  22. // Required. Default: nil
  23. Root http.FileSystem `json:"-"`
  24. // PathPrefix defines a prefix to be added to a filepath when
  25. // reading a file from the FileSystem.
  26. //
  27. // Use when using Go 1.16 embed.FS
  28. //
  29. // Optional. Default ""
  30. PathPrefix string `json:"path_prefix"`
  31. // Enable directory browsing.
  32. //
  33. // Optional. Default: false
  34. Browse bool `json:"browse"`
  35. // Index file for serving a directory.
  36. //
  37. // Optional. Default: "index.html"
  38. Index string `json:"index"`
  39. // The value for the Cache-Control HTTP-header
  40. // that is set on the file response. MaxAge is defined in seconds.
  41. //
  42. // Optional. Default value 0.
  43. MaxAge int `json:"max_age"`
  44. // File to return if path is not found. Useful for SPA's.
  45. //
  46. // Optional. Default: ""
  47. NotFoundFile string `json:"not_found_file"`
  48. // The value for the Content-Type HTTP-header
  49. // that is set on the file response
  50. //
  51. // Optional. Default: ""
  52. ContentTypeCharset string `json:"content_type_charset"`
  53. }
  54. // ConfigDefault is the default config
  55. var ConfigDefault = Config{
  56. Next: nil,
  57. Root: nil,
  58. PathPrefix: "",
  59. Browse: false,
  60. Index: "/index.html",
  61. MaxAge: 0,
  62. ContentTypeCharset: "",
  63. }
  64. // New creates a new middleware handler.
  65. //
  66. // filesystem does not handle url encoded values (for example spaces)
  67. // on it's own. If you need that functionality, set "UnescapePath"
  68. // in fiber.Config
  69. func New(config ...Config) fiber.Handler {
  70. // Set default config
  71. cfg := ConfigDefault
  72. // Override config if provided
  73. if len(config) > 0 {
  74. cfg = config[0]
  75. // Set default values
  76. if cfg.Index == "" {
  77. cfg.Index = ConfigDefault.Index
  78. }
  79. if !strings.HasPrefix(cfg.Index, "/") {
  80. cfg.Index = "/" + cfg.Index
  81. }
  82. if cfg.NotFoundFile != "" && !strings.HasPrefix(cfg.NotFoundFile, "/") {
  83. cfg.NotFoundFile = "/" + cfg.NotFoundFile
  84. }
  85. }
  86. if cfg.Root == nil {
  87. panic("filesystem: Root cannot be nil")
  88. }
  89. if cfg.PathPrefix != "" && !strings.HasPrefix(cfg.PathPrefix, "/") {
  90. cfg.PathPrefix = "/" + cfg.PathPrefix
  91. }
  92. var once sync.Once
  93. var prefix string
  94. cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge)
  95. // Return new handler
  96. return func(c *fiber.Ctx) error {
  97. // Don't execute middleware if Next returns true
  98. if cfg.Next != nil && cfg.Next(c) {
  99. return c.Next()
  100. }
  101. method := c.Method()
  102. // We only serve static assets on GET or HEAD methods
  103. if method != fiber.MethodGet && method != fiber.MethodHead {
  104. return c.Next()
  105. }
  106. // Set prefix once
  107. once.Do(func() {
  108. prefix = c.Route().Path
  109. })
  110. // Strip prefix
  111. path := strings.TrimPrefix(c.Path(), prefix)
  112. if !strings.HasPrefix(path, "/") {
  113. path = "/" + path
  114. }
  115. // Add PathPrefix
  116. if cfg.PathPrefix != "" {
  117. // PathPrefix already has a "/" prefix
  118. path = cfg.PathPrefix + path
  119. }
  120. if len(path) > 1 {
  121. path = utils.TrimRight(path, '/')
  122. }
  123. file, err := cfg.Root.Open(path)
  124. if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" {
  125. file, err = cfg.Root.Open(cfg.NotFoundFile)
  126. }
  127. if err != nil {
  128. if errors.Is(err, fs.ErrNotExist) {
  129. return c.Status(fiber.StatusNotFound).Next()
  130. }
  131. return fmt.Errorf("failed to open: %w", err)
  132. }
  133. stat, err := file.Stat()
  134. if err != nil {
  135. return fmt.Errorf("failed to stat: %w", err)
  136. }
  137. // Serve index if path is directory
  138. if stat.IsDir() {
  139. indexPath := utils.TrimRight(path, '/') + cfg.Index
  140. index, err := cfg.Root.Open(indexPath)
  141. if err == nil {
  142. indexStat, err := index.Stat()
  143. if err == nil {
  144. file = index
  145. stat = indexStat
  146. }
  147. }
  148. }
  149. // Browse directory if no index found and browsing is enabled
  150. if stat.IsDir() {
  151. if cfg.Browse {
  152. return dirList(c, file)
  153. }
  154. return fiber.ErrForbidden
  155. }
  156. c.Status(fiber.StatusOK)
  157. modTime := stat.ModTime()
  158. contentLength := int(stat.Size())
  159. // Set Content Type header
  160. if cfg.ContentTypeCharset == "" {
  161. c.Type(getFileExtension(stat.Name()))
  162. } else {
  163. c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset)
  164. }
  165. // Set Last Modified header
  166. if !modTime.IsZero() {
  167. c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
  168. }
  169. if method == fiber.MethodGet {
  170. if cfg.MaxAge > 0 {
  171. c.Set(fiber.HeaderCacheControl, cacheControlStr)
  172. }
  173. c.Response().SetBodyStream(file, contentLength)
  174. return nil
  175. }
  176. if method == fiber.MethodHead {
  177. c.Request().ResetBody()
  178. // Fasthttp should skipbody by default if HEAD?
  179. c.Response().SkipBody = true
  180. c.Response().Header.SetContentLength(contentLength)
  181. if err := file.Close(); err != nil {
  182. return fmt.Errorf("failed to close: %w", err)
  183. }
  184. return nil
  185. }
  186. return c.Next()
  187. }
  188. }
  189. // SendFile serves a file from an HTTP file system at the specified path.
  190. // It handles content serving, sets appropriate headers, and returns errors when needed.
  191. // Usage: err := SendFile(ctx, fs, "/path/to/file.txt")
  192. func SendFile(c *fiber.Ctx, filesystem http.FileSystem, path string) error {
  193. file, err := filesystem.Open(path)
  194. if err != nil {
  195. if errors.Is(err, fs.ErrNotExist) {
  196. return fiber.ErrNotFound
  197. }
  198. return fmt.Errorf("failed to open: %w", err)
  199. }
  200. stat, err := file.Stat()
  201. if err != nil {
  202. return fmt.Errorf("failed to stat: %w", err)
  203. }
  204. // Serve index if path is directory
  205. if stat.IsDir() {
  206. indexPath := utils.TrimRight(path, '/') + ConfigDefault.Index
  207. index, err := filesystem.Open(indexPath)
  208. if err == nil {
  209. indexStat, err := index.Stat()
  210. if err == nil {
  211. file = index
  212. stat = indexStat
  213. }
  214. }
  215. }
  216. // Return forbidden if no index found
  217. if stat.IsDir() {
  218. return fiber.ErrForbidden
  219. }
  220. c.Status(fiber.StatusOK)
  221. modTime := stat.ModTime()
  222. contentLength := int(stat.Size())
  223. // Set Content Type header
  224. c.Type(getFileExtension(stat.Name()))
  225. // Set Last Modified header
  226. if !modTime.IsZero() {
  227. c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
  228. }
  229. method := c.Method()
  230. if method == fiber.MethodGet {
  231. c.Response().SetBodyStream(file, contentLength)
  232. return nil
  233. }
  234. if method == fiber.MethodHead {
  235. c.Request().ResetBody()
  236. // Fasthttp should skipbody by default if HEAD?
  237. c.Response().SkipBody = true
  238. c.Response().Header.SetContentLength(contentLength)
  239. if err := file.Close(); err != nil {
  240. return fmt.Errorf("failed to close: %w", err)
  241. }
  242. return nil
  243. }
  244. return nil
  245. }