From 2002d60fbe0cbc0b74bfcc29305d018db1564d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Thu, 5 Oct 2023 22:23:29 -0700 Subject: [PATCH] Add new API endpoint /icons/{iconID} --- client/client.go | 18 +++++++++++++++++- internal/api/api.go | 3 ++- internal/api/icon.go | 23 ++++++++++++++++++++++- internal/model/icon.go | 2 +- internal/storage/icon.go | 4 ++-- internal/tests/feed_test.go | 22 ++++++++++++++++++---- 6 files changed, 62 insertions(+), 10 deletions(-) diff --git a/client/client.go b/client/client.go index d9d7fa8c..496eb50f 100644 --- a/client/client.go +++ b/client/client.go @@ -496,7 +496,7 @@ func (c *Client) SaveEntry(entryID int64) error { return err } -// FetchCounters +// FetchCounters fetches feed counters. func (c *Client) FetchCounters() (*FeedCounters, error) { body, err := c.request.Get("/v1/feeds/counters") if err != nil { @@ -518,6 +518,22 @@ func (c *Client) FlushHistory() error { return err } +// Icon fetches a feed icon. +func (c *Client) Icon(iconID int64) (*FeedIcon, error) { + body, err := c.request.Get(fmt.Sprintf("/v1/icons/%d", iconID)) + if err != nil { + return nil, err + } + defer body.Close() + + var feedIcon *FeedIcon + if err := json.NewDecoder(body).Decode(&feedIcon); err != nil { + return nil, fmt.Errorf("miniflux: response error (%v)", err) + } + + return feedIcon, nil +} + func buildFilterQueryString(path string, filter *Filter) string { if filter != nil { values := url.Values{} diff --git a/internal/api/api.go b/internal/api/api.go index 2da998b5..e00e8272 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -54,7 +54,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods(http.MethodGet) sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut) sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete) - sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet) + sr.HandleFunc("/feeds/{feedID}/icon", handler.getIconByFeedID).Methods(http.MethodGet) sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut) sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet) sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost) @@ -67,4 +67,5 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { sr.HandleFunc("/entries/{entryID}/save", handler.saveEntry).Methods(http.MethodPost) sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet) sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete) + sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet) } diff --git a/internal/api/icon.go b/internal/api/icon.go index 11cbbfa6..84db8652 100644 --- a/internal/api/icon.go +++ b/internal/api/icon.go @@ -10,7 +10,7 @@ import ( "miniflux.app/v2/internal/http/response/json" ) -func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) { +func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") if !h.store.HasIcon(feedID) { @@ -35,3 +35,24 @@ func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) { Data: icon.DataURL(), }) } + +func (h *handler) getIconByIconID(w http.ResponseWriter, r *http.Request) { + iconID := request.RouteInt64Param(r, "iconID") + + icon, err := h.store.IconByID(iconID) + if err != nil { + json.ServerError(w, r, err) + return + } + + if icon == nil { + json.NotFound(w, r) + return + } + + json.OK(w, r, &feedIconResponse{ + ID: icon.ID, + MimeType: icon.MimeType, + Data: icon.DataURL(), + }) +} diff --git a/internal/model/icon.go b/internal/model/icon.go index c02e5767..7a38b75c 100644 --- a/internal/model/icon.go +++ b/internal/model/icon.go @@ -13,7 +13,7 @@ type Icon struct { ID int64 `json:"id"` Hash string `json:"hash"` MimeType string `json:"mime_type"` - Content []byte `json:"content"` + Content []byte `json:"-"` } // DataURL returns the data URL of the icon. diff --git a/internal/storage/icon.go b/internal/storage/icon.go index dc04f657..bd407d92 100644 --- a/internal/storage/icon.go +++ b/internal/storage/icon.go @@ -27,7 +27,7 @@ func (s *Storage) IconByID(iconID int64) (*model.Icon, error) { if err == sql.ErrNoRows { return nil, nil } else if err != nil { - return nil, fmt.Errorf("store: unable to fetch icon by hash: %v", err) + return nil, fmt.Errorf("store: unable to fetch icon #%d: %w", iconID, err) } return &icon, nil @@ -63,7 +63,7 @@ func (s *Storage) IconByHash(icon *model.Icon) error { if err == sql.ErrNoRows { return nil } else if err != nil { - return fmt.Errorf(`store: unable to fetch icon by hash: %v`, err) + return fmt.Errorf(`store: unable to fetch icon by hash %q: %v`, icon.Hash, err) } return nil diff --git a/internal/tests/feed_test.go b/internal/tests/feed_test.go index cbcaf4c0..bf799cec 100644 --- a/internal/tests/feed_test.go +++ b/internal/tests/feed_test.go @@ -762,14 +762,28 @@ func TestGetFeedIcon(t *testing.T) { } if feedIcon.ID == 0 { - t.Fatalf(`Invalid feed icon ID, got "%v"`, feedIcon.ID) + t.Fatalf(`Invalid feed icon ID, got "%d"`, feedIcon.ID) } - if feedIcon.MimeType != "image/x-icon" { - t.Fatalf(`Invalid feed icon mime type, got "%v" instead of "%v"`, feedIcon.MimeType, "image/x-icon") + expectedMimeType := "image/x-icon" + if feedIcon.MimeType != expectedMimeType { + t.Fatalf(`Invalid feed icon mime type, got %q instead of %q`, feedIcon.MimeType, expectedMimeType) } - if !strings.Contains(feedIcon.Data, "image/x-icon") { + if !strings.HasPrefix(feedIcon.Data, expectedMimeType) { + t.Fatalf(`Invalid feed icon data, got "%v"`, feedIcon.Data) + } + + feedIcon, err = client.Icon(feedIcon.ID) + if err != nil { + t.Fatal(err) + } + + if feedIcon.MimeType != expectedMimeType { + t.Fatalf(`Invalid feed icon mime type, got %q instead of %q`, feedIcon.MimeType, expectedMimeType) + } + + if !strings.HasPrefix(feedIcon.Data, expectedMimeType) { t.Fatalf(`Invalid feed icon data, got "%v"`, feedIcon.Data) } }