feed.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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/model"
  10. "miniflux.app/timezone"
  11. )
  12. var feedListQuery = `
  13. SELECT
  14. f.id,
  15. f.feed_url,
  16. f.site_url,
  17. f.title,
  18. f.etag_header,
  19. f.last_modified_header,
  20. f.user_id,
  21. f.checked_at at time zone u.timezone,
  22. f.parsing_error_count,
  23. f.parsing_error_msg,
  24. f.scraper_rules,
  25. f.rewrite_rules,
  26. f.crawler,
  27. f.user_agent,
  28. f.username,
  29. f.password,
  30. f.disabled,
  31. f.category_id,
  32. c.title as category_title,
  33. fi.icon_id,
  34. u.timezone
  35. FROM
  36. feeds f
  37. LEFT JOIN
  38. categories c ON c.id=f.category_id
  39. LEFT JOIN
  40. feed_icons fi ON fi.feed_id=f.id
  41. LEFT JOIN
  42. users u ON u.id=f.user_id
  43. WHERE
  44. f.user_id=$1
  45. ORDER BY
  46. f.parsing_error_count DESC, lower(f.title) ASC
  47. `
  48. // FeedExists checks if the given feed exists.
  49. func (s *Storage) FeedExists(userID, feedID int64) bool {
  50. var result bool
  51. query := `SELECT true FROM feeds WHERE user_id=$1 AND id=$2`
  52. s.db.QueryRow(query, userID, feedID).Scan(&result)
  53. return result
  54. }
  55. // FeedURLExists checks if feed URL already exists.
  56. func (s *Storage) FeedURLExists(userID int64, feedURL string) bool {
  57. var result bool
  58. query := `SELECT true FROM feeds WHERE user_id=$1 AND feed_url=$2`
  59. s.db.QueryRow(query, userID, feedURL).Scan(&result)
  60. return result
  61. }
  62. // CountFeeds returns the number of feeds that belongs to the given user.
  63. func (s *Storage) CountFeeds(userID int64) int {
  64. var result int
  65. err := s.db.QueryRow(`SELECT count(*) FROM feeds WHERE user_id=$1`, userID).Scan(&result)
  66. if err != nil {
  67. return 0
  68. }
  69. return result
  70. }
  71. // CountErrorFeeds returns the number of feeds with parse errors that belong to the given user.
  72. func (s *Storage) CountErrorFeeds(userID int64) int {
  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, maxParsingError).Scan(&result)
  76. if err != nil {
  77. return 0
  78. }
  79. return result
  80. }
  81. // Feeds returns all feeds that belongs to the given user.
  82. func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
  83. return s.fetchFeeds(feedListQuery, "", userID)
  84. }
  85. // FeedsWithCounters returns all feeds of the given user with counters of read and unread entries.
  86. func (s *Storage) FeedsWithCounters(userID int64) (model.Feeds, error) {
  87. counterQuery := `
  88. SELECT
  89. feed_id,
  90. status,
  91. count(*)
  92. FROM
  93. entries
  94. WHERE
  95. user_id=$1 AND status IN ('read', 'unread')
  96. GROUP BY
  97. feed_id, status
  98. `
  99. return s.fetchFeeds(feedListQuery, counterQuery, userID)
  100. }
  101. // FeedsByCategoryWithCounters returns all feeds of the given user/category with counters of read and unread entries.
  102. func (s *Storage) FeedsByCategoryWithCounters(userID, categoryID int64) (model.Feeds, error) {
  103. feedQuery := `
  104. SELECT
  105. f.id,
  106. f.feed_url,
  107. f.site_url,
  108. f.title,
  109. f.etag_header,
  110. f.last_modified_header,
  111. f.user_id,
  112. f.checked_at at time zone u.timezone,
  113. f.parsing_error_count,
  114. f.parsing_error_msg,
  115. f.scraper_rules,
  116. f.rewrite_rules,
  117. f.crawler,
  118. f.user_agent,
  119. f.username,
  120. f.password,
  121. f.disabled,
  122. f.category_id,
  123. c.title as category_title,
  124. fi.icon_id,
  125. u.timezone
  126. FROM
  127. feeds f
  128. LEFT JOIN
  129. categories c ON c.id=f.category_id
  130. LEFT JOIN
  131. feed_icons fi ON fi.feed_id=f.id
  132. LEFT JOIN
  133. users u ON u.id=f.user_id
  134. WHERE
  135. f.user_id=$1 AND f.category_id=$2
  136. ORDER BY
  137. f.parsing_error_count DESC, lower(f.title) ASC
  138. `
  139. counterQuery := `
  140. SELECT
  141. e.feed_id,
  142. e.status,
  143. count(*)
  144. FROM
  145. entries e
  146. LEFT JOIN
  147. feeds f ON f.id=e.feed_id
  148. WHERE
  149. e.user_id=$1 AND f.category_id=$2 AND e.status IN ('read', 'unread')
  150. GROUP BY
  151. e.feed_id, e.status
  152. `
  153. return s.fetchFeeds(feedQuery, counterQuery, userID, categoryID)
  154. }
  155. func (s *Storage) fetchFeedCounter(query string, args ...interface{}) (unreadCounters map[int64]int, readCounters map[int64]int, err error) {
  156. rows, err := s.db.Query(query, args...)
  157. if err != nil {
  158. return nil, nil, fmt.Errorf(`store: unable to fetch feed counts: %v`, err)
  159. }
  160. defer rows.Close()
  161. readCounters = make(map[int64]int)
  162. unreadCounters = make(map[int64]int)
  163. for rows.Next() {
  164. var feedID int64
  165. var status string
  166. var count int
  167. if err := rows.Scan(&feedID, &status, &count); err != nil {
  168. return nil, nil, fmt.Errorf(`store: unable to fetch feed counter row: %v`, err)
  169. }
  170. if status == "read" {
  171. readCounters[feedID] = count
  172. } else if status == "unread" {
  173. unreadCounters[feedID] = count
  174. }
  175. }
  176. return readCounters, unreadCounters, nil
  177. }
  178. func (s *Storage) fetchFeeds(feedQuery, counterQuery string, args ...interface{}) (model.Feeds, error) {
  179. var (
  180. readCounters map[int64]int
  181. unreadCounters map[int64]int
  182. )
  183. if counterQuery != "" {
  184. var err error
  185. readCounters, unreadCounters, err = s.fetchFeedCounter(counterQuery, args...)
  186. if err != nil {
  187. return nil, err
  188. }
  189. }
  190. feeds := make(model.Feeds, 0)
  191. rows, err := s.db.Query(feedQuery, args...)
  192. if err != nil {
  193. return nil, fmt.Errorf(`store: unable to fetch feeds: %v`, err)
  194. }
  195. defer rows.Close()
  196. for rows.Next() {
  197. var feed model.Feed
  198. var iconID interface{}
  199. var tz string
  200. feed.Category = &model.Category{}
  201. err := rows.Scan(
  202. &feed.ID,
  203. &feed.FeedURL,
  204. &feed.SiteURL,
  205. &feed.Title,
  206. &feed.EtagHeader,
  207. &feed.LastModifiedHeader,
  208. &feed.UserID,
  209. &feed.CheckedAt,
  210. &feed.ParsingErrorCount,
  211. &feed.ParsingErrorMsg,
  212. &feed.ScraperRules,
  213. &feed.RewriteRules,
  214. &feed.Crawler,
  215. &feed.UserAgent,
  216. &feed.Username,
  217. &feed.Password,
  218. &feed.Disabled,
  219. &feed.Category.ID,
  220. &feed.Category.Title,
  221. &iconID,
  222. &tz,
  223. )
  224. if err != nil {
  225. return nil, fmt.Errorf(`store: unable to fetch feeds row: %v`, err)
  226. }
  227. if iconID != nil {
  228. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
  229. }
  230. if counterQuery != "" {
  231. if count, found := readCounters[feed.ID]; found {
  232. feed.ReadCount = count
  233. }
  234. if count, found := unreadCounters[feed.ID]; found {
  235. feed.UnreadCount = count
  236. }
  237. }
  238. feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
  239. feed.Category.UserID = feed.UserID
  240. feeds = append(feeds, &feed)
  241. }
  242. return feeds, nil
  243. }
  244. // FeedByID returns a feed by the ID.
  245. func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
  246. var feed model.Feed
  247. var iconID interface{}
  248. var tz string
  249. feed.Category = &model.Category{UserID: userID}
  250. query := `
  251. SELECT
  252. f.id,
  253. f.feed_url,
  254. f.site_url,
  255. f.title,
  256. f.etag_header,
  257. f.last_modified_header,
  258. f.user_id, f.checked_at at time zone u.timezone,
  259. f.parsing_error_count,
  260. f.parsing_error_msg,
  261. f.scraper_rules,
  262. f.rewrite_rules,
  263. f.crawler,
  264. f.user_agent,
  265. f.username,
  266. f.password,
  267. f.disabled,
  268. f.category_id,
  269. c.title as category_title,
  270. fi.icon_id,
  271. u.timezone
  272. FROM feeds f
  273. LEFT JOIN categories c ON c.id=f.category_id
  274. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  275. LEFT JOIN users u ON u.id=f.user_id
  276. WHERE
  277. f.user_id=$1 AND f.id=$2
  278. `
  279. err := s.db.QueryRow(query, userID, feedID).Scan(
  280. &feed.ID,
  281. &feed.FeedURL,
  282. &feed.SiteURL,
  283. &feed.Title,
  284. &feed.EtagHeader,
  285. &feed.LastModifiedHeader,
  286. &feed.UserID,
  287. &feed.CheckedAt,
  288. &feed.ParsingErrorCount,
  289. &feed.ParsingErrorMsg,
  290. &feed.ScraperRules,
  291. &feed.RewriteRules,
  292. &feed.Crawler,
  293. &feed.UserAgent,
  294. &feed.Username,
  295. &feed.Password,
  296. &feed.Disabled,
  297. &feed.Category.ID,
  298. &feed.Category.Title,
  299. &iconID,
  300. &tz,
  301. )
  302. switch {
  303. case err == sql.ErrNoRows:
  304. return nil, nil
  305. case err != nil:
  306. return nil, fmt.Errorf(`store: unable to fetch feed #%d: %v`, feedID, err)
  307. }
  308. if iconID != nil {
  309. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
  310. }
  311. feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
  312. return &feed, nil
  313. }
  314. // CreateFeed creates a new feed.
  315. func (s *Storage) CreateFeed(feed *model.Feed) error {
  316. sql := `
  317. INSERT INTO feeds (
  318. feed_url,
  319. site_url,
  320. title,
  321. category_id,
  322. user_id,
  323. etag_header,
  324. last_modified_header,
  325. crawler,
  326. user_agent,
  327. username,
  328. password,
  329. disabled,
  330. scraper_rules,
  331. rewrite_rules
  332. )
  333. VALUES
  334. ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
  335. RETURNING
  336. id
  337. `
  338. err := s.db.QueryRow(
  339. sql,
  340. feed.FeedURL,
  341. feed.SiteURL,
  342. feed.Title,
  343. feed.Category.ID,
  344. feed.UserID,
  345. feed.EtagHeader,
  346. feed.LastModifiedHeader,
  347. feed.Crawler,
  348. feed.UserAgent,
  349. feed.Username,
  350. feed.Password,
  351. feed.Disabled,
  352. feed.ScraperRules,
  353. feed.RewriteRules,
  354. ).Scan(&feed.ID)
  355. if err != nil {
  356. return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
  357. }
  358. for i := 0; i < len(feed.Entries); i++ {
  359. feed.Entries[i].FeedID = feed.ID
  360. feed.Entries[i].UserID = feed.UserID
  361. if !s.entryExists(feed.Entries[i]) {
  362. err := s.createEntry(feed.Entries[i])
  363. if err != nil {
  364. return err
  365. }
  366. }
  367. }
  368. return nil
  369. }
  370. // UpdateFeed updates an existing feed.
  371. func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
  372. query := `
  373. UPDATE
  374. feeds
  375. SET
  376. feed_url=$1,
  377. site_url=$2,
  378. title=$3,
  379. category_id=$4,
  380. etag_header=$5,
  381. last_modified_header=$6,
  382. checked_at=$7,
  383. parsing_error_msg=$8,
  384. parsing_error_count=$9,
  385. scraper_rules=$10,
  386. rewrite_rules=$11,
  387. crawler=$12,
  388. user_agent=$13,
  389. username=$14,
  390. password=$15,
  391. disabled=$16
  392. WHERE
  393. id=$17 AND user_id=$18
  394. `
  395. _, err = s.db.Exec(query,
  396. feed.FeedURL,
  397. feed.SiteURL,
  398. feed.Title,
  399. feed.Category.ID,
  400. feed.EtagHeader,
  401. feed.LastModifiedHeader,
  402. feed.CheckedAt,
  403. feed.ParsingErrorMsg,
  404. feed.ParsingErrorCount,
  405. feed.ScraperRules,
  406. feed.RewriteRules,
  407. feed.Crawler,
  408. feed.UserAgent,
  409. feed.Username,
  410. feed.Password,
  411. feed.Disabled,
  412. feed.ID,
  413. feed.UserID,
  414. )
  415. if err != nil {
  416. return fmt.Errorf(`store: unable to update feed #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  417. }
  418. return nil
  419. }
  420. // UpdateFeedError updates feed errors.
  421. func (s *Storage) UpdateFeedError(feed *model.Feed) (err error) {
  422. query := `
  423. UPDATE
  424. feeds
  425. SET
  426. parsing_error_msg=$1,
  427. parsing_error_count=$2,
  428. checked_at=$3
  429. WHERE
  430. id=$4 AND user_id=$5
  431. `
  432. _, err = s.db.Exec(query,
  433. feed.ParsingErrorMsg,
  434. feed.ParsingErrorCount,
  435. feed.CheckedAt,
  436. feed.ID,
  437. feed.UserID,
  438. )
  439. if err != nil {
  440. return fmt.Errorf(`store: unable to update feed error #%d (%s): %v`, feed.ID, feed.FeedURL, err)
  441. }
  442. return nil
  443. }
  444. // RemoveFeed removes a feed.
  445. func (s *Storage) RemoveFeed(userID, feedID int64) error {
  446. query := `DELETE FROM feeds WHERE id = $1 AND user_id = $2`
  447. result, err := s.db.Exec(query, feedID, userID)
  448. if err != nil {
  449. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  450. }
  451. count, err := result.RowsAffected()
  452. if err != nil {
  453. return fmt.Errorf(`store: unable to remove feed #%d: %v`, feedID, err)
  454. }
  455. if count == 0 {
  456. return errors.New(`store: no feed has been removed`)
  457. }
  458. return nil
  459. }
  460. // ResetFeedErrors removes all feed errors.
  461. func (s *Storage) ResetFeedErrors() error {
  462. _, err := s.db.Exec(`UPDATE feeds SET parsing_error_count=0, parsing_error_msg=''`)
  463. return err
  464. }