add storage interface, fix all references

This commit is contained in:
nkanaev
2026-06-10 22:24:04 +01:00
parent 3f10371975
commit f2556178b3
9 changed files with 65 additions and 57 deletions

View File

@@ -14,7 +14,7 @@ type Middleware struct {
Password string Password string
BasePath string BasePath string
Public []string Public []string
DB *storage.Storage DB storage.Storage
} }
func (m *Middleware) Handler(c *router.Context) { func (m *Middleware) Handler(c *router.Context) {

View File

@@ -14,6 +14,7 @@ import (
"github.com/nkanaev/yarr/src/server/auth" "github.com/nkanaev/yarr/src/server/auth"
"github.com/nkanaev/yarr/src/server/router" "github.com/nkanaev/yarr/src/server/router"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"github.com/nkanaev/yarr/src/storage/model"
) )
type FeverGroup struct { type FeverGroup struct {
@@ -61,7 +62,7 @@ func writeFeverJSON(c *router.Context, data map[string]any, lastRefreshed int64)
c.JSON(http.StatusOK, data) c.JSON(http.StatusOK, data)
} }
func getLastRefreshedOnTime(feedStates []storage.FeedState) int64 { func getLastRefreshedOnTime(feedStates []model.FeedState) int64 {
var lastRefreshed int64 var lastRefreshed int64
for _, state := range feedStates { for _, state := range feedStates {
if state.LastRefreshed.Unix() > lastRefreshed { if state.LastRefreshed.Unix() > lastRefreshed {
@@ -140,7 +141,7 @@ func joinInts(values []int64) string {
return result.String() return result.String()
} }
func feedGroups(db *storage.Storage) []*FeverFeedsGroup { func feedGroups(db storage.Storage) []*FeverFeedsGroup {
feeds := db.ListFeeds() feeds := db.ListFeeds()
groupFeeds := make(map[int64][]int64) groupFeeds := make(map[int64][]int64)
@@ -176,7 +177,7 @@ func (s *Server) feverGroupsHandler(c *router.Context) {
func (s *Server) feverFeedsHandler(c *router.Context) { func (s *Server) feverFeedsHandler(c *router.Context) {
feeds := s.db.ListFeeds() feeds := s.db.ListFeeds()
states, _ := s.db.ListFeedStates() states, _ := s.db.ListFeedStates()
statesMap := make(map[int64]storage.FeedState) statesMap := make(map[int64]model.FeedState)
for _, state := range states { for _, state := range states {
statesMap[state.FeedID] = state statesMap[state.FeedID] = state
} }
@@ -230,7 +231,7 @@ func (s *Server) feverFaviconsHandler(c *router.Context) {
const listLimit = 50 const listLimit = 50
func (s *Server) feverItemsHandler(c *router.Context) { func (s *Server) feverItemsHandler(c *router.Context) {
filter := storage.ItemFilter{} filter := model.ItemFilter{}
query := c.Req.URL.Query() query := c.Req.URL.Query()
switch { switch {
@@ -262,11 +263,11 @@ func (s *Server) feverItemsHandler(c *router.Context) {
time := date.Unix() time := date.Unix()
isSaved := 0 isSaved := 0
if item.Status == storage.STARRED { if item.Status == model.STARRED {
isSaved = 1 isSaved = 1
} }
isRead := 0 isRead := 0
if item.Status == storage.READ { if item.Status == model.READ {
isRead = 1 isRead = 1
} }
feverItems[i] = FeverItem{ feverItems[i] = FeverItem{
@@ -299,10 +300,10 @@ func (s *Server) feverLinksHandler(c *router.Context) {
} }
func (s *Server) feverUnreadItemIDsHandler(c *router.Context) { func (s *Server) feverUnreadItemIDsHandler(c *router.Context) {
status := storage.UNREAD status := model.UNREAD
itemIds := make([]int64, 0) itemIds := make([]int64, 0)
itemFilter := storage.ItemFilter{ itemFilter := model.ItemFilter{
Status: &status, Status: &status,
} }
for { for {
@@ -322,10 +323,10 @@ func (s *Server) feverUnreadItemIDsHandler(c *router.Context) {
} }
func (s *Server) feverSavedItemIDsHandler(c *router.Context) { func (s *Server) feverSavedItemIDsHandler(c *router.Context) {
status := storage.STARRED status := model.STARRED
itemIds := make([]int64, 0) itemIds := make([]int64, 0)
itemFilter := storage.ItemFilter{ itemFilter := model.ItemFilter{
Status: &status, Status: &status,
} }
for { for {
@@ -353,16 +354,16 @@ func (s *Server) feverMarkHandler(c *router.Context) {
switch c.Req.Form.Get("mark") { switch c.Req.Form.Get("mark") {
case "item": case "item":
var status storage.ItemStatus var status model.ItemStatus
switch c.Req.Form.Get("as") { switch c.Req.Form.Get("as") {
case "read": case "read":
status = storage.READ status = model.READ
case "unread": case "unread":
status = storage.UNREAD status = model.UNREAD
case "saved": case "saved":
status = storage.STARRED status = model.STARRED
case "unsaved": case "unsaved":
status = storage.READ status = model.READ
default: default:
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
return return
@@ -372,7 +373,7 @@ func (s *Server) feverMarkHandler(c *router.Context) {
if c.Req.Form.Get("as") != "read" { if c.Req.Form.Get("as") != "read" {
c.Out.WriteHeader(http.StatusBadRequest) 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) x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64)
if x > 0 { if x > 0 {
before := time.Unix(x, 0).UTC() before := time.Unix(x, 0).UTC()
@@ -383,7 +384,7 @@ func (s *Server) feverMarkHandler(c *router.Context) {
if c.Req.Form.Get("as") != "read" { if c.Req.Form.Get("as") != "read" {
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
} }
markFilter := storage.MarkFilter{} markFilter := model.MarkFilter{}
if id > 0 { if id > 0 {
markFilter.FolderID = &id markFilter.FolderID = &id
} }

View File

@@ -1,9 +1,9 @@
package server package server
import "github.com/nkanaev/yarr/src/storage" import "github.com/nkanaev/yarr/src/storage/model"
type ItemUpdateForm struct { type ItemUpdateForm struct {
Status *storage.ItemStatus `json:"status,omitempty"` Status *model.ItemStatus `json:"status,omitempty"`
} }
type FolderCreateForm struct { type FolderCreateForm struct {

View File

@@ -20,7 +20,7 @@ import (
"github.com/nkanaev/yarr/src/server/gzip" "github.com/nkanaev/yarr/src/server/gzip"
"github.com/nkanaev/yarr/src/server/opml" "github.com/nkanaev/yarr/src/server/opml"
"github.com/nkanaev/yarr/src/server/router" "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" "github.com/nkanaev/yarr/src/worker"
) )
@@ -141,7 +141,7 @@ func (s *Server) handleFolder(c *router.Context) {
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
return return
} }
s.db.UpdateFolder(id, storage.UpdateFolderParams{ s.db.UpdateFolder(id, model.UpdateFolderParams{
Title: body.Title, Title: body.Title,
IsExpanded: body.IsExpanded, IsExpanded: body.IsExpanded,
}) })
@@ -248,7 +248,7 @@ func (s *Server) handleFeedList(c *router.Context) {
map[string]any{"status": "multiple", "choice": result.Sources}, map[string]any{"status": "multiple", "choice": result.Sources},
) )
case result.Feed != nil: case result.Feed != nil:
feed := s.db.CreateFeed(storage.CreateFeedParams{ feed := s.db.CreateFeed(model.CreateFeedParams{
Title: result.Feed.Title, Title: result.Feed.Title,
Link: result.Feed.SiteURL, Link: result.Feed.SiteURL,
FeedLink: result.FeedLink, FeedLink: result.FeedLink,
@@ -288,7 +288,7 @@ func (s *Server) handleFeed(c *router.Context) {
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
return return
} }
params := storage.UpdateFeedParams{} params := model.UpdateFeedParams{}
if title, ok := body["title"]; ok { if title, ok := body["title"]; ok {
if reflect.TypeOf(title).Kind() == reflect.String { if reflect.TypeOf(title).Kind() == reflect.String {
t := title.(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, ok := body["folder_id"]; ok {
if f_id == nil { 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 { } else if reflect.TypeOf(f_id).Kind() == reflect.Float64 {
folderId := int64(f_id.(float64)) folderId := int64(f_id.(float64))
params.FolderID = storage.SetNullable(&folderId) params.FolderID = model.SetNullable(&folderId)
} }
} }
if link, ok := body["feed_link"]; ok { if link, ok := body["feed_link"]; ok {
@@ -366,7 +366,7 @@ func (s *Server) handleItemList(c *router.Context) {
perPage := 20 perPage := 20
query := c.Req.URL.Query() query := c.Req.URL.Query()
filter := storage.ItemFilter{} filter := model.ItemFilter{}
if folderID, err := c.QueryInt64("folder_id"); err == nil { if folderID, err := c.QueryInt64("folder_id"); err == nil {
filter.FolderID = &folderID filter.FolderID = &folderID
} }
@@ -377,7 +377,7 @@ func (s *Server) handleItemList(c *router.Context) {
filter.After = &after filter.After = &after
} }
if status := query.Get("status"); len(status) != 0 { if status := query.Get("status"); len(status) != 0 {
statusValue := storage.StatusValues[status] statusValue := model.StatusValues[status]
filter.Status = &statusValue filter.Status = &statusValue
} }
if search := query.Get("search"); len(search) != 0 { if search := query.Get("search"); len(search) != 0 {
@@ -403,7 +403,7 @@ func (s *Server) handleItemList(c *router.Context) {
"has_more": hasMore, "has_more": hasMore,
}) })
} else if c.Req.Method == "PUT" { } else if c.Req.Method == "PUT" {
filter := storage.MarkFilter{} filter := model.MarkFilter{}
if folderID, err := c.QueryInt64("folder_id"); err == nil { if folderID, err := c.QueryInt64("folder_id"); err == nil {
filter.FolderID = &folderID filter.FolderID = &folderID
@@ -422,7 +422,7 @@ func (s *Server) handleSettings(c *router.Context) {
if c.Req.Method == "GET" { if c.Req.Method == "GET" {
c.JSON(http.StatusOK, s.db.GetSettings()) c.JSON(http.StatusOK, s.db.GetSettings())
} else if c.Req.Method == "PUT" { } else if c.Req.Method == "PUT" {
var params storage.UpdateSettingsParams var params model.UpdateSettingsParams
if err := json.NewDecoder(c.Req.Body).Decode(&params); err != nil { if err := json.NewDecoder(c.Req.Body).Decode(&params); err != nil {
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
return return
@@ -452,7 +452,7 @@ func (s *Server) handleOPMLImport(c *router.Context) {
return return
} }
for _, f := range doc.Feeds { for _, f := range doc.Feeds {
s.db.CreateFeed(storage.CreateFeedParams{ s.db.CreateFeed(model.CreateFeedParams{
Title: f.Title, Title: f.Title,
Link: f.SiteUrl, Link: f.SiteUrl,
FeedLink: f.FeedUrl, FeedLink: f.FeedUrl,
@@ -461,7 +461,7 @@ func (s *Server) handleOPMLImport(c *router.Context) {
for _, f := range doc.Folders { for _, f := range doc.Folders {
folder := s.db.CreateFolder(f.Title) folder := s.db.CreateFolder(f.Title)
for _, ff := range f.AllFeeds() { for _, ff := range f.AllFeeds() {
s.db.CreateFeed(storage.CreateFeedParams{ s.db.CreateFeed(model.CreateFeedParams{
Title: ff.Title, Title: ff.Title,
Link: ff.SiteUrl, Link: ff.SiteUrl,
FeedLink: ff.FeedUrl, FeedLink: ff.FeedUrl,
@@ -485,7 +485,7 @@ func (s *Server) handleOPMLExport(c *router.Context) {
doc := opml.Folder{} doc := opml.Folder{}
feedsByFolderID := make(map[int64][]*storage.Feed) feedsByFolderID := make(map[int64][]*model.Feed)
for _, feed := range s.db.ListFeeds() { for _, feed := range s.db.ListFeeds() {
if feed.FolderId == nil { if feed.FolderId == nil {
doc.Feeds = append(doc.Feeds, opml.Feed{ doc.Feeds = append(doc.Feeds, opml.Feed{

View File

@@ -11,6 +11,7 @@ import (
"testing" "testing"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"github.com/nkanaev/yarr/src/storage/model"
) )
func TestStatic(t *testing.T) { func TestStatic(t *testing.T) {
@@ -79,8 +80,8 @@ func TestFeedIcons(t *testing.T) {
log.SetOutput(io.Discard) log.SetOutput(io.Discard)
db, _ := storage.New(":memory:") db, _ := storage.New(":memory:")
icon := []byte("test") icon := []byte("test")
feed := db.CreateFeed(storage.CreateFeedParams{}) feed := db.CreateFeed(model.CreateFeedParams{})
db.UpdateFeed(feed.Id, storage.UpdateFeedParams{Icon: storage.SetNullable(&icon)}) db.UpdateFeed(feed.Id, model.UpdateFeedParams{Icon: model.SetNullable(&icon)})
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()

View File

@@ -14,7 +14,7 @@ import (
type Server struct { type Server struct {
Addr string Addr string
db *storage.Storage db storage.Storage
worker *worker.Worker worker *worker.Worker
cache map[string]any cache map[string]any
cache_mutex *sync.Mutex cache_mutex *sync.Mutex
@@ -29,7 +29,7 @@ type Server struct {
KeyFile string KeyFile string
} }
func NewServer(db *storage.Storage, addr string) *Server { func NewServer(db storage.Storage, addr string) *Server {
return &Server{ return &Server{
db: db, db: db,
Addr: addr, Addr: addr,

View File

@@ -1,12 +1,12 @@
package factory package storage
import ( import (
"github.com/nkanaev/yarr/src/storage/model" "github.com/nkanaev/yarr/src/storage/model"
"github.com/nkanaev/yarr/src/storage/sqlite"
) )
type Storage interface { type Storage interface {
Close() error Close() error
Migrate() error
CountItems() int CountItems() int
CreateFeed(params model.CreateFeedParams) *model.Feed CreateFeed(params model.CreateFeedParams) *model.Feed
CreateFolder(title string) *model.Folder CreateFolder(title string) *model.Folder
@@ -30,3 +30,7 @@ type Storage interface {
UpdateItemStatus(item_id int64, status model.ItemStatus) bool UpdateItemStatus(item_id int64, status model.ItemStatus) bool
UpdateSettings(params model.UpdateSettingsParams) bool UpdateSettings(params model.UpdateSettingsParams) bool
} }
func New(path string) (Storage, error) {
return sqlite.New(path)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/nkanaev/yarr/src/content/scraper" "github.com/nkanaev/yarr/src/content/scraper"
"github.com/nkanaev/yarr/src/parser" "github.com/nkanaev/yarr/src/parser"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"github.com/nkanaev/yarr/src/storage/model"
"golang.org/x/net/html/charset" "golang.org/x/net/html/charset"
) )
@@ -139,28 +140,28 @@ func findFavicon(siteUrl, feedUrl string) (*[]byte, error) {
return &emptyIcon, nil return &emptyIcon, nil
} }
func ConvertItems(items []parser.Item, feed storage.Feed) []storage.Item { func ConvertItems(items []parser.Item, feed model.Feed) []model.Item {
result := make([]storage.Item, len(items)) result := make([]model.Item, len(items))
for i, item := range items { for i, item := range items {
mediaLinks := make(storage.MediaLinks, 0) mediaLinks := make(model.MediaLinks, 0)
for _, link := range item.MediaLinks { 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, GUID: item.GUID,
FeedId: feed.Id, FeedId: feed.Id,
Title: item.Title, Title: item.Title,
Link: item.URL, Link: item.URL,
Content: item.Content, Content: item.Content,
Date: item.Date, Date: item.Date,
Status: storage.UNREAD, Status: model.UNREAD,
MediaLinks: mediaLinks, MediaLinks: mediaLinks,
} }
} }
return result 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 := "" lmod := ""
etag := "" etag := ""
if state, _ := db.GetFeedState(f.Id); state != nil { 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") etag = res.Header.Get("Etag")
now := time.Now().UTC() now := time.Now().UTC()
if lmod != "" || etag != "" { if lmod != "" || etag != "" {
db.UpdateFeedState(f.Id, storage.UpdateFeedStateParams{ db.UpdateFeedState(f.Id, model.UpdateFeedStateParams{
HTTPLastModified: &lmod, HTTPLastModified: &lmod,
HTTPEtag: &etag, HTTPEtag: &etag,
LastRefreshed: &now, LastRefreshed: &now,

View File

@@ -7,19 +7,20 @@ import (
"time" "time"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"github.com/nkanaev/yarr/src/storage/model"
) )
const NUM_WORKERS = 4 const NUM_WORKERS = 4
type Worker struct { type Worker struct {
db *storage.Storage db storage.Storage
pending *int32 pending *int32
refresh *time.Ticker refresh *time.Ticker
reflock sync.Mutex reflock sync.Mutex
stopper chan bool stopper chan bool
} }
func NewWorker(db *storage.Storage) *Worker { func NewWorker(db storage.Storage) *Worker {
pending := int32(0) pending := int32(0)
return &Worker{db: db, pending: &pending} 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) icon, err := findFavicon(feed.Link, feed.FeedLink)
if err != nil { if err != nil {
log.Printf("Failed to find favicon for %s (%s): %s", feed.FeedLink, feed.Link, err) log.Printf("Failed to find favicon for %s (%s): %s", feed.FeedLink, feed.Link, err)
} }
if icon != nil { 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) go w.refresher(feeds)
} }
func (w *Worker) refresher(feeds []storage.Feed) { func (w *Worker) refresher(feeds []model.Feed) {
// w.db.ResetFeedErrors() // w.db.ResetFeedErrors()
srcqueue := make(chan storage.Feed, len(feeds)) srcqueue := make(chan model.Feed, len(feeds))
dstqueue := make(chan []storage.Item) dstqueue := make(chan []model.Item)
for range NUM_WORKERS { for range NUM_WORKERS {
go w.worker(srcqueue, dstqueue) go w.worker(srcqueue, dstqueue)
@@ -125,15 +126,15 @@ func (w *Worker) refresher(feeds []storage.Feed) {
log.Printf("Finished refreshing %d feeds", len(feeds)) 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 { for feed := range srcqueue {
empty := "" 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) items, err := listItems(feed, w.db)
if err != nil { if err != nil {
errMsg := err.Error() 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 { if len(items) > 0 && !feed.HasIcon {
w.FindFeedFavicon(feed) w.FindFeedFavicon(feed)