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
BasePath string
Public []string
DB *storage.Storage
DB storage.Storage
}
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/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
}

View File

@@ -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 {

View File

@@ -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(&params); 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{

View File

@@ -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()

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)