8000 [pull] main from miniflux:main by pull[bot] · Pull Request #217 · suhaoGe/miniflux · GitHub
[go: up one dir, main page]

Skip to content
Merged
< 8000 /div>
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions internal/api/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ import (
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")

if !h.store.HasFeedIcon(feedID) {
json.NotFound(w, r)
return
}

icon, err := h.store.IconByFeedID(request.UserID(r), feedID)
if err != nil {
json.ServerError(w, r, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/cleanup_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func runCleanupTasks(store *storage.Storage) {
}
}

if enclosuresAffected, err := store.DeleteRemovedEntriesEnclosures(); err != nil {
if enclosuresAffected, err := store.DeleteEnclosuresOfRemovedEntries(); err != nil {
slog.Error("Unable to delete enclosures from removed entries", slog.Any("error", err))
} else {
slog.Info("Deleting enclosures from removed entries completed",
Expand Down
33 changes: 33 additions & 0 deletions internal/storage/enclosure.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error

func (s *Storage) updateEnclosures(tx *sql.Tx, entry *model.Entry) error {
if len(entry.Enclosures) == 0 {
// Do not keep any old enclosures if there is none in the updated entry.
query := `
DELETE FROM
enclosures
WHERE
user_id=$1 AND entry_id=$2
`

_, err := tx.Exec(query, entry.UserID, entry.ID)
if err != nil {
return fmt.Errorf(`store: unable to delete old enclosures: %v`, err)
}
return nil
}

Expand Down Expand Up @@ -233,3 +245,24 @@ func (s *Storage) UpdateEnclosure(enclosure *model.Enclosure) error {

return nil
}

// DeleteEnclosuresOfRemovedEntries deletes enclosures associated with entries marked as "removed".
func (s *Storage) DeleteEnclosuresOfRemovedEntries() (int64, error) {
query := `
DELETE FROM
enclosures
WHERE
enclosures.entry_id IN (SELECT id FROM entries WHERE status=$1)
`
result, err := s.db.Exec(query, model.EntryStatusRemoved)
if err != nil {
return 0, fmt.Errorf(`store: unable to delete enclosures from removed entries: %v`, err)
}

count, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf(`store: unable to get the number of rows affected while deleting enclosures from removed entries: %v`, err)
}

return count, nil
}
47 changes: 19 additions & 28 deletions internal/storage/entry.go
Original file line number Diff line number Diff line change
8000 Expand Up @@ -315,44 +315,34 @@ func (s *Storage) GetReadTime(feedID int64, entryHash string) int {

// cleanupRemovedEntriesNotInFeed deletes from the database entries marked as "removed" and not visible anymore in the feed.
func (s *Storage) cleanupRemovedEntriesNotInFeed(feedID int64, entryHashes []string) error {
// Acquire locks in id order and skip already-locked rows to avoid deadlocks with
// ClearRemovedEntriesContent, which also updates removed entries concurrently.
query := `
DELETE FROM
entries
WHERE
feed_id=$1 AND
status=$2 AND
NOT (hash=ANY($3))
WITH to_delete AS (
SELECT id
FROM entries
WHERE
feed_id=$1 AND
status=$2 AND
NOT (hash=ANY($3))
ORDER BY id
FOR UPDATE SKIP LOCKED
)
DELETE FROM entries
USING to_delete
WHERE entries.id = to_delete.id
`
if _, err := s.db.Exec(query, feedID, model.EntryStatusRemoved, pq.Array(entryHashes)); err != nil {
return fmt.Errorf(`store: unable to cleanup entries: %v`, err)
return fmt.Errorf(`store: unable to remove entries not in feed: %v`, err)
}

return nil
}

// DeleteRemovedEntriesEnclosures deletes enclosures associated with entries marked as "removed".
func (s *Storage) DeleteRemovedEntriesEnclosures() (int64, error) {
query := `
DELETE FROM
enclosures
WHERE
enclosures.entry_id IN (SELECT id FROM entries WHERE status=$1)
`
result, err := s.db.Exec(query, model.EntryStatusRemoved)
if err != nil {
return 0, fmt.Errorf(`store: unable to delete enclosures from removed entries: %v`, err)
}

count, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf(`store: unable to get the number of rows affected while deleting enclosures from removed entries: %v`, err)
}

return count, nil
}

// ClearRemovedEntriesContent clears the content fields of entries marked as "removed", keeping only their metadata.
func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
// Skip locked rows so this batch scrubber doesn't block or deadlock with the
// concurrent cleanup that deletes removed entries in the same table.
query := `
UPDATE
entries
Expand All @@ -368,6 +358,7 @@ func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
FROM entries
WHERE status = $1 AND content IS NOT NULL
ORDER BY id ASC
FOR UPDATE SKIP LOCKED
LIMIT $2
)
`
Expand Down
45 changes: 25 additions & 20 deletions internal/storage/icon.go
BC43
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import (
"miniflux.app/v2/internal/model"
)

// HasFeedIcon checks if the given feed has an icon.
// HasFeedIcon reports whether the specified feed already has an associated icon record.
func (s *Storage) HasFeedIcon(feedID int64) bool {
var result bool
query := `SELECT true FROM feed_icons WHERE feed_id=$1 LIMIT 1`
s.db.QueryRow(query, feedID).Scan(&result)
return result
}

// IconByID returns an icon by the ID.
// IconByID fetches a single icon by its internal identifier, returning nil when it is not found.
func (s *Storage) IconByID(iconID int64) (*model.Icon, error) {
var icon model.Icon
query := `
Expand All @@ -33,16 +33,17 @@ func (s *Storage) IconByID(iconID int64) (*model.Icon, error) {
FROM icons
WHERE id=$1`
err := s.db.QueryRow(query, iconID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err == sql.ErrNoRows {
switch {
case err == sql.ErrNoRows:
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("store: unable to fetch icon #%d: %w", iconID, err)
case err != nil:
return nil, fmt.Errorf("store: cannot load icon id=%d: %w", iconID, err)
default:
return &icon, nil
}

return &icon, nil
}

// IconByExternalID returns an icon by the External Icon ID.
// IconByExternalID fetches an icon using its external identifier, returning nil when no match exists.
func (s *Storage) IconByExternalID(externalIconID string) (*model.Icon, error) {
var icon model.Icon
query := `
Expand All @@ -56,16 +57,17 @@ func (s *Storage) IconByExternalID(externalIconID string) (*model.Icon, error) {
WHERE external_id=$1
`
err := s.db.QueryRow(query, externalIconID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err == sql.ErrNoRows {
switch {
case err == sql.ErrNoRows:
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("store: unable to fetch icon %s: %w", externalIconID, err)
case err != nil:
return nil, fmt.Errorf("store: cannot load icon external_id=%s: %w", externalIconID, err)
default:
return &icon, nil
}

return &icon, nil
8 }

// IconByFeedID returns a feed icon.
// IconByFeedID returns the icon linked to the given feed for the specified user, or nil if none is set.
func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
query := `
SELECT
Expand All @@ -83,14 +85,17 @@ func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
`
var icon model.Icon
err := s.db.QueryRow(query, userID, feedID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch icon: %v`, err)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, fmt.Errorf("store: cannot load icon for feed_id=%d user_id=%d: %w", feedID, userID, err)
default:
return &icon, nil
}

return &icon, nil
}

// StoreFeedIcon creates or updates a feed icon.
// StoreFeedIcon creates or reuses an icon by hash and associates it with the given feed atomically.
func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
tx, err := s.db.Begin()
if err != nil {
Expand Down Expand Up @@ -140,7 +145,7 @@ func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
return nil
}

// Icons returns all icons that belongs to a user.
// Icons lists all icons currently associated with any feed owned by the given user.
func (s *Storage) Icons(userID int64) (model.Icons, error) {
query := `
SELECT
Expand Down
Loading
0