feed.go 9.9 KB

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