From 5564c4319729e93e4cba98f507c36431b252e9a1 Mon Sep 17 00:00:00 2001 From: shaouai <56815047+dushaoshuai@users.noreply.github.com> Date: Sat, 10 May 2025 23:43:13 +0800 Subject: [PATCH] feat: serve files using embed.FS (#4847) --- rest/internal/fileserver/filehandler.go | 13 ++ rest/internal/fileserver/filehandler_test.go | 164 ++++++++++++++++++ rest/internal/fileserver/testdata/index.html | 1 + .../fileserver/testdata/nested/index.html | 1 + 4 files changed, 179 insertions(+) create mode 100644 rest/internal/fileserver/testdata/index.html create mode 100644 rest/internal/fileserver/testdata/nested/index.html diff --git a/rest/internal/fileserver/filehandler.go b/rest/internal/fileserver/filehandler.go index 163cdea4a..ff9ccd9c5 100644 --- a/rest/internal/fileserver/filehandler.go +++ b/rest/internal/fileserver/filehandler.go @@ -2,6 +2,7 @@ package fileserver import ( "net/http" + pathpkg "path" "strings" "sync" ) @@ -29,6 +30,18 @@ func createFileChecker(fs http.FileSystem) func(string) bool { fileChecker := make(map[string]bool) return func(path string) bool { + // Emulate http.Dir.Open’s path normalization for embed.FS.Open. + // http.FileServer redirects any request ending in "/index.html" + // to the same path without the final "index.html". + // So the path here may be empty or end with a "/". + // http.Dir.Open uses this logic to clean the path, + // correctly handling those two cases. + // embed.FS doesn’t perform this normalization, so we apply the same logic here. + path = pathpkg.Clean("/" + path)[1:] + if path == "" { + path = "." + } + lock.RLock() exist, ok := fileChecker[path] lock.RUnlock() diff --git a/rest/internal/fileserver/filehandler_test.go b/rest/internal/fileserver/filehandler_test.go index 37308f727..2b784e043 100644 --- a/rest/internal/fileserver/filehandler_test.go +++ b/rest/internal/fileserver/filehandler_test.go @@ -1,6 +1,8 @@ package fileserver import ( + "embed" + "io/fs" "net/http" "net/http/httptest" "testing" @@ -61,6 +63,46 @@ func TestMiddleware(t *testing.T) { requestPath: "/ws", expectedStatus: http.StatusAlreadyReported, }, + + // http.FileServer redirects any request ending in "/index.html" + // to the same path, without the final "index.html". + { + name: "Serve index.html", + path: "/static", + dir: "testdata", + requestPath: "/static/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Serve index.html with path with trailing slash", + path: "/static/", + dir: "testdata", + requestPath: "/static/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Serve index.html in a nested directory", + path: "/static", + dir: "testdata", + requestPath: "/static/nested/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Request index.html indirectly", + path: "/static", + dir: "testdata", + requestPath: "/static/", + expectedStatus: http.StatusOK, + expectedContent: "hello", + }, + { + name: "Request index.html in a nested directory indirectly", + path: "/static", + dir: "testdata", + requestPath: "/static/nested/", + expectedStatus: http.StatusOK, + expectedContent: "hello", + }, } for _, tt := range tests { @@ -87,6 +129,128 @@ func TestMiddleware(t *testing.T) { } } +var ( + //go:embed testdata + testdataFS embed.FS +) + +func TestMiddleware_embedFS(t *testing.T) { + tests := []struct { + name string + path string + requestPath string + expectedStatus int + expectedContent string + }{ + { + name: "Serve static file", + path: "/static", + requestPath: "/static/example.txt", + expectedStatus: http.StatusOK, + expectedContent: "1", + }, + { + name: "Path with trailing slash", + path: "/static/", + requestPath: "/static/example.txt", + expectedStatus: http.StatusOK, + expectedContent: "1", + }, + { + name: "Root path", + path: "/", + requestPath: "/example.txt", + expectedStatus: http.StatusOK, + expectedContent: "1", + }, + { + name: "Pass through non-matching path", + path: "/static/", + requestPath: "/other/path", + expectedStatus: http.StatusAlreadyReported, + }, + { + name: "Not exist file", + path: "/assets", + requestPath: "/assets/not-exist.txt", + expectedStatus: http.StatusAlreadyReported, + }, + { + name: "Not exist file in root", + path: "/", + requestPath: "/not-exist.txt", + expectedStatus: http.StatusAlreadyReported, + }, + { + name: "websocket request", + path: "/", + requestPath: "/ws", + expectedStatus: http.StatusAlreadyReported, + }, + + // http.FileServer redirects any request ending in "/index.html" + // to the same path, without the final "index.html". + { + name: "Serve index.html", + path: "/static", + requestPath: "/static/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Serve index.html with path with trailing slash", + path: "/static/", + requestPath: "/static/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Serve index.html in a nested directory", + path: "/static", + requestPath: "/static/nested/index.html", + expectedStatus: http.StatusMovedPermanently, + }, + { + name: "Request index.html indirectly", + path: "/static", + requestPath: "/static/", + expectedStatus: http.StatusOK, + expectedContent: "hello", + }, + { + name: "Request index.html in a nested directory indirectly", + path: "/static", + requestPath: "/static/nested/", + expectedStatus: http.StatusOK, + expectedContent: "hello", + }, + } + + subFS, err := fs.Sub(testdataFS, "testdata") + assert.Nil(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + middleware := Middleware(tt.path, http.FS(subFS)) + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAlreadyReported) + }) + + handlerToTest := middleware(nextHandler) + + for i := 0; i < 2; i++ { + req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil) + rr := httptest.NewRecorder() + + handlerToTest.ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + if len(tt.expectedContent) > 0 { + assert.Equal(t, tt.expectedContent, rr.Body.String()) + } + } + }) + } +} + func TestEnsureTrailingSlash(t *testing.T) { tests := []struct { input string diff --git a/rest/internal/fileserver/testdata/index.html b/rest/internal/fileserver/testdata/index.html new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/rest/internal/fileserver/testdata/index.html @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/rest/internal/fileserver/testdata/nested/index.html b/rest/internal/fileserver/testdata/nested/index.html new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/rest/internal/fileserver/testdata/nested/index.html @@ -0,0 +1 @@ +hello \ No newline at end of file