From 1f78d7279553e6f3df19659a8143c9bf10a9a500 Mon Sep 17 00:00:00 2001 From: Doychin Atanasov Date: Sun, 19 May 2024 19:38:44 +0300 Subject: [PATCH] Support HEAD requests for artwork endpoints --- src/webserver/apiv1_endpoints.go | 8 +++++-- src/webserver/handler_album_artwork.go | 11 ++++++++- src/webserver/handler_album_artwork_test.go | 25 +++++++++++++++++++-- src/webserver/handler_artist_images.go | 11 ++++++++- src/webserver/handler_artist_images_test.go | 24 ++++++++++++++++++-- src/webserver/subsonic/index.go | 2 +- 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/webserver/apiv1_endpoints.go b/src/webserver/apiv1_endpoints.go index 7de30c9..8fb701a 100644 --- a/src/webserver/apiv1_endpoints.go +++ b/src/webserver/apiv1_endpoints.go @@ -19,12 +19,16 @@ const ( // It is an uri_path => list of HTTP methods map. var APIv1Methods map[string][]string = map[string][]string{ APIv1EndpointFile: {http.MethodGet}, - APIv1EndpointAlbumArtwork: {http.MethodGet, http.MethodPut, http.MethodDelete}, APIv1EndpointDownloadAlbum: {http.MethodGet}, - APIv1EndpointArtistImage: {http.MethodGet, http.MethodPut, http.MethodDelete}, APIv1EndpointBrowse: {http.MethodGet}, APIv1EndpointSearchWithPath: {http.MethodGet}, APIv1EndpointSearch: {http.MethodGet}, APIv1EndpointLoginToken: {http.MethodPost}, APIv1EndpointRegisterToken: {http.MethodPost}, + APIv1EndpointArtistImage: { + http.MethodGet, http.MethodHead, http.MethodPut, http.MethodDelete, + }, + APIv1EndpointAlbumArtwork: { + http.MethodGet, http.MethodHead, http.MethodPut, http.MethodDelete, + }, } diff --git a/src/webserver/handler_album_artwork.go b/src/webserver/handler_album_artwork.go index 0170ec8..a2991e7 100644 --- a/src/webserver/handler_album_artwork.go +++ b/src/webserver/handler_album_artwork.go @@ -77,6 +77,10 @@ func (aah AlbumArtworkHandler) Find( if err == library.ErrArtworkNotFound || os.IsNotExist(err) { writer.WriteHeader(http.StatusNotFound) + if req.Method == http.MethodHead { + return nil + } + notFoundImage, err := aah.rootFS.Open(aah.notFoundPath) if err == nil { defer notFoundImage.Close() @@ -96,8 +100,13 @@ func (aah AlbumArtworkHandler) Find( defer imgReader.Close() writer.Header().Set("Cache-Control", "max-age=604800") - _, err = io.Copy(writer, imgReader) + if req.Method == http.MethodHead { + n, _ := io.Copy(io.Discard, imgReader) + writer.Header().Set("Content-Length", strconv.FormatInt(n, 10)) + return nil + } + _, err = io.Copy(writer, imgReader) if err != nil { log.Printf("еrror sending HTTP data for artwork %d: %s", id, err) } diff --git a/src/webserver/handler_album_artwork_test.go b/src/webserver/handler_album_artwork_test.go index 9e69741..b514e52 100644 --- a/src/webserver/handler_album_artwork_test.go +++ b/src/webserver/handler_album_artwork_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "strings" "testing" "testing/fstest" @@ -19,9 +20,9 @@ import ( "github.com/ironsmile/euterpe/src/webserver" ) -// TestAlbumArtworkHandlerGET makes sure the artwork handler is processing the HTTP +// TestAlbumArtworkHandler makes sure the artwork handler is processing the HTTP // request correctly and sending the expected arguments to its artwork manager. -func TestAlbumArtworkHandlerGET(t *testing.T) { +func TestAlbumArtworkHandler(t *testing.T) { imgBytesOriginal := []byte("album 321 image original") imgBytesSmall := []byte("album 321 image small") @@ -155,6 +156,26 @@ func TestAlbumArtworkHandlerGET(t *testing.T) { respString, ) } + + // Test the HEAD request. It should return the Content-Length and 200 for + // images which are present. + resp = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodHead, "/v1/album/321/artwork?size=small", nil) + handler.ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Errorf("HEAD small: expected code %d but got %d", http.StatusOK, resp.Code) + } + + clExpected := strconv.FormatInt(int64(len(imgBytesSmall)), 10) + if cl := resp.Result().Header.Get("Content-Length"); clExpected != cl { + t.Errorf("HEAD small: expected Content-Length `%s` but got `%s`", + clExpected, cl) + } + respBodySize, err := io.Copy(io.Discard, resp.Result().Body) + if err != nil || respBodySize != 0 { + t.Errorf("HEAD response should have empty body") + } } // TestAlbumArtworkHandlerDELETE tests what happens when artwork is removed. diff --git a/src/webserver/handler_artist_images.go b/src/webserver/handler_artist_images.go index cdbcea7..feb0650 100644 --- a/src/webserver/handler_artist_images.go +++ b/src/webserver/handler_artist_images.go @@ -74,6 +74,10 @@ func (aih ArtstImageHandler) Find( if err == library.ErrArtworkNotFound || os.IsNotExist(err) { writer.WriteHeader(http.StatusNotFound) + if req.Method == http.MethodHead { + return nil + } + fmt.Fprintln(writer, "404 image not found") return nil } @@ -86,8 +90,13 @@ func (aih ArtstImageHandler) Find( defer imgReader.Close() writer.Header().Set("Cache-Control", "max-age=604800") - _, err = io.Copy(writer, imgReader) + if req.Method == http.MethodHead { + n, _ := io.Copy(io.Discard, imgReader) + writer.Header().Set("Content-Length", strconv.FormatInt(n, 10)) + return nil + } + _, err = io.Copy(writer, imgReader) if err != nil { log.Printf("еrror sending HTTP data for artwork %d: %s", id, err) } diff --git a/src/webserver/handler_artist_images_test.go b/src/webserver/handler_artist_images_test.go index 25ca799..3b429da 100644 --- a/src/webserver/handler_artist_images_test.go +++ b/src/webserver/handler_artist_images_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "strings" "testing" @@ -17,9 +18,9 @@ import ( "github.com/ironsmile/euterpe/src/webserver" ) -// TestArtstImageHandlerGET checks that the HTTP handler for GET requests is parsing +// TestArtstImageHandler checks that the HTTP handler for GET requests is parsing // its arguments as expected and responds with the image found by its image manager. -func TestArtstImageHandlerGET(t *testing.T) { +func TestArtstImageHandler(t *testing.T) { imgBytesOriginal := []byte("artist 321 image original") imgBytesSmall := []byte("artist 321 image small") @@ -128,6 +129,25 @@ func TestArtstImageHandlerGET(t *testing.T) { respString, ) } + + // Test the HEDE reqeuest. + resp = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodHead, "/v1/artist/321/image?size=small", nil) + handler.ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Errorf("HEAD small: expected code %d but got %d", http.StatusOK, resp.Code) + } + + clExpected := strconv.FormatInt(int64(len(imgBytesSmall)), 10) + if cl := resp.Result().Header.Get("Content-Length"); clExpected != cl { + t.Errorf("HEAD Dsmall: expected image `%s` but got `%s`", + clExpected, cl) + } + respBodySize, err := io.Copy(io.Discard, resp.Result().Body) + if err != nil || respBodySize != 0 { + t.Errorf("HEAD response should have empty body") + } } // TestArtstImageHandlerDELETE tests what happens when artwork is removed. diff --git a/src/webserver/subsonic/index.go b/src/webserver/subsonic/index.go index afb8073..cc64cdf 100644 --- a/src/webserver/subsonic/index.go +++ b/src/webserver/subsonic/index.go @@ -89,7 +89,7 @@ func (s *subsonic) initRouter() { setUpHandler("/getArtist", s.getArtist) setUpHandler("/getArtistInfo", s.getArtistInfo) setUpHandler("/getArtistInfo2", s.getArtistInfo2) - setUpHandler("/getCoverArt", s.getCoverArt) + setUpHandler("/getCoverArt", s.getCoverArt, "GET", "HEAD") setUpHandler("/stream", s.stream, "GET", "HEAD") setUpHandler("/download", s.stream, "GET", "HEAD") setUpHandler("/getSong", s.getSong)