diff --git a/middleware/fileserver.go b/middleware/fileserver.go index 8d9dabcc..cba4da09 100644 --- a/middleware/fileserver.go +++ b/middleware/fileserver.go @@ -1,6 +1,7 @@ package middleware import ( + "fmt" "net/http" "os" "path" @@ -128,6 +129,10 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st return http.StatusNotFound, nil } + // Add ETag header + e := fmt.Sprintf(`W/"%x-%x"`, d.ModTime().Unix(), d.Size()) + w.Header().Set("ETag", e) + // Note: Errors generated by ServeContent are written immediately // to the response. This usually only happens if seeking fails (rare). http.ServeContent(w, r, d.Name(), d.ModTime(), f) diff --git a/middleware/fileserver_test.go b/middleware/fileserver_test.go index c9011238..96292bdc 100644 --- a/middleware/fileserver_test.go +++ b/middleware/fileserver_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) var testDir = filepath.Join(os.TempDir(), "caddy_testdir") @@ -44,6 +45,7 @@ func TestServeHTTP(t *testing.T) { expectedStatus int expectedBodyContent string + expectedEtag string }{ // Test 0 - access without any path { @@ -60,12 +62,14 @@ func TestServeHTTP(t *testing.T) { url: "https://foo/file1.html", expectedStatus: http.StatusOK, expectedBodyContent: testFiles["file1.html"], + expectedEtag: `W/"1e240-13"`, }, // Test 3 - access folder with index file with trailing slash { url: "https://foo/dirwithindex/", expectedStatus: http.StatusOK, expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")], + expectedEtag: `W/"1e240-20"`, }, // Test 4 - access folder with index file without trailing slash { @@ -105,6 +109,7 @@ func TestServeHTTP(t *testing.T) { url: "https://foo/dirwithindex/index.html", expectedStatus: http.StatusOK, expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")], + expectedEtag: `W/"1e240-20"`, }, // Test 11 - send a request with query params { @@ -143,6 +148,7 @@ func TestServeHTTP(t *testing.T) { responseRecorder := httptest.NewRecorder() request, err := http.NewRequest("GET", test.url, strings.NewReader("")) status, err := fileserver.ServeHTTP(responseRecorder, request) + etag := responseRecorder.Header().Get("Etag") // check if error matches expectations if err != nil { @@ -154,6 +160,11 @@ func TestServeHTTP(t *testing.T) { t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) } + // check etag + if test.expectedEtag != etag { + t.Errorf(getTestPrefix(i)+"Expected Etag header %d, found %d", test.expectedEtag, etag) + } + // check body content if !strings.Contains(responseRecorder.Body.String(), test.expectedBodyContent) { t.Errorf(getTestPrefix(i)+"Expected body to contain %q, found %q", test.expectedBodyContent, responseRecorder.Body.String()) @@ -173,6 +184,8 @@ func beforeServeHTTPTest(t *testing.T) { } } + fixedTime := time.Unix(123456, 0) + for relFile, fileContent := range testFiles { absFile := filepath.Join(testDir, relFile) @@ -197,6 +210,12 @@ func beforeServeHTTPTest(t *testing.T) { return } f.Close() + + // and set the last modified time + err = os.Chtimes(absFile, fixedTime, fixedTime) + if err != nil { + t.Fatalf("Failed to set file time to %s. Error was: %v", fixedTime, err) + } } }