diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 4a081e0c..4cbc4554 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml
index f3927b3d..5b818a9e 100644
--- a/.github/workflows/gin.yml
+++ b/.github/workflows/gin.yml
@@ -17,7 +17,7 @@ jobs:
with:
go-version: '^1.16'
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v2
with:
@@ -48,7 +48,7 @@ jobs:
go-version: ${{ matrix.go }}
- name: Checkout Code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
diff --git a/README.md b/README.md
index b75df63f..4b4d236b 100644
--- a/README.md
+++ b/README.md
@@ -385,7 +385,7 @@ func main() {
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Single file
- file, _ := c.FormFile("Filename")
+ file, _ := c.FormFile("file")
log.Println(file.Filename)
// Upload the file to specific dst.
@@ -417,7 +417,7 @@ func main() {
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
- files := form.File["Filename[]"]
+ files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
@@ -513,6 +513,7 @@ func main() {
// nested group
testing := authorized.Group("testing")
+ // visit 0.0.0.0:8080/testing/analytics
testing.GET("/analytics", analyticsEndpoint)
}
@@ -1243,7 +1244,8 @@ func main() {
router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")
-
+ router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system"))
+
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
diff --git a/context.go b/context.go
index d69df70b..48d75506 100644
--- a/context.go
+++ b/context.go
@@ -6,7 +6,6 @@ package gin
import (
"errors"
- "fmt"
"io"
"io/ioutil"
"log"
@@ -1018,7 +1017,11 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
// FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) {
- c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+ if isASCII(filename) {
+ c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
+ } else {
+ c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename))
+ }
http.ServeFile(c.Writer, c.Request, filepath)
}
diff --git a/context_test.go b/context_test.go
index 9e02aede..4eed164a 100644
--- a/context_test.go
+++ b/context_test.go
@@ -15,6 +15,7 @@ import (
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"reflect"
"strings"
@@ -1033,6 +1034,19 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
}
+func TestContextRenderUTF8Attachment(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ newFilename := "newπ§‘_filename.go"
+
+ c.Request, _ = http.NewRequest("GET", "/", nil)
+ c.FileAttachment("./gin.go", newFilename)
+
+ assert.Equal(t, 200, w.Code)
+ assert.Contains(t, w.Body.String(), "func New() *Engine {")
+ assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition"))
+}
+
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) {
diff --git a/gin_test.go b/gin_test.go
index 629a109b..ae1762ef 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -90,7 +90,12 @@ func TestH2c(t *testing.T) {
r.GET("/", func(c *Context) {
c.String(200, "
Hello world
")
})
- go http.Serve(ln, r.Handler())
+ go func() {
+ err := http.Serve(ln, r.Handler())
+ if err != nil {
+ fmt.Println(err)
+ }
+ }()
defer ln.Close()
url := "http://" + ln.Addr().String() + "/"
diff --git a/go.mod b/go.mod
index b59e5907..8087227e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.13
require (
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.10.0
- github.com/goccy/go-json v0.9.0
+ github.com/goccy/go-json v0.9.5
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.14
github.com/stretchr/testify v1.7.0
diff --git a/go.sum b/go.sum
index c01ba524..7f892453 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/goccy/go-json v0.9.0 h1:2flW7bkbrRgU8VuDi0WXDqTmPimjv1thfxkPe8sug+8=
-github.com/goccy/go-json v0.9.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.5 h1:ooSMW526ZjK+EaL5elrSyN2EzIfi/3V0m4+HJEDYLik=
+github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
diff --git a/routergroup.go b/routergroup.go
index 27d7aad6..3fba3a91 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -12,7 +12,7 @@ import (
)
var (
- // reg match english letters for http method name
+ // regEnLetter matches english letters for http method name
regEnLetter = regexp.MustCompile("^[A-Z]+$")
// anyMethods for RouterGroup Any method
@@ -44,6 +44,7 @@ type IRoutes interface {
HEAD(string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
+ StaticFileFS(string, string, http.FileSystem) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
@@ -153,12 +154,24 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRou
// StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
+ return group.staticFileHandler(relativePath, func(c *Context) {
+ c.File(filepath)
+ })
+}
+
+// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
+// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
+// Gin by default user: gin.Dir()
+func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
+ return group.staticFileHandler(relativePath, func(c *Context) {
+ c.FileFromFS(filepath, fs)
+ })
+}
+
+func (group *RouterGroup) staticFileHandler(relativePath string, handler HandlerFunc) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static file")
}
- handler := func(c *Context) {
- c.File(filepath)
- }
group.GET(relativePath, handler)
group.HEAD(relativePath, handler)
return group.returnObj()
diff --git a/routergroup_test.go b/routergroup_test.go
index 232f595b..c1fad3a9 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -111,6 +111,17 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
})
}
+func TestRouterGroupInvalidStaticFileFS(t *testing.T) {
+ router := New()
+ assert.Panics(t, func() {
+ router.StaticFileFS("/path/:param", "favicon.ico", Dir(".", false))
+ })
+
+ assert.Panics(t, func() {
+ router.StaticFileFS("/path/*param", "favicon.ico", Dir(".", false))
+ })
+}
+
func TestRouterGroupTooManyHandlers(t *testing.T) {
const (
panicValue = "too many handlers"
@@ -177,6 +188,7 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.HEAD("/", handler))
assert.Equal(t, r, r.StaticFile("/file", "."))
+ assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false)))
assert.Equal(t, r, r.Static("/static", "."))
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
}
diff --git a/routes_test.go b/routes_test.go
index b3f0c47b..4a0cb493 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -325,6 +325,40 @@ func TestRouteStaticFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w3.Code)
}
+// TestHandleStaticFile - ensure the static file handles properly
+func TestRouteStaticFileFS(t *testing.T) {
+ // SETUP file
+ testRoot, _ := os.Getwd()
+ f, err := ioutil.TempFile(testRoot, "")
+ if err != nil {
+ t.Error(err)
+ }
+ defer os.Remove(f.Name())
+ _, err = f.WriteString("Gin Web Framework")
+ assert.NoError(t, err)
+ f.Close()
+
+ dir, filename := filepath.Split(f.Name())
+ // SETUP gin
+ router := New()
+ router.Static("/using_static", dir)
+ router.StaticFileFS("/result_fs", filename, Dir(dir, false))
+
+ w := performRequest(router, http.MethodGet, "/using_static/"+filename)
+ w2 := performRequest(router, http.MethodGet, "/result_fs")
+
+ assert.Equal(t, w, w2)
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "Gin Web Framework", w.Body.String())
+ assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
+
+ w3 := performRequest(router, http.MethodHead, "/using_static/"+filename)
+ w4 := performRequest(router, http.MethodHead, "/result_fs")
+
+ assert.Equal(t, w3, w4)
+ assert.Equal(t, http.StatusOK, w3.Code)
+}
+
// TestHandleStaticDir - ensure the root/sub dir handles properly
func TestRouteStaticListingDir(t *testing.T) {
router := New()
diff --git a/utils.go b/utils.go
index c32f0eeb..e4599ea9 100644
--- a/utils.go
+++ b/utils.go
@@ -12,6 +12,7 @@ import (
"reflect"
"runtime"
"strings"
+ "unicode"
)
// BindKey indicates a default bind key.
@@ -151,3 +152,13 @@ func resolveAddress(addr []string) string {
panic("too many parameters")
}
}
+
+// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
+func isASCII(s string) bool {
+ for i := 0; i < len(s); i++ {
+ if s[i] > unicode.MaxASCII {
+ return false
+ }
+ }
+ return true
+}
diff --git a/utils_test.go b/utils_test.go
index b50914f2..d2a740bf 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -143,3 +143,8 @@ func TestMarshalXMLforH(t *testing.T) {
e := h.MarshalXML(enc, x)
assert.Error(t, e)
}
+
+func TestIsASCII(t *testing.T) {
+ assert.Equal(t, isASCII("test"), true)
+ assert.Equal(t, isASCII("π§‘ππππ"), false)
+}