entry.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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. "time"
  10. "miniflux.app/crypto"
  11. "miniflux.app/logger"
  12. "miniflux.app/model"
  13. "github.com/lib/pq"
  14. )
  15. // CountUnreadEntries returns the number of unread entries.
  16. func (s *Storage) CountUnreadEntries(userID int64) int {
  17. builder := s.NewEntryQueryBuilder(userID)
  18. builder.WithStatus(model.EntryStatusUnread)
  19. n, err := builder.CountEntries()
  20. if err != nil {
  21. logger.Error(`store: unable to count unread entries for user #%d: %v`, userID, err)
  22. return 0
  23. }
  24. return n
  25. }
  26. // NewEntryQueryBuilder returns a new EntryQueryBuilder
  27. func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
  28. return NewEntryQueryBuilder(s, userID)
  29. }
  30. // UpdateEntryContent updates entry content.
  31. func (s *Storage) UpdateEntryContent(entry *model.Entry) error {
  32. tx, err := s.db.Begin()
  33. if err != nil {
  34. return err
  35. }
  36. query := `
  37. UPDATE
  38. entries
  39. SET
  40. content=$1
  41. WHERE
  42. id=$2 AND user_id=$3
  43. `
  44. _, err = tx.Exec(query, entry.Content, entry.ID, entry.UserID)
  45. if err != nil {
  46. tx.Rollback()
  47. return fmt.Errorf(`store: unable to update content of entry #%d: %v`, entry.ID, err)
  48. }
  49. query = `
  50. UPDATE
  51. entries
  52. SET
  53. document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
  54. WHERE
  55. id=$1 AND user_id=$2
  56. `
  57. _, err = tx.Exec(query, entry.ID, entry.UserID)
  58. if err != nil {
  59. tx.Rollback()
  60. return fmt.Errorf(`store: unable to update content of entry #%d: %v`, entry.ID, err)
  61. }
  62. return tx.Commit()
  63. }
  64. // createEntry add a new entry.
  65. func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
  66. query := `
  67. INSERT INTO entries
  68. (title, hash, url, comments_url, published_at, content, author, user_id, feed_id, changed_at, document_vectors)
  69. VALUES
  70. ($1, $2, $3, $4, $5, $6, $7, $8, $9, now(), setweight(to_tsvector(substring(coalesce($1, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce($6, '') for 1000000)), 'B'))
  71. RETURNING
  72. id, status
  73. `
  74. err := tx.QueryRow(
  75. query,
  76. entry.Title,
  77. entry.Hash,
  78. entry.URL,
  79. entry.CommentsURL,
  80. entry.Date,
  81. entry.Content,
  82. entry.Author,
  83. entry.UserID,
  84. entry.FeedID,
  85. ).Scan(&entry.ID, &entry.Status)
  86. if err != nil {
  87. return fmt.Errorf(`store: unable to create entry %q (feed #%d): %v`, entry.URL, entry.FeedID, err)
  88. }
  89. for i := 0; i < len(entry.Enclosures); i++ {
  90. entry.Enclosures[i].EntryID = entry.ID
  91. entry.Enclosures[i].UserID = entry.UserID
  92. err := s.createEnclosure(tx, entry.Enclosures[i])
  93. if err != nil {
  94. return err
  95. }
  96. }
  97. return nil
  98. }
  99. // updateEntry updates an entry when a feed is refreshed.
  100. // Note: we do not update the published date because some feeds do not contains any date,
  101. // it default to time.Now() which could change the order of items on the history page.
  102. func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
  103. query := `
  104. UPDATE
  105. entries
  106. SET
  107. title=$1,
  108. url=$2,
  109. comments_url=$3,
  110. content=$4,
  111. author=$5,
  112. document_vectors = setweight(to_tsvector(substring(coalesce($1, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce($4, '') for 1000000)), 'B')
  113. WHERE
  114. user_id=$6 AND feed_id=$7 AND hash=$8
  115. RETURNING
  116. id
  117. `
  118. err := tx.QueryRow(
  119. query,
  120. entry.Title,
  121. entry.URL,
  122. entry.CommentsURL,
  123. entry.Content,
  124. entry.Author,
  125. entry.UserID,
  126. entry.FeedID,
  127. entry.Hash,
  128. ).Scan(&entry.ID)
  129. if err != nil {
  130. return fmt.Errorf(`store: unable to update entry %q: %v`, entry.URL, err)
  131. }
  132. for _, enclosure := range entry.Enclosures {
  133. enclosure.UserID = entry.UserID
  134. enclosure.EntryID = entry.ID
  135. }
  136. return s.updateEnclosures(tx, entry.UserID, entry.ID, entry.Enclosures)
  137. }
  138. // entryExists checks if an entry already exists based on its hash when refreshing a feed.
  139. func (s *Storage) entryExists(tx *sql.Tx, entry *model.Entry) bool {
  140. var result bool
  141. tx.QueryRow(
  142. `SELECT true FROM entries WHERE user_id=$1 AND feed_id=$2 AND hash=$3`,
  143. entry.UserID,
  144. entry.FeedID,
  145. entry.Hash,
  146. ).Scan(&result)
  147. return result
  148. }
  149. // cleanupEntries deletes from the database entries marked as "removed" and not visible anymore in the feed.
  150. func (s *Storage) cleanupEntries(feedID int64, entryHashes []string) error {
  151. query := `
  152. DELETE FROM
  153. entries
  154. WHERE
  155. feed_id=$1
  156. AND
  157. id IN (SELECT id FROM entries WHERE feed_id=$2 AND status=$3 AND NOT (hash=ANY($4)))
  158. `
  159. if _, err := s.db.Exec(query, feedID, feedID, model.EntryStatusRemoved, pq.Array(entryHashes)); err != nil {
  160. return fmt.Errorf(`store: unable to cleanup entries: %v`, err)
  161. }
  162. return nil
  163. }
  164. // RefreshFeedEntries updates feed entries while refreshing a feed.
  165. func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (err error) {
  166. var entryHashes []string
  167. for _, entry := range entries {
  168. entry.UserID = userID
  169. entry.FeedID = feedID
  170. tx, err := s.db.Begin()
  171. if err != nil {
  172. return fmt.Errorf(`store: unable to start transaction: %v`, err)
  173. }
  174. if s.entryExists(tx, entry) {
  175. if updateExistingEntries {
  176. err = s.updateEntry(tx, entry)
  177. }
  178. } else {
  179. err = s.createEntry(tx, entry)
  180. }
  181. if err != nil {
  182. tx.Rollback()
  183. return err
  184. }
  185. if err := tx.Commit(); err != nil {
  186. return fmt.Errorf(`store: unable to commit transaction: %v`, err)
  187. }
  188. entryHashes = append(entryHashes, entry.Hash)
  189. }
  190. go func() {
  191. if err := s.cleanupEntries(feedID, entryHashes); err != nil {
  192. logger.Error(`store: feed #%d: %v`, feedID, err)
  193. }
  194. }()
  195. return nil
  196. }
  197. // ArchiveEntries changes the status of entries to "removed" after the given number of days.
  198. func (s *Storage) ArchiveEntries(status string, days int) (int64, error) {
  199. if days < 0 {
  200. return 0, nil
  201. }
  202. query := `
  203. UPDATE
  204. entries
  205. SET
  206. status='removed'
  207. WHERE
  208. id=ANY(SELECT id FROM entries WHERE status=$1 AND starred is false AND share_code='' AND published_at < now () - '%d days'::interval ORDER BY published_at ASC LIMIT 5000)
  209. `
  210. result, err := s.db.Exec(fmt.Sprintf(query, days), status)
  211. if err != nil {
  212. return 0, fmt.Errorf(`store: unable to archive %s entries: %v`, status, err)
  213. }
  214. count, err := result.RowsAffected()
  215. if err != nil {
  216. return 0, fmt.Errorf(`store: unable to get the number of rows affected: %v`, err)
  217. }
  218. return count, nil
  219. }
  220. // SetEntriesStatus update the status of the given list of entries.
  221. func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string) error {
  222. query := `UPDATE entries SET status=$1, changed_at=now() WHERE user_id=$2 AND id=ANY($3)`
  223. result, err := s.db.Exec(query, status, userID, pq.Array(entryIDs))
  224. if err != nil {
  225. return fmt.Errorf(`store: unable to update entries statuses %v: %v`, entryIDs, err)
  226. }
  227. count, err := result.RowsAffected()
  228. if err != nil {
  229. return fmt.Errorf(`store: unable to update these entries %v: %v`, entryIDs, err)
  230. }
  231. if count == 0 {
  232. return errors.New(`store: nothing has been updated`)
  233. }
  234. return nil
  235. }
  236. // ToggleBookmark toggles entry bookmark value.
  237. func (s *Storage) ToggleBookmark(userID int64, entryID int64) error {
  238. query := `UPDATE entries SET starred = NOT starred, changed_at=now() WHERE user_id=$1 AND id=$2`
  239. result, err := s.db.Exec(query, userID, entryID)
  240. if err != nil {
  241. return fmt.Errorf(`store: unable to toggle bookmark flag for entry #%d: %v`, entryID, err)
  242. }
  243. count, err := result.RowsAffected()
  244. if err != nil {
  245. return fmt.Errorf(`store: unable to toggle bookmark flag for entry #%d: %v`, entryID, err)
  246. }
  247. if count == 0 {
  248. return errors.New(`store: nothing has been updated`)
  249. }
  250. return nil
  251. }
  252. // FlushHistory set all entries with the status "read" to "removed".
  253. func (s *Storage) FlushHistory(userID int64) error {
  254. query := `
  255. UPDATE
  256. entries
  257. SET
  258. status=$1,
  259. changed_at=now()
  260. WHERE
  261. user_id=$2 AND status=$3 AND starred is false AND share_code=''
  262. `
  263. _, err := s.db.Exec(query, model.EntryStatusRemoved, userID, model.EntryStatusRead)
  264. if err != nil {
  265. return fmt.Errorf(`store: unable to flush history: %v`, err)
  266. }
  267. return nil
  268. }
  269. // MarkAllAsRead updates all user entries to the read status.
  270. func (s *Storage) MarkAllAsRead(userID int64) error {
  271. query := `UPDATE entries SET status=$1, changed_at=now() WHERE user_id=$2 AND status=$3`
  272. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread)
  273. if err != nil {
  274. return fmt.Errorf(`store: unable to mark all entries as read: %v`, err)
  275. }
  276. count, _ := result.RowsAffected()
  277. logger.Debug("[Storage:MarkAllAsRead] %d items marked as read", count)
  278. return nil
  279. }
  280. // MarkFeedAsRead updates all feed entries to the read status.
  281. func (s *Storage) MarkFeedAsRead(userID, feedID int64, before time.Time) error {
  282. query := `
  283. UPDATE
  284. entries
  285. SET
  286. status=$1,
  287. changed_at=now()
  288. WHERE
  289. user_id=$2 AND feed_id=$3 AND status=$4 AND published_at < $5
  290. `
  291. result, err := s.db.Exec(query, model.EntryStatusRead, userID, feedID, model.EntryStatusUnread, before)
  292. if err != nil {
  293. return fmt.Errorf(`store: unable to mark feed entries as read: %v`, err)
  294. }
  295. count, _ := result.RowsAffected()
  296. logger.Debug("[Storage:MarkFeedAsRead] %d items marked as read", count)
  297. return nil
  298. }
  299. // MarkCategoryAsRead updates all category entries to the read status.
  300. func (s *Storage) MarkCategoryAsRead(userID, categoryID int64, before time.Time) error {
  301. query := `
  302. UPDATE
  303. entries
  304. SET
  305. status=$1,
  306. changed_at=now()
  307. WHERE
  308. user_id=$2
  309. AND
  310. status=$3
  311. AND
  312. published_at < $4
  313. AND
  314. feed_id IN (SELECT id FROM feeds WHERE user_id=$2 AND category_id=$5)
  315. `
  316. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, before, categoryID)
  317. if err != nil {
  318. return fmt.Errorf(`store: unable to mark category entries as read: %v`, err)
  319. }
  320. count, _ := result.RowsAffected()
  321. logger.Debug("[Storage:MarkCategoryAsRead] %d items marked as read", count)
  322. return nil
  323. }
  324. // EntryURLExists returns true if an entry with this URL already exists.
  325. func (s *Storage) EntryURLExists(feedID int64, entryURL string) bool {
  326. var result bool
  327. query := `SELECT true FROM entries WHERE feed_id=$1 AND url=$2`
  328. s.db.QueryRow(query, feedID, entryURL).Scan(&result)
  329. return result
  330. }
  331. // EntryShareCode returns the share code of the provided entry.
  332. // It generates a new one if not already defined.
  333. func (s *Storage) EntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
  334. query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2`
  335. err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode)
  336. if err != nil {
  337. err = fmt.Errorf(`store: unable to get share code for entry #%d: %v`, entryID, err)
  338. return
  339. }
  340. if shareCode == "" {
  341. shareCode = crypto.GenerateRandomStringHex(20)
  342. query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
  343. _, err = s.db.Exec(query, shareCode, userID, entryID)
  344. if err != nil {
  345. err = fmt.Errorf(`store: unable to set share code for entry #%d: %v`, entryID, err)
  346. return
  347. }
  348. }
  349. return
  350. }
  351. // UnshareEntry removes the share code for the given entry.
  352. func (s *Storage) UnshareEntry(userID int64, entryID int64) (err error) {
  353. query := `UPDATE entries SET share_code='' WHERE user_id=$1 AND id=$2`
  354. _, err = s.db.Exec(query, userID, entryID)
  355. if err != nil {
  356. err = fmt.Errorf(`store: unable to remove share code for entry #%d: %v`, entryID, err)
  357. }
  358. return
  359. }