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  }