feed.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // Copyright 2017 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package storage // import "miniflux.app/storage"
  5. import (
  6. "database/sql"
  7. "errors"
  8. "fmt"
  9. "miniflux.app/config"
  10. "miniflux.app/model"
  11. )
  12. // FeedExists checks if the given feed exists.
  13. func (s *Storage) FeedExists(userID, feedID int64) bool {
  14. var result bool
  15. query := `SELECT true FROM feeds WHERE user_id=$1 AND id=$2`
  16. s.db.QueryRow(query, userID, feedID).Scan(&result)
  17. return result
  18. }
  19. // FeedURLExists checks if feed URL already exists.
  20. func (s *Storage) FeedURLExists(userID int64, feedURL string) bool {
  21. var result bool
  22. query := `SELECT true FROM feeds WHERE user_id=$1 AND feed_url=$2`
  23. s.db.QueryRow(query, userID, feedURL).Scan(&result)
  24. return result
  25. }
  26. // AnotherFeedURLExists checks if the user a duplicated feed.
  27. func (s *Storage) AnotherFeedURLExists(userID, feedID int64, feedURL string) bool {
  28. var result bool
  29. query := `SELECT true FROM feeds WHERE id <> $1 AND user_id=$2 AND feed_url=$3`
  30. s.db.QueryRow(query, feedID, userID, feedURL).Scan(&result)
  31. return result
  32. }
  33. // CountAllFeeds returns the number of feeds in the database.
  34. func (s *Storage) CountAllFeeds() map[string]int64 {
  35. rows, err := s.db.Query(`SELECT disabled, count(*) FROM feeds GROUP BY disabled`)
  36. if err != nil {
  37. return nil
  38. }
  39. defer rows.Close()
  40. results := make(map[string]int64)
  41. results["enabled"] = 0
  42. results["disabled"] = 0
  43. for rows.Next() {
  44. var disabled bool
  45. var count int64
  46. if err := rows.Scan(&disabled, &count); err != nil {
  47. continue
  48. }
  49. if disabled {
  50. results["disabled"] = count
  51. } else {
  52. results["enabled"] = count
  53. }
  54. }
  55. results["total"] = results["disabled"] + results["enabled"]
  56. return results
  57. }
  58. // CountFeeds returns the number of feeds that belongs to the given user.
  59. func (s *Storage) CountFeeds(userID int64) int {
  60. var result int
  61. err := s.db.QueryRow(`SELECT count(*) FROM feeds WHERE user_id=$1`, userID).Scan(&result)
  62. if err != nil {
  63. return 0
  64. }
  65. return result
  66. }
  67. // CountUserFeedsWithErrors returns the number of feeds with parsing errors that belong to the given user.
  68. func (s *Storage) CountUserFeedsWithErrors(userID int64) int {
  69. pollingParsingErrorLimit := config.Opts.PollingParsingErrorLimit()
  70. if pollingParsingErrorLimit <= 0 {
  71. pollingParsingErrorLimit = 1
  72. }
  73. query := `SELECT count(*) FROM feeds WHERE user_id=$1 AND parsing_error_count >= $2`
  74. var result int
  75. err := s.db.QueryRow(query, userID, pollingParsingErrorLimit).Scan(&result)
  76. if err != nil {
  77. return 0
  78. }
  79. return result
  80. }
  81. // CountAllFeedsWithErrors returns the number of feeds with parsing errors.
  82. func (s *Storage) CountAllFeedsWithErrors() int {
  83. pollingParsingErrorLimit := config.Opts.PollingParsingErrorLimit()
  84. if pollingParsingErrorLimit <= 0 {
  85. pollingParsingErrorLimit = 1
  86. }
  87. query := `SELECT count(*) FROM feeds WHERE parsing_error_count >= $1`
  88. var result int
  89. err := s.db.QueryRow(query, pollingParsingErrorLimit).Scan(&result)
  90. if err != nil {
  91. return 0
  92. }
  93. return result
  94. }
  95. // Feeds returns all feeds that belongs to the given user.
  96. func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
  97. builder := NewFeedQueryBuilder(s, userID)
  98. builder.WithOrder(model.DefaultFeedSorting)
  99. builder.WithDirection(model.DefaultFeedSortingDirection)
  100. return builder.GetFeeds()
  101. }
  102. // FeedsWithCounters returns all feeds of the given user with counters of read and unread entries.
  103. func (s *Storage) FeedsWithCounters(userID int64) (model.Feeds, error) {
  104. builder := NewFeedQueryBuilder(s, userID)
  105. builder.WithCounters()
  106. builder.WithOrder(model.DefaultFeedSorting)
  107. builder.WithDirection(model.DefaultFeedSortingDirection)
  108. return builder.GetFeeds()
  109. }
  110. // FeedsByCategoryWithCounters returns all feeds of the given user/category with counters of read and unread entries.
  111. func (s *Storage) FeedsByCategoryWithCounters(userID, categoryID int64) (model.Feeds, error) {
  112. builder := NewFeedQueryBuilder(s, userID)
  113. builder.WithCategoryID(categoryID)
  114. builder.WithCounters()
  115. builder.WithOrder(model.DefaultFeedSorting)
  116. builder.WithDirection(model.DefaultFeedSortingDirection)
  117. return builder.GetFeeds()
  118. }
  119. // WeeklyFeedEntryCount returns the weekly entry count for a feed.
  120. func (s *Storage) WeeklyFeedEntryCount(userID, feedID int64) (int, error) {
  121. query := `
  122. SELECT
  123. count(*)
  124. FROM
  125. entries
  126. WHERE
  127. entries.user_id=$1 AND
  128. entries.feed_id=$2 AND
  129. entries.published_at BETWEEN (now() - interval '1 week') AND now();
  130. `
  131. var weeklyCount int
  132. err := s.db.QueryRow(query, userID, feedID).Scan(&weeklyCount)
  133. switch {
  134. case errors.Is(err, sql.ErrNoRows):
  135. return 0, nil
  136. case err != nil:
  137. return 0, fmt.Errorf(`store: unable to fetch weekly count for feed #%d: %v`, feedID, err)
  138. }
  139. return weeklyCount, nil
  140. }
  141. // FeedByID returns a feed by the ID.
  142. func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
  143. builder := NewFeedQueryBuilder(s, userID)
  144. builder.WithFeedID(feedID)
  145. feed, err := builder.GetFeed()
  146. switch {
  147. case errors.Is(err, sql.ErrNoRows):
  148. return nil, nil
  149. case err != nil:
  150. return nil, fmt.Errorf(`store: unable to fetch feed #%d: %v`, feedID, err)
  151. }
  152. return feed, nil
  153. }
  154. // CreateFeed creates a new feed.
  155. func (s *Storage) CreateFeed(feed *model.Feed) error {
  156. sql := `
  157. INSERT INTO feeds (
  158. feed_url,
  159. site_url,
  160. title,
  161. category_id,
  162. user_id,
  163. etag_header,
  164. last_modified_header,
  165. crawler,
  166. user_agent,
  167. username,
  168. password,
  169. disabled,
  170. scraper_rules,
  171. rewrite_rules,
  172. blocklist_rules,
  173. keeplist_rules,
  174. ignore_http_cache,
  175. fetch_via_proxy
  176. )
  177. VALUES
  178. ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
  179. RETURNING
  180. id
  181. `
  182. err := s.db.QueryRow(
  183. sql,
  184. feed.FeedURL,
  185. feed.SiteURL,
  186. feed.Title,
  187. feed.Category.ID,
  188. feed.UserID,
  189. feed.EtagHeader,
  190. feed.LastModifiedHeader,
  191. feed.Crawler,
  192. feed.UserAgent,
  193. feed.Username,
  194. feed.Password,
  195. feed.Disabled,
  196. feed.ScraperRules,
  197. feed.RewriteRules,
  198. feed.BlocklistRules,
  199. feed.KeeplistRules,
  200. feed.IgnoreHTTPCache,
  201. feed.FetchViaProxy,
  202. ).Scan(&feed.ID)
  203. if err != nil {
  204. return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
  205. }
  206. for i := 0; i < len(feed.Entries); i++ {
  207. feed.Entries[i].FeedID = feed.ID
  208. feed.Entries[i].UserID = feed.UserID
  209. tx, err := s.db.Begin()
  210. if err != nil {
  211. return fmt.Errorf(`store: unable to start transaction: %v`, err)
  212. }
  213. if !s.entryExists(tx, feed.Entries[i]) {
  214. if err := s.createEntry(tx, feed.Entries[i]); err != nil {
  215. tx.Rollback()
  216. return err
  217. }
  218. }
  219. if err := tx.Commit(); err != nil {
  220. return fmt.Errorf(`store: unable to commit transaction: %v`, err)
  221. }
  222. }
  223. return nil
  224. }
  225. // UpdateFeed updates an existing feed.
  226. func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
  227. query := `
  228. UPDATE
  229. feeds
  230. SET
  231. feed_url=$1,
  232. site_url=$2,
  233. title=$3,
  234. category_id=$4,
  235. etag_header=$5,
  236. last_modified_header=$6,
  237. checked_at=$7,
  238. parsing_error_msg=$8,
  239. parsing_error_count=$9,
  240. scraper_rules=$10,
  241. rewrite_rules=$11,
  242. blocklist_rules=$12,
  243. keeplist_rules=$13,
  244. crawler=$14,
  245. user_agent=$15,
  246. username=$16,
  247. password=$17,
  248. disabled=$18,
  249. next_check_at=$19,
  250. ignore_http_cache=$20,
  251. fetch_via_proxy=$21
  252. WHERE
  253. id=$22 AND user_id=$23
  254. `
  255. _, err = s.db.Exec(query,
  256. feed.FeedURL,
  257. feed.SiteURL,
  258. feed.Title,
  259. feed.Category.ID,
  260. feed.EtagHeader,
  261. feed.LastModifiedHeader,
  262. feed.CheckedAt,
  263. feed.ParsingErrorMsg,
  264. feed.ParsingErrorCount,
  265. feed.ScraperRules,
  266. feed.RewriteRules,
  267. feed.BlocklistRules,
  268. feed.KeeplistRules,
  269. feed.Crawler,
  270. feed.UserAgent,
  271. feed.Username,
  272. feed.Password,
  273. feed.Disabled,
  274. feed.NextCheckAt,
  275. feed.IgnoreHTTPCache,
  276. feed.FetchViaProxy,
  277. feed.ID,
  278. feed.UserID,
  279. )
  280. if err != nil {
  281. return fmt.Errorf(`store: unable to update feed #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  282. }
  283. return nil
  284. }
  285. // UpdateFeedError updates feed errors.
  286. func (s *Storage) UpdateFeedError(feed *model.Feed) (err error) {
  287. query := `
  288. UPDATE
  289. feeds
  290. SET
  291. parsing_error_msg=$1,
  292. parsing_error_count=$2,
  293. checked_at=$3,
  294. next_check_at=$4
  295. WHERE
  296. id=$5 AND user_id=$6
  297. `
  298. _, err = s.db.Exec(query,
  299. feed.ParsingErrorMsg,
  300. feed.ParsingErrorCount,
  301. feed.CheckedAt,
  302. feed.NextCheckAt,
  303. feed.ID,
  304. feed.UserID,
  305. )
  306. if err != nil {
  307. return fmt.Errorf(`store: unable to update feed error #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  308. }
  309. return nil
  310. }
  311. // RemoveFeed removes a feed.
  312. func (s *Storage) RemoveFeed(userID, feedID int64) error {
  313. query := `DELETE FROM feeds WHERE id = $1 AND user_id = $2`
  314. result, err := s.db.Exec(query, feedID, userID)
  315. if err != nil {
  316. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  317. }
  318. count, err := result.RowsAffected()
  319. if err != nil {
  320. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  321. }
  322. if count == 0 {
  323. return errors.New(`store: no feed has been removed`)
  324. }
  325. return nil
  326. }
  327. // ResetFeedErrors removes all feed errors.
  328. func (s *Storage) ResetFeedErrors() error {
  329. _, err := s.db.Exec(`UPDATE feeds SET parsing_error_count=0, parsing_error_msg=''`)
  330. return err
  331. }