From 3397ab61696fd016a191cf523a1c167187a3f40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20=C5=9Awi=C4=99cki?= Date: Sun, 10 Dec 2023 22:56:40 +0100 Subject: [PATCH] Add basic ETag support to cinodefs web interface --- pkg/cinodefs/httphandler/http.go | 19 +++++++++++++ pkg/cinodefs/httphandler/http_test.go | 40 +++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/pkg/cinodefs/httphandler/http.go b/pkg/cinodefs/httphandler/http.go index 0a1d4c1..7af70f7 100644 --- a/pkg/cinodefs/httphandler/http.go +++ b/pkg/cinodefs/httphandler/http.go @@ -17,6 +17,7 @@ limitations under the License. package httphandler import ( + "crypto/sha256" "errors" "fmt" "io" @@ -80,6 +81,11 @@ func (h *Handler) serveGet(w http.ResponseWriter, r *http.Request, log *slog.Log return } + if h.handleEtag(w, r, fileEP, log) { + // Client ETag matches, can optimize out the data + return + } + rc, err := h.FS.OpenEntrypointData(r.Context(), fileEP) if h.handleHttpError(err, w, log, "Error opening file") { return @@ -102,3 +108,16 @@ func (h *Handler) handleHttpError(err error, w http.ResponseWriter, log *slog.Lo } return false } + +func (h *Handler) handleEtag(w http.ResponseWriter, r *http.Request, ep *cinodefs.Entrypoint, log *slog.Logger) bool { + currentEtag := fmt.Sprintf("\"%X\"", sha256.Sum256(ep.Bytes())) + + if strings.Contains(r.Header.Get("If-None-Match"), currentEtag) { + log.Debug("Valid ETag found, sending 304 Not Modified") + w.WriteHeader(http.StatusNotModified) + return true + } + + w.Header().Set("ETag", currentEtag) + return false +} diff --git a/pkg/cinodefs/httphandler/http_test.go b/pkg/cinodefs/httphandler/http_test.go index 25b5f9f..61046b7 100644 --- a/pkg/cinodefs/httphandler/http_test.go +++ b/pkg/cinodefs/httphandler/http_test.go @@ -96,15 +96,27 @@ func (s *HandlerTestSuite) setEntry(t *testing.T, data string, path ...string) { require.NoError(t, err) } -func (s *HandlerTestSuite) getEntry(t *testing.T, path string) (string, string, int) { - resp, err := http.Get(s.server.URL + path) +func (s *HandlerTestSuite) getEntryETag(t *testing.T, path, etag string) (string, string, string, int) { + req, err := http.NewRequest(http.MethodGet, s.server.URL+path, nil) + require.NoError(t, err) + + if etag != "" { + req.Header.Set("If-None-Match", etag) + } + + resp, err := http.DefaultClient.Do(req) require.NoError(t, err) defer resp.Body.Close() data, err := io.ReadAll(resp.Body) require.NoError(t, err) - return string(data), resp.Header.Get("content-type"), resp.StatusCode + return string(data), resp.Header.Get("content-type"), resp.Header.Get("ETag"), resp.StatusCode +} + +func (s *HandlerTestSuite) getEntry(t *testing.T, path string) (string, string, int) { + data, contentType, _, code := s.getEntryETag(t, path, "") + return data, contentType, code } func (s *HandlerTestSuite) getData(t *testing.T, path string) string { @@ -119,6 +131,28 @@ func (s *HandlerTestSuite) TestSuccessfulFileDownload() { require.Equal(s.T(), "hello", readBack) } +func (s *HandlerTestSuite) TestEtag() { + s.setEntry(s.T(), "hello", "file.txt") + + readBack, _, etag, code := s.getEntryETag(s.T(), "/file.txt", "") + require.NotEmpty(s.T(), etag) + require.Greater(s.T(), len(etag), 10) + require.Equal(s.T(), http.StatusOK, code) + require.Equal(s.T(), "hello", readBack) + + readBack, _, _, code = s.getEntryETag(s.T(), "/file.txt", etag) + require.Equal(s.T(), http.StatusNotModified, code) + require.Empty(s.T(), readBack) + + s.setEntry(s.T(), "updated", "file.txt") + + readBack, _, etag2, code := s.getEntryETag(s.T(), "/file.txt", etag) + require.Equal(s.T(), http.StatusOK, code) + require.Greater(s.T(), len(etag2), 10) + require.NotEqual(s.T(), etag, etag2) + require.Equal(s.T(), "updated", readBack) +} + func (s *HandlerTestSuite) TestNonGetRequest() { t := s.T() resp, err := http.Post(s.server.URL, "text/plain", strings.NewReader("Hello world!"))