fix: saving bookmarks inconsistencies (#500)

* chore: updated go-migrate dependencies

* fix: specify if we're saving bookmarks expecting a creation

up until now the SaveBookmarks method was doing some "magic" to do
"upserts" on the databases, but consistency between engines was scarce
and not knowing if we were expecting saving a new bookmark or updating
an existing one was leading to errors and inconsistencies in logic all
around the place. Now we need to specify a creation boolean when
saving and a differnt query will be make (INSERT vs UPDATE).

* fix(api): using incorrect bookmark for content downlaod

* test(db): added test pipeline for databases

Added functions that will share logic among the engines and will be
called on fresh databases on each test run

* dev: added basic docker-compose for development

* chore: uncommented tests

* ci(test): added mysql service

* typo

* test(mysql): select database after reset

* fix(mysql): ignore empty row errors when parsing tags

* fix(mysql): handle insert errors

* chore: added mysql variables to compose

* ci: explicit mysql service port exposed
This commit is contained in:
Felipe Martin Garcia 2022-10-11 23:47:38 +02:00 committed by GitHub
parent 040dc5c5d1
commit 05fee53bd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 385 additions and 83 deletions

View file

@ -21,6 +21,21 @@ jobs:
--health-retries 5
ports:
- 5432:5432
mariadb:
image: mariadb
env:
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
MYSQL_DATABASE: shiori
MYSQL_ROOT_PASSWORD: shiori
options: >-
--health-cmd="mysqladmin ping"
--health-interval=5s
--health-timeout=2s
--health-retries=3
ports:
- 3306:3306
name: Go ${{ matrix.go }} unit tests
steps:
- uses: actions/checkout@v2
@ -40,4 +55,5 @@ jobs:
- run: go test ./...
env:
SHIORI_TEST_PG_URL: "postgres://shiori:shiori@localhost:5432/shiori?sslmode=disable"
SHIORI_TEST_MYSQL_URL: "shiori:shiori@(localhost:3306)/shiori"
- run: CGO_ENABLED=0 go build -tags osusergo,netgo -ldflags="-s -w -X main.version=$(git describe --tags) -X main.date=$(date --iso-8601=seconds)"

6
Dockerfile.compose Normal file
View file

@ -0,0 +1,6 @@
FROM docker.io/golang:1.19-alpine3.16
WORKDIR /src/shiori
ENTRYPOINT ["go", "run", "main.go"]
CMD ["server"]

46
docker-compose.yaml Normal file
View file

@ -0,0 +1,46 @@
# Docker compose for development purposes only
version: "3"
services:
shiori:
build:
context: .
dockerfile: Dockerfile.compose
container_name: shiori
ports:
- "8080:8080"
volumes:
- "./dev-data:/srv/shiori"
- ".:/src/shiori"
restart: unless-stopped
links:
- "postgres"
- "mariadb"
environment:
SHIORI_DBMS: mysql
SHIORI_PG_USER: shiori
SHIORI_PG_PASS: shiori
SHIORI_PG_NAME: shiori
SHIORI_PG_HOST: postgres
SHIORI_PG_PORT: 5432
SHIORI_MYSQL_USER: shiori
SHIORI_MYSQL_PASS: shiori
SHIORI_MYSQL_NAME: shiori
SHIORI_MYSQL_ADDRESS: (mariadb)
postgres:
image: postgres:13
environment:
POSTGRES_PASSWORD: shiori
POSTGRES_USER: shiori
ports:
- "5432:5432"
mariadb:
image: mariadb:10.9
environment:
MYSQL_ROOT_PASSWORD: toor
MYSQL_DATABASE: shiori
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
ports:
- "3306:3306"

2
go.mod
View file

@ -47,7 +47,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

4
go.sum
View file

@ -1170,8 +1170,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=

View file

@ -111,7 +111,7 @@ func addHandler(cmd *cobra.Command, args []string) {
}
// Save bookmark to database
_, err = db.SaveBookmarks(cmd.Context(), book)
_, err = db.SaveBookmarks(cmd.Context(), true, book)
if err != nil {
cError.Printf("Failed to save bookmark: %v\n", err)
os.Exit(1)

View file

@ -154,7 +154,7 @@ func importHandler(cmd *cobra.Command, args []string) {
})
// Save bookmark to database
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
bookmarks, err = db.SaveBookmarks(cmd.Context(), true, bookmarks...)
if err != nil {
cError.Printf("Failed to save bookmarks: %v\n", err)
os.Exit(1)

View file

@ -112,7 +112,7 @@ func pocketHandler(cmd *cobra.Command, args []string) {
})
// Save bookmark to database
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
bookmarks, err = db.SaveBookmarks(cmd.Context(), true, bookmarks...)
if err != nil {
cError.Printf("Failed to save bookmarks: %v\n", err)
os.Exit(1)

View file

@ -285,7 +285,7 @@ func updateHandler(cmd *cobra.Command, args []string) {
}
// Save bookmarks to database
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
bookmarks, err = db.SaveBookmarks(cmd.Context(), false, bookmarks...)
if err != nil {
cError.Printf("Failed to save bookmark: %v\n", err)
os.Exit(1)

View file

@ -49,7 +49,7 @@ type DB interface {
Migrate() error
// SaveBookmarks saves bookmarks data to database.
SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error)
SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error)
// GetBookmarks fetch list of bookmarks based on submitted options.
GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.Bookmark, error)

View file

@ -0,0 +1,103 @@
package database
import (
"context"
"testing"
"github.com/go-shiori/shiori/internal/model"
"github.com/stretchr/testify/assert"
)
type databaseTestCase func(t *testing.T, db DB)
type testDatabaseFactory func(ctx context.Context) (DB, error)
func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
tests := map[string]databaseTestCase{
"testCreateBookmark": testCreateBookmark,
"testCreateBookmarkTwice": testCreateBookmarkTwice,
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
"testUpdateBookmark": testUpdateBookmark,
}
for testName, testCase := range tests {
t.Run(testName, func(tInner *testing.T) {
ctx := context.TODO()
db, err := dbFactory(ctx)
assert.NoError(tInner, err, "Error recreating database")
testCase(tInner, db)
})
}
}
func testCreateBookmark(t *testing.T, db DB) {
ctx := context.TODO()
book := model.Bookmark{
URL: "https://github.com/go-shiori/obelisk",
Title: "shiori",
}
result, err := db.SaveBookmarks(ctx, true, book)
assert.NoError(t, err, "Save bookmarks must not fail")
assert.Equal(t, 1, result[0].ID, "Saved bookmark must have an ID set")
}
func testCreateBookmarkWithTag(t *testing.T, db DB) {
ctx := context.TODO()
book := model.Bookmark{
URL: "https://github.com/go-shiori/obelisk",
Title: "shiori",
Tags: []model.Tag{
{
Name: "test-tag",
},
},
}
result, err := db.SaveBookmarks(ctx, true, book)
assert.NoError(t, err, "Save bookmarks must not fail")
assert.Equal(t, book.URL, result[0].URL)
assert.Equal(t, book.Tags[0].Name, result[0].Tags[0].Name)
}
func testCreateBookmarkTwice(t *testing.T, db DB) {
ctx := context.TODO()
book := model.Bookmark{
URL: "https://github.com/go-shiori/shiori",
Title: "shiori",
}
result, err := db.SaveBookmarks(ctx, true, book)
assert.NoError(t, err, "Save bookmarks must not fail")
savedBookmark := result[0]
savedBookmark.Title = "modified"
_, err = db.SaveBookmarks(ctx, true, savedBookmark)
assert.Error(t, err, "Save bookmarks must fail")
}
func testUpdateBookmark(t *testing.T, db DB) {
ctx := context.TODO()
book := model.Bookmark{
URL: "https://github.com/go-shiori/shiori",
Title: "shiori",
}
result, err := db.SaveBookmarks(ctx, true, book)
assert.NoError(t, err, "Save bookmarks must not fail")
savedBookmark := result[0]
savedBookmark.Title = "modified"
result, err = db.SaveBookmarks(ctx, false, savedBookmark)
assert.NoError(t, err, "Save bookmarks must not fail")
assert.Equal(t, "modified", result[0].Title)
assert.Equal(t, savedBookmark.ID, result[0].ID)
}

