entry.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package storage // import "miniflux.app/v2/internal/storage"
  4. import (
  5. "database/sql"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "time"
  10. "miniflux.app/v2/internal/crypto"
  11. "miniflux.app/v2/internal/model"
  12. "github.com/lib/pq"
  13. )
  14. // ErrEntryTombstoned is returned when an entry cannot be created because its
  15. // (feed_id, hash) pair has a tombstone recording a prior deletion.
  16. var ErrEntryTombstoned = errors.New("store: entry is tombstoned")
  17. // CountAllEntries returns the number of entries for each status in the database.
  18. func (s *Storage) CountAllEntries() (map[string]int64, error) {
  19. rows, err := s.db.Query(`SELECT status, count(*) FROM entries GROUP BY status`)
  20. if err != nil {
  21. return nil, fmt.Errorf("storage: unable to count entries: %w", err)
  22. }
  23. defer rows.Close()
  24. results := make(map[string]int64)
  25. results[model.EntryStatusUnread] = 0
  26. results[model.EntryStatusRead] = 0
  27. for rows.Next() {
  28. var status string
  29. var count int64
  30. if err := rows.Scan(&status, &count); err != nil {
  31. continue
  32. }
  33. results[status] = count
  34. }
  35. results["total"] = results[model.EntryStatusUnread] + results[model.EntryStatusRead]
  36. return results, nil
  37. }
  38. // CountUnreadEntries returns the number of unread entries.
  39. func (s *Storage) CountUnreadEntries(userID int64) int {
  40. builder := s.NewEntryQueryBuilder(userID)
  41. builder.WithStatus(model.EntryStatusUnread)
  42. builder.WithGloballyVisible()
  43. n, err := builder.CountEntries()
  44. if err != nil {
  45. slog.Error("Unable to count unread entries",
  46. slog.Int64("user_id", userID),
  47. slog.Any("error", err),
  48. )
  49. return 0
  50. }
  51. return n
  52. }
  53. // NewEntryQueryBuilder returns a new EntryQueryBuilder
  54. func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
  55. return NewEntryQueryBuilder(s, userID)
  56. }
  57. // UpdateEntryTitleAndContent updates entry title and content.
  58. func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
  59. truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
  60. query := `
  61. UPDATE
  62. entries
  63. SET
  64. title=$1,
  65. content=$2,
  66. reading_time=$3,
  67. document_vectors = setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')
  68. WHERE
  69. id=$6 AND user_id=$7
  70. `
  71. if _, err := s.db.Exec(
  72. query,
  73. entry.Title,
  74. entry.Content,
  75. entry.ReadingTime,
  76. truncatedTitle,
  77. truncatedContent,
  78. entry.ID,
  79. entry.UserID); err != nil {
  80. return fmt.Errorf(`store: unable to update entry #%d: %v`, entry.ID, err)
  81. }
  82. return nil
  83. }
  84. // createEntry add a new entry.
  85. func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
  86. truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
  87. // The WHERE NOT EXISTS guard makes the tombstone check atomic with the insert, so a
  88. // concurrent archive committing between an earlier existence check and this statement
  89. // cannot bring a deleted entry back as unread.
  90. query := `
  91. INSERT INTO entries
  92. (
  93. title,
  94. hash,
  95. url,
  96. comments_url,
  97. published_at,
  98. content,
  99. author,
  100. user_id,
  101. feed_id,
  102. reading_time,
  103. changed_at,
  104. document_vectors,
  105. tags
  106. )
  107. SELECT
  108. $1,
  109. $2,
  110. $3,
  111. $4,
  112. $5,
  113. $6,
  114. $7,
  115. $8,
  116. $9,
  117. $10,
  118. now(),
  119. setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B'),
  120. $13
  121. WHERE NOT EXISTS (
  122. SELECT 1 FROM entry_tombstones WHERE feed_id=$9 AND hash=$2
  123. )
  124. RETURNING
  125. id, status, created_at, changed_at
  126. `
  127. err := tx.QueryRow(
  128. query,
  129. entry.Title,
  130. entry.Hash,
  131. entry.URL,
  132. entry.CommentsURL,
  133. entry.Date,
  134. entry.Content,
  135. entry.Author,
  136. entry.UserID,
  137. entry.FeedID,
  138. entry.ReadingTime,
  139. truncatedTitle,
  140. truncatedContent,
  141. pq.Array(entry.Tags),
  142. ).Scan(
  143. &entry.ID,
  144. &entry.Status,
  145. &entry.CreatedAt,
  146. &entry.ChangedAt,
  147. )
  148. if errors.Is(err, sql.ErrNoRows) {
  149. return ErrEntryTombstoned
  150. }
  151. if err != nil {
  152. return fmt.Errorf(`store: unable to create entry %q (feed #%d): %v`, entry.URL, entry.FeedID, err)
  153. }
  154. for _, enclosure := range entry.Enclosures {
  155. enclosure.EntryID = entry.ID
  156. enclosure.UserID = entry.UserID
  157. err := s.createEnclosure(tx, enclosure)
  158. if err != nil {
  159. return err
  160. }
  161. }
  162. return nil
  163. }
  164. // updateEntry updates an entry when a feed is refreshed.
  165. // Note: we do not update the published date because some feeds do not contains any date,
  166. // it default to time.Now() which could change the order of items on the history page.
  167. func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
  168. truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
  169. query := `
  170. UPDATE
  171. entries
  172. SET
  173. title=$1,
  174. url=$2,
  175. comments_url=$3,
  176. content=$4,
  177. author=$5,
  178. reading_time=$6,
  179. document_vectors = setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B'),
  180. tags=$12
  181. WHERE
  182. user_id=$9 AND feed_id=$10 AND hash=$11
  183. RETURNING
  184. id
  185. `
  186. err := tx.QueryRow(
  187. query,
  188. entry.Title,
  189. entry.URL,
  190. entry.CommentsURL,
  191. entry.Content,
  192. entry.Author,
  193. entry.ReadingTime,
  194. truncatedTitle,
  195. truncatedContent,
  196. entry.UserID,
  197. entry.FeedID,
  198. entry.Hash,
  199. pq.Array(entry.Tags),
  200. ).Scan(&entry.ID)
  201. if err != nil {
  202. return fmt.Errorf(`store: unable to update entry %q: %v`, entry.URL, err)
  203. }
  204. for _, enclosure := range entry.Enclosures {
  205. enclosure.UserID = entry.UserID
  206. enclosure.EntryID = entry.ID
  207. }
  208. return s.updateEnclosures(tx, entry)
  209. }
  210. // entryExists checks if an entry already exists based on its hash when refreshing a feed.
  211. func (s *Storage) entryExists(tx *sql.Tx, entry *model.Entry) (bool, error) {
  212. var result bool
  213. // Note: This query uses entries_feed_id_hash_key index (filtering on user_id is not necessary).
  214. err := tx.QueryRow(`SELECT true FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`, entry.FeedID, entry.Hash).Scan(&result)
  215. if err != nil && err != sql.ErrNoRows {
  216. return result, fmt.Errorf(`store: unable to check if entry exists: %v`, err)
  217. }
  218. return result, nil
  219. }
  220. func (s *Storage) getEntryIDByHash(tx *sql.Tx, feedID int64, entryHash string) (int64, error) {
  221. var entryID int64
  222. err := tx.QueryRow(
  223. `SELECT id FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`,
  224. feedID,
  225. entryHash,
  226. ).Scan(&entryID)
  227. if err == sql.ErrNoRows {
  228. return 0, nil
  229. }
  230. if err != nil {
  231. return 0, fmt.Errorf(`store: unable to fetch entry ID: %v`, err)
  232. }
  233. return entryID, nil
  234. }
  235. // InsertEntryForFeed inserts a single entry into a feed, optionally updating if it already exists.
  236. // Returns true if a new entry was created, false if an existing one was reused.
  237. func (s *Storage) InsertEntryForFeed(userID, feedID int64, entry *model.Entry) (bool, error) {
  238. entry.UserID = userID
  239. entry.FeedID = feedID
  240. tx, err := s.db.Begin()
  241. if err != nil {
  242. return false, fmt.Errorf("store: unable to start transaction: %v", err)
  243. }
  244. defer tx.Rollback()
  245. entryID, err := s.getEntryIDByHash(tx, entry.FeedID, entry.Hash)
  246. if err != nil {
  247. return false, err
  248. }
  249. alreadyExistingEntry := entryID > 0
  250. if alreadyExistingEntry {
  251. entry.ID = entryID
  252. } else {
  253. if err := s.createEntry(tx, entry); err != nil {
  254. return false, err
  255. }
  256. }
  257. if err := tx.Commit(); err != nil {
  258. return false, err
  259. }
  260. return !alreadyExistingEntry, nil
  261. }
  262. func (s *Storage) IsNewEntry(feedID int64, entryHash string) bool {
  263. // An entry is new only if it is neither stored nor tombstoned; otherwise
  264. // callers (such as the crawler) would do expensive work on every refresh
  265. // for items that will be discarded.
  266. query := `
  267. SELECT
  268. EXISTS (
  269. SELECT 1 FROM entries WHERE feed_id=$1 AND hash=$2
  270. ) OR EXISTS (
  271. SELECT 1 FROM entry_tombstones WHERE feed_id=$1 AND hash=$2
  272. )
  273. `
  274. var known bool
  275. s.db.QueryRow(query, feedID, entryHash).Scan(&known)
  276. return !known
  277. }
  278. func (s *Storage) GetReadTime(feedID int64, entryHash string) int {
  279. var result int
  280. // Note: This query uses entries_feed_id_hash_key index
  281. s.db.QueryRow(
  282. `SELECT
  283. reading_time
  284. FROM
  285. entries
  286. WHERE
  287. feed_id=$1 AND
  288. hash=$2
  289. `,
  290. feedID,
  291. entryHash,
  292. ).Scan(&result)
  293. return result
  294. }
  295. // RefreshFeedEntries updates feed entries while refreshing a feed.
  296. func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (newEntries model.Entries, err error) {
  297. for _, entry := range entries {
  298. entry.UserID = userID
  299. entry.FeedID = feedID
  300. tx, err := s.db.Begin()
  301. if err != nil {
  302. return nil, fmt.Errorf(`store: unable to start transaction: %v`, err)
  303. }
  304. entryExists, err := s.entryExists(tx, entry)
  305. if err != nil {
  306. if rollbackErr := tx.Rollback(); rollbackErr != nil {
  307. return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
  308. }
  309. return nil, err
  310. }
  311. if entryExists {
  312. if updateExistingEntries {
  313. err = s.updateEntry(tx, entry)
  314. }
  315. } else {
  316. err = s.createEntry(tx, entry)
  317. switch {
  318. case errors.Is(err, ErrEntryTombstoned):
  319. err = nil
  320. case err == nil:
  321. newEntries = append(newEntries, entry)
  322. }
  323. }
  324. if err != nil {
  325. if rollbackErr := tx.Rollback(); rollbackErr != nil {
  326. return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
  327. }
  328. return nil, err
  329. }
  330. if err := tx.Commit(); err != nil {
  331. return nil, fmt.Errorf(`store: unable to commit transaction: %v`, err)
  332. }
  333. }
  334. return newEntries, nil
  335. }
  336. // ArchiveEntries deletes entries older than the given interval and records tombstones so they are not re-ingested.
  337. func (s *Storage) ArchiveEntries(status string, interval time.Duration, limit int) (int64, error) {
  338. if interval < 0 || limit <= 0 {
  339. return 0, nil
  340. }
  341. query := `
  342. WITH to_delete AS (
  343. SELECT id, feed_id, hash
  344. FROM entries
  345. WHERE
  346. status=$1 AND
  347. starred is false AND
  348. share_code='' AND
  349. created_at < now() - $2::interval
  350. ORDER BY created_at ASC
  351. FOR UPDATE SKIP LOCKED
  352. LIMIT $3
  353. ), deleted AS (
  354. DELETE FROM entries
  355. USING to_delete
  356. WHERE entries.id = to_delete.id
  357. RETURNING entries.feed_id, entries.hash
  358. )
  359. INSERT INTO entry_tombstones (feed_id, hash)
  360. SELECT feed_id, hash FROM deleted WHERE hash <> ''
  361. ON CONFLICT (feed_id, hash) DO NOTHING
  362. `
  363. days := max(int(interval/(24*time.Hour)), 1)
  364. result, err := s.db.Exec(query, status, fmt.Sprintf("%d days", days), limit)
  365. if err != nil {
  366. return 0, fmt.Errorf(`store: unable to archive %s entries: %v`, status, err)
  367. }
  368. count, err := result.RowsAffected()
  369. if err != nil {
  370. return 0, fmt.Errorf(`store: unable to get the number of rows affected: %v`, err)
  371. }
  372. return count, nil
  373. }
  374. // SetEntriesStatus update the status of the given list of entries.
  375. func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string) error {
  376. query := `
  377. UPDATE
  378. entries
  379. SET
  380. status=$1,
  381. changed_at=now()
  382. WHERE
  383. user_id=$2 AND
  384. id=ANY($3)
  385. `
  386. if _, err := s.db.Exec(query, status, userID, pq.Array(entryIDs)); err != nil {
  387. return fmt.Errorf(`store: unable to update entries statuses %v: %v`, entryIDs, err)
  388. }
  389. return nil
  390. }
  391. // SetEntriesStatusAndCountVisible updates the status of the given entries and returns how many are visible in global views.
  392. func (s *Storage) SetEntriesStatusAndCountVisible(userID int64, entryIDs []int64, status string) (int, error) {
  393. query := `
  394. WITH updated AS (
  395. UPDATE entries
  396. SET
  397. status=$1,
  398. changed_at=now()
  399. WHERE
  400. user_id=$2 AND
  401. id=ANY($3)
  402. RETURNING feed_id
  403. )
  404. SELECT count(*)
  405. FROM updated u
  406. JOIN feeds f ON (f.id = u.feed_id)
  407. JOIN categories c ON (c.id = f.category_id)
  408. WHERE NOT f.hide_globally AND NOT c.hide_globally
  409. `
  410. var visible int
  411. if err := s.db.QueryRow(query, status, userID, pq.Array(entryIDs)).Scan(&visible); err != nil {
  412. return 0, fmt.Errorf(`store: unable to update entries status %v: %v`, entryIDs, err)
  413. }
  414. return visible, nil
  415. }
  416. // SetEntriesStarredState updates the starred state for the given list of entries.
  417. func (s *Storage) SetEntriesStarredState(userID int64, entryIDs []int64, starred bool) error {
  418. query := `UPDATE entries SET starred=$1, changed_at=now() WHERE user_id=$2 AND id=ANY($3)`
  419. result, err := s.db.Exec(query, starred, userID, pq.Array(entryIDs))
  420. if err != nil {
  421. return fmt.Errorf(`store: unable to update the starred state %v: %v`, entryIDs, err)
  422. }
  423. count, err := result.RowsAffected()
  424. if err != nil {
  425. return fmt.Errorf(`store: unable to update these entries %v: %v`, entryIDs, err)
  426. }
  427. if count == 0 {
  428. return errors.New(`store: nothing has been updated`)
  429. }
  430. return nil
  431. }
  432. // ToggleStarred toggles entry starred value.
  433. func (s *Storage) ToggleStarred(userID int64, entryID int64) error {
  434. query := `UPDATE entries SET starred = NOT starred, changed_at=now() WHERE user_id=$1 AND id=$2`
  435. result, err := s.db.Exec(query, userID, entryID)
  436. if err != nil {
  437. return fmt.Errorf(`store: unable to toggle starred flag for entry #%d: %v`, entryID, err)
  438. }
  439. count, err := result.RowsAffected()
  440. if err != nil {
  441. return fmt.Errorf(`store: unable to toggle starred flag for entry #%d: %v`, entryID, err)
  442. }
  443. if count == 0 {
  444. return errors.New(`store: nothing has been updated`)
  445. }
  446. return nil
  447. }
  448. // FlushHistory deletes all read entries (non-starred, non-shared) and records tombstones to prevent re-ingestion.
  449. func (s *Storage) FlushHistory(userID int64) error {
  450. query := `
  451. WITH deleted AS (
  452. DELETE FROM entries
  453. WHERE user_id=$1 AND status=$2 AND starred is false AND share_code=''
  454. RETURNING feed_id, hash
  455. )
  456. INSERT INTO entry_tombstones (feed_id, hash)
  457. SELECT feed_id, hash FROM deleted WHERE hash <> ''
  458. ON CONFLICT (feed_id, hash) DO NOTHING
  459. `
  460. if _, err := s.db.Exec(query, userID, model.EntryStatusRead); err != nil {
  461. return fmt.Errorf(`store: unable to flush history: %v`, err)
  462. }
  463. return nil
  464. }
  465. // MarkAllAsRead updates all user entries to the read status.
  466. func (s *Storage) MarkAllAsRead(userID int64) error {
  467. query := `UPDATE entries SET status=$1, changed_at=now() WHERE user_id=$2 AND status=$3`
  468. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread)
  469. if err != nil {
  470. return fmt.Errorf(`store: unable to mark all entries as read: %v`, err)
  471. }
  472. count, _ := result.RowsAffected()
  473. slog.Debug("Marked all entries as read",
  474. slog.Int64("user_id", userID),
  475. slog.Int64("nb_entries", count),
  476. )
  477. return nil
  478. }
  479. // MarkAllAsReadBeforeDate updates all user entries to the read status before the given date.
  480. func (s *Storage) MarkAllAsReadBeforeDate(userID int64, before time.Time) error {
  481. query := `
  482. UPDATE
  483. entries
  484. SET
  485. status=$1,
  486. changed_at=now()
  487. WHERE
  488. user_id=$2 AND status=$3 AND published_at < $4
  489. `
  490. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, before)
  491. if err != nil {
  492. return fmt.Errorf(`store: unable to mark all entries as read before %s: %v`, before.Format(time.RFC3339), err)
  493. }
  494. count, _ := result.RowsAffected()
  495. slog.Debug("Marked all entries as read before date",
  496. slog.Int64("user_id", userID),
  497. slog.Int64("nb_entries", count),
  498. slog.String("before", before.Format(time.RFC3339)),
  499. )
  500. return nil
  501. }
  502. // MarkGloballyVisibleFeedsAsRead updates all user entries to the read status.
  503. func (s *Storage) MarkGloballyVisibleFeedsAsRead(userID int64) error {
  504. query := `
  505. UPDATE
  506. entries
  507. SET
  508. status=$1,
  509. changed_at=now()
  510. FROM
  511. feeds
  512. WHERE
  513. entries.feed_id = feeds.id
  514. AND entries.user_id=$2
  515. AND entries.status=$3
  516. AND feeds.hide_globally=$4
  517. `
  518. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, false)
  519. if err != nil {
  520. return fmt.Errorf(`store: unable to mark globally visible feeds as read: %v`, err)
  521. }
  522. count, _ := result.RowsAffected()
  523. slog.Debug("Marked globally visible feed entries as read",
  524. slog.Int64("user_id", userID),
  525. slog.Int64("nb_entries", count),
  526. )
  527. return nil
  528. }
  529. // MarkFeedAsRead updates all feed entries to the read status.
  530. func (s *Storage) MarkFeedAsRead(userID, feedID int64, before time.Time) error {
  531. query := `
  532. UPDATE
  533. entries
  534. SET
  535. status=$1,
  536. changed_at=now()
  537. WHERE
  538. user_id=$2 AND feed_id=$3 AND status=$4 AND published_at < $5
  539. `
  540. result, err := s.db.Exec(query, model.EntryStatusRead, userID, feedID, model.EntryStatusUnread, before)
  541. if err != nil {
  542. return fmt.Errorf(`store: unable to mark feed entries as read: %v`, err)
  543. }
  544. count, _ := result.RowsAffected()
  545. slog.Debug("Marked feed entries as read",
  546. slog.Int64("user_id", userID),
  547. slog.Int64("feed_id", feedID),
  548. slog.Int64("nb_entries", count),
  549. slog.String("before", before.Format(time.RFC3339)),
  550. )
  551. return nil
  552. }
  553. // MarkCategoryAsRead updates all category entries to the read status.
  554. func (s *Storage) MarkCategoryAsRead(userID, categoryID int64, before time.Time) error {
  555. query := `
  556. UPDATE
  557. entries
  558. SET
  559. status=$1,
  560. changed_at=now()
  561. FROM
  562. feeds
  563. WHERE
  564. feed_id=feeds.id
  565. AND
  566. feeds.user_id=$2
  567. AND
  568. status=$3
  569. AND
  570. published_at < $4
  571. AND
  572. feeds.category_id=$5
  573. `
  574. result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, before, categoryID)
  575. if err != nil {
  576. return fmt.Errorf(`store: unable to mark category entries as read: %v`, err)
  577. }
  578. count, _ := result.RowsAffected()
  579. slog.Debug("Marked category entries as read",
  580. slog.Int64("user_id", userID),
  581. slog.Int64("category_id", categoryID),
  582. slog.Int64("nb_entries", count),
  583. slog.String("before", before.Format(time.RFC3339)),
  584. )
  585. return nil
  586. }
  587. // EntryShareCode returns the share code of the provided entry.
  588. // It generates a new one if not already defined.
  589. func (s *Storage) EntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
  590. query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2`
  591. err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode)
  592. if err != nil {
  593. err = fmt.Errorf(`store: unable to get share code for entry #%d: %v`, entryID, err)
  594. return
  595. }
  596. if shareCode == "" {
  597. shareCode = crypto.GenerateRandomStringHex(20)
  598. query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
  599. _, err = s.db.Exec(query, shareCode, userID, entryID)
  600. if err != nil {
  601. err = fmt.Errorf(`store: unable to set share code for entry #%d: %v`, entryID, err)
  602. return
  603. }
  604. }
  605. return
  606. }
  607. // UnshareEntry removes the share code for the given entry.
  608. func (s *Storage) UnshareEntry(userID int64, entryID int64) (err error) {
  609. query := `UPDATE entries SET share_code='' WHERE user_id=$1 AND id=$2`
  610. _, err = s.db.Exec(query, userID, entryID)
  611. if err != nil {
  612. err = fmt.Errorf(`store: unable to remove share code for entry #%d: %v`, entryID, err)
  613. }
  614. return
  615. }
  616. func truncateTitleAndContentForTSVectorField(title, content string) (string, string) {
  617. // The length of a tsvector (lexemes + positions) must be less than 1 megabyte.
  618. // We don't need to index the entire content, and we need to keep a buffer for the positions.
  619. return truncateStringForTSVectorField(title, 200000), truncateStringForTSVectorField(content, 500000)
  620. }
  621. // truncateStringForTSVectorField truncates a string and don't break UTF-8 characters.
  622. func truncateStringForTSVectorField(s string, maxSize int) string {
  623. if len(s) < maxSize {
  624. return s
  625. }
  626. // Truncate to fit under the limit, ensuring we don't break UTF-8 characters
  627. truncated := s[:maxSize-1]
  628. // Walk backwards to find the last complete UTF-8 character
  629. for i := len(truncated) - 1; i >= 0; i-- {
  630. if (truncated[i] & 0x80) == 0 {
  631. // ASCII character, we can stop here
  632. return truncated[:i+1]
  633. }
  634. if (truncated[i] & 0xC0) == 0xC0 {
  635. // Start of a multi-byte UTF-8 character
  636. return truncated[:i]
  637. }
  638. }
  639. // Fallback: return empty string if we can't find a valid UTF-8 boundary
  640. return ""
  641. }