diff --git a/src/server/auth/middleware.go b/src/server/auth/middleware.go index 4dd12a7..0704622 100644 --- a/src/server/auth/middleware.go +++ b/src/server/auth/middleware.go @@ -14,7 +14,7 @@ type Middleware struct { Password string BasePath string Public []string - DB *storage.Storage + DB storage.Storage } func (m *Middleware) Handler(c *router.Context) { diff --git a/src/server/fever.go b/src/server/fever.go index 16d239b..29a0b5b 100644 --- a/src/server/fever.go +++ b/src/server/fever.go @@ -14,6 +14,7 @@ import ( "github.com/nkanaev/yarr/src/server/auth" "github.com/nkanaev/yarr/src/server/router" "github.com/nkanaev/yarr/src/storage" + "github.com/nkanaev/yarr/src/storage/model" ) type FeverGroup struct { @@ -61,7 +62,7 @@ func writeFeverJSON(c *router.Context, data map[string]any, lastRefreshed int64) c.JSON(http.StatusOK, data) } -func getLastRefreshedOnTime(feedStates []storage.FeedState) int64 { +func getLastRefreshedOnTime(feedStates []model.FeedState) int64 { var lastRefreshed int64 for _, state := range feedStates { if state.LastRefreshed.Unix() > lastRefreshed { @@ -140,7 +141,7 @@ func joinInts(values []int64) string { return result.String() } -func feedGroups(db *storage.Storage) []*FeverFeedsGroup { +func feedGroups(db storage.Storage) []*FeverFeedsGroup { feeds := db.ListFeeds() groupFeeds := make(map[int64][]int64) @@ -176,7 +177,7 @@ func (s *Server) feverGroupsHandler(c *router.Context) { func (s *Server) feverFeedsHandler(c *router.Context) { feeds := s.db.ListFeeds() states, _ := s.db.ListFeedStates() - statesMap := make(map[int64]storage.FeedState) + statesMap := make(map[int64]model.FeedState) for _, state := range states { statesMap[state.FeedID] = state } @@ -230,7 +231,7 @@ func (s *Server) feverFaviconsHandler(c *router.Context) { const listLimit = 50 func (s *Server) feverItemsHandler(c *router.Context) { - filter := storage.ItemFilter{} + filter := model.ItemFilter{} query := c.Req.URL.Query() switch { @@ -262,11 +263,11 @@ func (s *Server) feverItemsHandler(c *router.Context) { time := date.Unix() isSaved := 0 - if item.Status == storage.STARRED { + if item.Status == model.STARRED { isSaved = 1 } isRead := 0 - if item.Status == storage.READ { + if item.Status == model.READ { isRead = 1 } feverItems[i] = FeverItem{ @@ -299,10 +300,10 @@ func (s *Server) feverLinksHandler(c *router.Context) { } func (s *Server) feverUnreadItemIDsHandler(c *router.Context) { - status := storage.UNREAD + status := model.UNREAD itemIds := make([]int64, 0) - itemFilter := storage.ItemFilter{ + itemFilter := model.ItemFilter{ Status: &status, } for { @@ -322,10 +323,10 @@ func (s *Server) feverUnreadItemIDsHandler(c *router.Context) { } func (s *Server) feverSavedItemIDsHandler(c *router.Context) { - status := storage.STARRED + status := model.STARRED itemIds := make([]int64, 0) - itemFilter := storage.ItemFilter{ + itemFilter := model.ItemFilter{ Status: &status, } for { @@ -353,16 +354,16 @@ func (s *Server) feverMarkHandler(c *router.Context) { switch c.Req.Form.Get("mark") { case "item": - var status storage.ItemStatus + var status model.ItemStatus switch c.Req.Form.Get("as") { case "read": - status = storage.READ + status = model.READ case "unread": - status = storage.UNREAD + status = model.UNREAD case "saved": - status = storage.STARRED + status = model.STARRED case "unsaved": - status = storage.READ + status = model.READ default: c.Out.WriteHeader(http.StatusBadRequest) return @@ -372,7 +373,7 @@ func (s *Server) feverMarkHandler(c *router.Context) { if c.Req.Form.Get("as") != "read" { c.Out.WriteHeader(http.StatusBadRequest) } - markFilter := storage.MarkFilter{FeedID: &id} + markFilter := model.MarkFilter{FeedID: &id} x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64) if x > 0 { before := time.Unix(x, 0).UTC() @@ -383,7 +384,7 @@ func (s *Server) feverMarkHandler(c *router.Context) { if c.Req.Form.Get("as") != "read" { c.Out.WriteHeader(http.StatusBadRequest) } - markFilter := storage.MarkFilter{} + markFilter := model.MarkFilter{} if id > 0 { markFilter.FolderID = &id } diff --git a/src/server/forms.go b/src/server/forms.go index 4cc2270..a6e61dd 100644 --- a/src/server/forms.go +++ b/src/server/forms.go @@ -1,9 +1,9 @@ package server -import "github.com/nkanaev/yarr/src/storage" +import "github.com/nkanaev/yarr/src/storage/model" type ItemUpdateForm struct { - Status *storage.ItemStatus `json:"status,omitempty"` + Status *model.ItemStatus `json:"status,omitempty"` } type FolderCreateForm struct { diff --git a/src/server/routes.go b/src/server/routes.go index 764cbd8..c1785e1 100644 --- a/src/server/routes.go +++ b/src/server/routes.go @@ -20,7 +20,7 @@ import ( "github.com/nkanaev/yarr/src/server/gzip" "github.com/nkanaev/yarr/src/server/opml" "github.com/nkanaev/yarr/src/server/router" - "github.com/nkanaev/yarr/src/storage" + "github.com/nkanaev/yarr/src/storage/model" "github.com/nkanaev/yarr/src/worker" ) @@ -141,7 +141,7 @@ func (s *Server) handleFolder(c *router.Context) { c.Out.WriteHeader(http.StatusBadRequest) return } - s.db.UpdateFolder(id, storage.UpdateFolderParams{ + s.db.UpdateFolder(id, model.UpdateFolderParams{ Title: body.Title, IsExpanded: body.IsExpanded, }) @@ -248,7 +248,7 @@ func (s *Server) handleFeedList(c *router.Context) { map[string]any{"status": "multiple", "choice": result.Sources}, ) case result.Feed != nil: - feed := s.db.CreateFeed(storage.CreateFeedParams{ + feed := s.db.CreateFeed(model.CreateFeedParams{ Title: result.Feed.Title, Link: result.Feed.SiteURL, FeedLink: result.FeedLink, @@ -288,7 +288,7 @@ func (s *Server) handleFeed(c *router.Context) { c.Out.WriteHeader(http.StatusBadRequest) return } - params := storage.UpdateFeedParams{} + params := model.UpdateFeedParams{} if title, ok := body["title"]; ok { if reflect.TypeOf(title).Kind() == reflect.String { t := title.(string) @@ -297,10 +297,10 @@ func (s *Server) handleFeed(c *router.Context) { } if f_id, ok := body["folder_id"]; ok { if f_id == nil { - params.FolderID = storage.SetNullable[int64](nil) + params.FolderID = model.SetNullable[int64](nil) } else if reflect.TypeOf(f_id).Kind() == reflect.Float64 { folderId := int64(f_id.(float64)) - params.FolderID = storage.SetNullable(&folderId) + params.FolderID = model.SetNullable(&folderId) } } if link, ok := body["feed_link"]; ok { @@ -366,7 +366,7 @@ func (s *Server) handleItemList(c *router.Context) { perPage := 20 query := c.Req.URL.Query() - filter := storage.ItemFilter{} + filter := model.ItemFilter{} if folderID, err := c.QueryInt64("folder_id"); err == nil { filter.FolderID = &folderID } @@ -377,7 +377,7 @@ func (s *Server) handleItemList(c *router.Context) { filter.After = &after } if status := query.Get("status"); len(status) != 0 { - statusValue := storage.StatusValues[status] + statusValue := model.StatusValues[status] filter.Status = &statusValue } if search := query.Get("search"); len(search) != 0 { @@ -403,7 +403,7 @@ func (s *Server) handleItemList(c *router.Context) { "has_more": hasMore, }) } else if c.Req.Method == "PUT" { - filter := storage.MarkFilter{} + filter := model.MarkFilter{} if folderID, err := c.QueryInt64("folder_id"); err == nil { filter.FolderID = &folderID @@ -422,7 +422,7 @@ func (s *Server) handleSettings(c *router.Context) { if c.Req.Method == "GET" { c.JSON(http.StatusOK, s.db.GetSettings()) } else if c.Req.Method == "PUT" { - var params storage.UpdateSettingsParams + var params model.UpdateSettingsParams if err := json.NewDecoder(c.Req.Body).Decode(¶ms); err != nil { c.Out.WriteHeader(http.StatusBadRequest) return @@ -452,7 +452,7 @@ func (s *Server) handleOPMLImport(c *router.Context) { return } for _, f := range doc.Feeds { - s.db.CreateFeed(storage.CreateFeedParams{ + s.db.CreateFeed(model.CreateFeedParams{ Title: f.Title, Link: f.SiteUrl, FeedLink: f.FeedUrl, @@ -461,7 +461,7 @@ func (s *Server) handleOPMLImport(c *router.Context) { for _, f := range doc.Folders { folder := s.db.CreateFolder(f.Title) for _, ff := range f.AllFeeds() { - s.db.CreateFeed(storage.CreateFeedParams{ + s.db.CreateFeed(model.CreateFeedParams{ Title: ff.Title, Link: ff.SiteUrl, FeedLink: ff.FeedUrl, @@ -485,7 +485,7 @@ func (s *Server) handleOPMLExport(c *router.Context) { doc := opml.Folder{} - feedsByFolderID := make(map[int64][]*storage.Feed) + feedsByFolderID := make(map[int64][]*model.Feed) for _, feed := range s.db.ListFeeds() { if feed.FolderId == nil { doc.Feeds = append(doc.Feeds, opml.Feed{ diff --git a/src/server/routes_test.go b/src/server/routes_test.go index 03e6881..52ee1e0 100644 --- a/src/server/routes_test.go +++ b/src/server/routes_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/nkanaev/yarr/src/storage" + "github.com/nkanaev/yarr/src/storage/model" ) func TestStatic(t *testing.T) { @@ -79,8 +80,8 @@ func TestFeedIcons(t *testing.T) { log.SetOutput(io.Discard) db, _ := storage.New(":memory:") icon := []byte("test") - feed := db.CreateFeed(storage.CreateFeedParams{}) - db.UpdateFeed(feed.Id, storage.UpdateFeedParams{Icon: storage.SetNullable(&icon)}) + feed := db.CreateFeed(model.CreateFeedParams{}) + db.UpdateFeed(feed.Id, model.UpdateFeedParams{Icon: model.SetNullable(&icon)}) log.SetOutput(os.Stderr) recorder := httptest.NewRecorder() diff --git a/src/server/server.go b/src/server/server.go index d4b0dbb..76c8d48 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -14,7 +14,7 @@ import ( type Server struct { Addr string - db *storage.Storage + db storage.Storage worker *worker.Worker cache map[string]any cache_mutex *sync.Mutex @@ -29,7 +29,7 @@ type Server struct { KeyFile string } -func NewServer(db *storage.Storage, addr string) *Server { +func NewServer(db storage.Storage, addr string) *Server { return &Server{ db: db, Addr: addr, diff --git a/src/storage/factory/factory.go b/src/storage/storage.go similarity index 89% rename from src/storage/factory/factory.go rename to src/storage/storage.go index a709b8c..b8a890c 100644 --- a/src/storage/factory/factory.go +++ b/src/storage/storage.go @@ -1,12 +1,12 @@ -package factory +package storage import ( "github.com/nkanaev/yarr/src/storage/model" + "github.com/nkanaev/yarr/src/storage/sqlite" ) type Storage interface { Close() error - Migrate() error CountItems() int CreateFeed(params model.CreateFeedParams) *model.Feed CreateFolder(title string) *model.Folder @@ -30,3 +30,7 @@ type Storage interface { UpdateItemStatus(item_id int64, status model.ItemStatus) bool UpdateSettings(params model.UpdateSettingsParams) bool } + +func New(path string) (Storage, error) { + return sqlite.New(path) +} diff --git a/src/worker/crawler.go b/src/worker/crawler.go index c14e2da..b501ec3 100644 --- a/src/worker/crawler.go +++ b/src/worker/crawler.go @@ -14,6 +14,7 @@ import ( "github.com/nkanaev/yarr/src/content/scraper" "github.com/nkanaev/yarr/src/parser" "github.com/nkanaev/yarr/src/storage" + "github.com/nkanaev/yarr/src/storage/model" "golang.org/x/net/html/charset" ) @@ -139,28 +140,28 @@ func findFavicon(siteUrl, feedUrl string) (*[]byte, error) { return &emptyIcon, nil } -func ConvertItems(items []parser.Item, feed storage.Feed) []storage.Item { - result := make([]storage.Item, len(items)) +func ConvertItems(items []parser.Item, feed model.Feed) []model.Item { + result := make([]model.Item, len(items)) for i, item := range items { - mediaLinks := make(storage.MediaLinks, 0) + mediaLinks := make(model.MediaLinks, 0) for _, link := range item.MediaLinks { - mediaLinks = append(mediaLinks, storage.MediaLink(link)) + mediaLinks = append(mediaLinks, model.MediaLink(link)) } - result[i] = storage.Item{ + result[i] = model.Item{ GUID: item.GUID, FeedId: feed.Id, Title: item.Title, Link: item.URL, Content: item.Content, Date: item.Date, - Status: storage.UNREAD, + Status: model.UNREAD, MediaLinks: mediaLinks, } } return result } -func listItems(f storage.Feed, db *storage.Storage) ([]storage.Item, error) { +func listItems(f model.Feed, db storage.Storage) ([]model.Item, error) { lmod := "" etag := "" if state, _ := db.GetFeedState(f.Id); state != nil { @@ -193,7 +194,7 @@ func listItems(f storage.Feed, db *storage.Storage) ([]storage.Item, error) { etag = res.Header.Get("Etag") now := time.Now().UTC() if lmod != "" || etag != "" { - db.UpdateFeedState(f.Id, storage.UpdateFeedStateParams{ + db.UpdateFeedState(f.Id, model.UpdateFeedStateParams{ HTTPLastModified: &lmod, HTTPEtag: &etag, LastRefreshed: &now, diff --git a/src/worker/worker.go b/src/worker/worker.go index 3e69a0f..9bbfade 100644 --- a/src/worker/worker.go +++ b/src/worker/worker.go @@ -7,19 +7,20 @@ import ( "time" "github.com/nkanaev/yarr/src/storage" + "github.com/nkanaev/yarr/src/storage/model" ) const NUM_WORKERS = 4 type Worker struct { - db *storage.Storage + db storage.Storage pending *int32 refresh *time.Ticker reflock sync.Mutex stopper chan bool } -func NewWorker(db *storage.Storage) *Worker { +func NewWorker(db storage.Storage) *Worker { pending := int32(0) return &Worker{db: db, pending: &pending} } @@ -39,13 +40,13 @@ func (w *Worker) StartFeedCleaner() { }() } -func (w *Worker) FindFeedFavicon(feed storage.Feed) { +func (w *Worker) FindFeedFavicon(feed model.Feed) { icon, err := findFavicon(feed.Link, feed.FeedLink) if err != nil { log.Printf("Failed to find favicon for %s (%s): %s", feed.FeedLink, feed.Link, err) } if icon != nil { - w.db.UpdateFeed(feed.Id, storage.UpdateFeedParams{Icon: storage.SetNullable(icon)}) + w.db.UpdateFeed(feed.Id, model.UpdateFeedParams{Icon: model.SetNullable(icon)}) } } @@ -99,11 +100,11 @@ func (w *Worker) RefreshFeeds() { go w.refresher(feeds) } -func (w *Worker) refresher(feeds []storage.Feed) { +func (w *Worker) refresher(feeds []model.Feed) { // w.db.ResetFeedErrors() - srcqueue := make(chan storage.Feed, len(feeds)) - dstqueue := make(chan []storage.Item) + srcqueue := make(chan model.Feed, len(feeds)) + dstqueue := make(chan []model.Item) for range NUM_WORKERS { go w.worker(srcqueue, dstqueue) @@ -125,15 +126,15 @@ func (w *Worker) refresher(feeds []storage.Feed) { log.Printf("Finished refreshing %d feeds", len(feeds)) } -func (w *Worker) worker(srcqueue <-chan storage.Feed, dstqueue chan<- []storage.Item) { +func (w *Worker) worker(srcqueue <-chan model.Feed, dstqueue chan<- []model.Item) { for feed := range srcqueue { empty := "" - w.db.UpdateFeedState(feed.Id, storage.UpdateFeedStateParams{LastError: &empty}) + w.db.UpdateFeedState(feed.Id, model.UpdateFeedStateParams{LastError: &empty}) items, err := listItems(feed, w.db) if err != nil { errMsg := err.Error() - w.db.UpdateFeedState(feed.Id, storage.UpdateFeedStateParams{LastError: &errMsg}) + w.db.UpdateFeedState(feed.Id, model.UpdateFeedStateParams{LastError: &errMsg}) } if len(items) > 0 && !feed.HasIcon { w.FindFeedFavicon(feed)