View file

@ -64,23 +64,27 @@ func (db *MySQLDatabase) Migrate() error {
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
var result []model.Bookmark
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
// Prepare statement
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
(id, url, title, excerpt, author, public, content, html, modified)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
url = VALUES(url),
title = VALUES(title),
excerpt = VALUES(excerpt),
author = VALUES(author),
public = VALUES(public),
content = VALUES(content),
html = VALUES(html),
modified = VALUES(modified)`)
(url, title, excerpt, author, public, content, html, modified)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return errors.WithStack(err)
}
stmtUpdateBook, err := tx.Preparex(`UPDATE bookmark
SET url = ?,
title = ?,
excerpt = ?,
author = ?,
public = ?,
content = ?,
html = ?,
modified = ?`)
if err != nil {
return errors.WithStack(err)
}
@ -113,11 +117,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
// Execute statements
for _, book := range bookmarks {
// Check ID, URL and title
if book.ID == 0 {
return errors.New("ID must not be empty")
}
// Check URL and title
if book.URL == "" {
return errors.New("URL must not be empty")
}
@ -132,9 +132,25 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
}
// Save bookmark
_, err := stmtInsertBook.ExecContext(ctx, book.ID,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified)
var err error
if create {
var res sql.Result
res, err = stmtInsertBook.ExecContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified)
if err != nil {
return errors.WithStack(err)
}
bookID, err := res.LastInsertId()
if err != nil {
return errors.WithStack(err)
}
book.ID = int(bookID)
} else {
_, err = stmtUpdateBook.ExecContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified)
}
if err != nil {
return errors.WithStack(err)
}
@ -158,8 +174,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
// If tag doesn't have any ID, fetch it from database
if tag.ID == 0 {
err = stmtGetTag.Get(&tag.ID, tagName)
if err != nil {
if err := stmtGetTag.GetContext(ctx, &tag.ID, tagName); err != nil && err != sql.ErrNoRows {
return errors.WithStack(err)
}

View file

@ -0,0 +1,60 @@
package database
import (
"context"
"log"
"os"
"testing"
"github.com/golang-migrate/migrate/v4"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
func init() {
connString := os.Getenv("SHIORI_TEST_MYSQL_URL")
if connString == "" {
log.Fatal("mysql tests can't run without a MysQL database, set SHIORI_TEST_MYSQL_URL environment variable")
}
}
func mysqlTestDatabaseFactory(ctx context.Context) (DB, error) {
connString := os.Getenv("SHIORI_TEST_MYSQL_URL")
db, err := OpenMySQLDatabase(ctx, connString)
if err != nil {
return nil, err
}
var dbname string
err = db.withTx(ctx, func(tx *sqlx.Tx) error {
err := tx.QueryRow("SELECT DATABASE()").Scan(&dbname)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DROP DATABASE IF EXISTS "+dbname)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "CREATE DATABASE "+dbname)
return err
})
if err != nil {
return nil, err
}
if _, err := db.Exec("USE " + dbname); err != nil {
return nil, err
}
if err = db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
return nil, err
}
return db, err
}
func TestMysqlsDatabase(t *testing.T) {
testDatabase(t, mysqlTestDatabaseFactory)
}

View file

@ -64,14 +64,19 @@ func (db *PGDatabase) Migrate() error {
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {
func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {
result = []model.Bookmark{}
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
// Prepare statement
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
(url, title, excerpt, author, public, content, html, modified)
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT(url) DO UPDATE SET
RETURNING id`)
if err != nil {
return errors.WithStack(err)
}
stmtUpdateBook, err := tx.Preparex(`UPDATE bookmark SET
url = $1,
title = $2,
excerpt = $3,
@ -79,8 +84,7 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Book
public = $5,
content = $6,
html = $7,
modified = $8
RETURNING id`)
modified = $8`)
if err != nil {
return errors.WithStack(err)
}
@ -128,9 +132,16 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Book
}
// Save bookmark
err := stmtInsertBook.QueryRowContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
var err error
if create {
err = stmtInsertBook.QueryRowContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
} else {
_, err = stmtUpdateBook.ExecContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author,
book.Public, book.Content, book.HTML, book.Modified)
}
if err != nil {
return errors.WithStack(err)
}

View file

@ -7,42 +7,34 @@ import (
"os"
"testing"
"github.com/go-shiori/shiori/internal/model"
"github.com/golang-migrate/migrate/v4"
"github.com/stretchr/testify/assert"
)
func init() {
testPsqlURL := os.Getenv("SHIORI_TEST_PG_URL")
if testPsqlURL == "" {
log.Fatal("psql tests can't run without a PSQL database")
connString := os.Getenv("SHIORI_TEST_PG_URL")
if connString == "" {
log.Fatal("psql tests can't run without a PSQL database, set SHIORI_TEST_PG_URL environment variable")
}
}
func TestPsqlSaveBookmarkWithTag(t *testing.T) {
ctx := context.TODO()
pgDB, err := OpenPGDatabase(ctx, os.Getenv("SHIORI_TEST_PG_URL"))
func postgresqlTestDatabaseFactory(ctx context.Context) (DB, error) {
db, err := OpenPGDatabase(ctx, os.Getenv("SHIORI_TEST_PG_URL"))
if err != nil {
t.Error(err)
return nil, err
}
if err := pgDB.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
t.Error(err)
_, err = db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
if err != nil {
return nil, err
}
book := model.Bookmark{
URL: "https://github.com/go-shiori/obelisk",
Title: "shiori",
Tags: []model.Tag{
{
Name: "test-tag",
},
},
if err := db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
return nil, err
}
result, err := pgDB.SaveBookmarks(ctx, book)
assert.NoError(t, err, "Save bookmarks must not fail")
assert.Equal(t, book.URL, result[0].URL)
assert.Equal(t, book.Tags[0].Name, result[0].Tags[0].Name)
return db, nil
}
func TestPostgresDatabase(t *testing.T) {
testDatabase(t, postgresqlTestDatabaseFactory)
}

View file

@ -73,15 +73,20 @@ func (db *SQLiteDatabase) Migrate() error {
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
var result []model.Bookmark
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
// Prepare statement
stmtInsertBook, err := tx.PreparexContext(ctx, `INSERT INTO bookmark
(id, url, title, excerpt, author, public, modified, has_content)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
(url, title, excerpt, author, public, modified, has_content)
VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING id`)
if err != nil {
return errors.WithStack(err)
}
stmtUpdateBook, err := tx.PreparexContext(ctx, `UPDATE bookmark SET
url = ?, title = ?, excerpt = ?, author = ?,
public = ?, modified = ?, has_content = ?`)
if err != nil {
@ -130,11 +135,7 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
// Execute statements
for _, book := range bookmarks {
// Check ID, URL and title
if book.ID == 0 {
return errors.New("ID must not be empty")
}
// Check URL and title
if book.URL == "" {
return errors.New("URL must not be empty")
}
@ -148,11 +149,18 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
book.Modified = modifiedTime
}
// Save bookmark
hasContent := book.Content != ""
_, err = stmtInsertBook.ExecContext(ctx, book.ID,
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent,
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent)
// Create or update bookmark
var err error
if create {
err = stmtInsertBook.QueryRowContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent).Scan(&book.ID)
} else {
_, err = stmtUpdateBook.ExecContext(ctx,
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent)
}
if err != nil {
return errors.WithStack(err)
}
@ -169,6 +177,13 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
return errors.WithStack(err)
}
bookID, err := res.LastInsertId()
if err != nil {
return errors.WithStack(err)
}
book.ID = int(bookID)
if rows == 0 {
_, err = stmtInsertBookContent.ExecContext(ctx, book.ID, book.Title, book.Content, book.HTML)
if err != nil {

View file

@ -0,0 +1,36 @@
package database
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/golang-migrate/migrate/v4"
"github.com/pkg/errors"
)
var sqliteDatabaseTestPath string
func init() {
sqliteDatabaseTestPath = filepath.Join(os.TempDir(), "shiori.db")
}
func sqliteTestDatabaseFactory(ctx context.Context) (DB, error) {
os.Remove(sqliteDatabaseTestPath)
db, err := OpenSQLiteDatabase(ctx, sqliteDatabaseTestPath)
if err != nil {
return nil, err
}
if err := db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
return nil, err
}
return db, nil
}
func TestSqliteDatabase(t *testing.T) {
testDatabase(t, sqliteTestDatabaseFactory)
}

View file

@ -98,12 +98,12 @@ func (h *handler) apiInsertViaExtension(w http.ResponseWriter, r *http.Request,
panic(fmt.Errorf("failed to process bookmark: %v", err))
}
}
if _, err := h.DB.SaveBookmarks(ctx, book); err != nil {
if _, err := h.DB.SaveBookmarks(ctx, true, book); err != nil {
log.Printf("error saving bookmark after downloading content: %s", err)
}
// Save bookmark to database
results, err := h.DB.SaveBookmarks(ctx, book)
results, err := h.DB.SaveBookmarks(ctx, false, book)
if err != nil || len(results) == 0 {
panic(fmt.Errorf("failed to save bookmark: %v", err))
}

View file

@ -316,18 +316,20 @@ func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps h
}
// Save bookmark to database
results, err := h.DB.SaveBookmarks(ctx, *book)
results, err := h.DB.SaveBookmarks(ctx, true, *book)
if err != nil || len(results) == 0 {
panic(fmt.Errorf("failed to save bookmark: %v", err))
}
book = &results[0]
if payload.Async {
go func() {
bookmark, err := downloadBookmarkContent(&results[0], h.DataDir, r)
bookmark, err := downloadBookmarkContent(book, h.DataDir, r)
if err != nil {
log.Printf("error downloading boorkmark: %s", err)
}
if _, err := h.DB.SaveBookmarks(context.Background(), *bookmark); err != nil {
if _, err := h.DB.SaveBookmarks(context.Background(), false, *bookmark); err != nil {
log.Printf("failed to save bookmark: %s", err)
}
}()
@ -338,7 +340,7 @@ func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps h
if err != nil {
log.Printf("error downloading boorkmark: %s", err)
}
if _, err := h.DB.SaveBookmarks(ctx, *book); err != nil {
if _, err := h.DB.SaveBookmarks(ctx, false, *book); err != nil {
log.Printf("failed to save bookmark: %s", err)
}
}
@ -442,7 +444,7 @@ func (h *handler) apiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
}
// Update database
res, err := h.DB.SaveBookmarks(ctx, book)
res, err := h.DB.SaveBookmarks(ctx, false, book)
checkError(err)
// Add thumbnail image to the saved bookmarks again
@ -566,7 +568,7 @@ func (h *handler) apiUpdateCache(w http.ResponseWriter, r *http.Request, ps http
close(chDone)
// Update database
_, err = h.DB.SaveBookmarks(ctx, bookmarks...)
_, err = h.DB.SaveBookmarks(ctx, false, bookmarks...)
checkError(err)
// Return new saved result
@ -628,7 +630,7 @@ func (h *handler) apiUpdateBookmarkTags(w http.ResponseWriter, r *http.Request,
}
// Update database
bookmarks, err = h.DB.SaveBookmarks(ctx, bookmarks...)
bookmarks, err = h.DB.SaveBookmarks(ctx, false, bookmarks...)
checkError(err)
// Get image URL for each bookmark