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