Search code examples
gogo-gin

How to serve files from dynamic subdirectories using Gin?


I have a directory structure like:

- uploads
    - rooms
        - 1
            - a.jpg
            - b.jpg
        - 2
            - a.jpg
            - c.jpg

The subdirectories in the rooms directory (1, 2, ...) are added dynamically. I want to create an endpoint in Gin that can serve all these files when given the exact path but not expose the list of files/folders.

For example, http://localhost:8080/media/1/a.jpg should return an image. But http://localhost:8080/media/1 should return 404.


Solution

  • Use gin.Dir with listDirectory == false. It works on subdirectories too.

    Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally in router.Static(). If listDirectory == true, then it works the same as http.Dir() otherwise it returns a filesystem that prevents http.FileServer() to list the directory files.

    Example usage:

    Local filesystem

    /
    |_ static
       |_ 1
          |_ foo.jpg
          |_ bar.jpg
    |_ main.go
    |_ go.mod
    |_ go.sum
    

    main.go

    r := gin.New()
    r.StaticFS("/foo", gin.Dir("static", false))
    

    HTTP request

    GET: <host:port>/foo/1/foo.jpg
    

    As an alternative, you can also declare a route with path params, and just serve the content:

        r.GET("/media/:dir/*asset", func(c *gin.Context) {
            dir := c.Param("dir")
            asset := c.Param("asset")
            if strings.TrimPrefix(asset, "/") == "" {
                c.AbortWithStatus(http.StatusNotFound)
                return
            }
            fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+asset)))
            c.File(fullName)
        })
    

    By checking if the trimmed wildcard path *asset is empty, you prevent queries to, e.g. /media/1/ (with final slash) to list the directory. Instead /media/1 (without final slash) doesn't match any route (it should automatically redirect to /media/1/).