shiori/database/sqlite.go

555 lines
14 KiB
Go
Raw Normal View History

package database
import (
"database/sql"
2018-01-28 15:53:37 +08:00
"fmt"
"strings"
2018-02-25 17:04:12 +08:00
"time"
2018-03-03 17:50:54 +08:00
"github.com/RadhiFadlillah/shiori/model"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
2018-02-03 21:20:10 +08:00
// SQLiteDatabase is implementation of Database interface for connecting to SQLite3 database.
type SQLiteDatabase struct {
sqlx.DB
}
2018-02-03 21:20:10 +08:00
// OpenSQLiteDatabase creates and open connection to new SQLite3 database.
func OpenSQLiteDatabase(databasePath string) (*SQLiteDatabase, error) {
// Open database and start transaction
var err error
db := sqlx.MustConnect("sqlite3", databasePath)
tx := db.MustBegin()
// Make sure to rollback if panic ever happened
defer func() {
if r := recover(); r != nil {
panicErr, _ := r.(error)
2018-01-29 16:00:37 +08:00
fmt.Println("Database error:", panicErr)
tx.Rollback()
db = nil
err = panicErr
}
}()
2018-01-29 16:00:37 +08:00
tx.MustExec(`CREATE TABLE IF NOT EXISTS account(
id INTEGER NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
CONSTRAINT account_PK PRIMARY KEY(id),
CONSTRAINT account_username_UNIQUE UNIQUE(username))`)
2018-01-29 16:00:37 +08:00
tx.MustExec(`CREATE TABLE IF NOT EXISTS bookmark(
id INTEGER NOT NULL,
url TEXT NOT NULL,
title TEXT NOT NULL,
image_url TEXT NOT NULL DEFAULT "",
excerpt TEXT NOT NULL DEFAULT "",
author TEXT NOT NULL DEFAULT "",
min_read_time INTEGER NOT NULL DEFAULT 0,
max_read_time INTEGER NOT NULL DEFAULT 0,
modified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT bookmark_PK PRIMARY KEY(id),
CONSTRAINT bookmark_url_UNIQUE UNIQUE(url))`)
2018-01-29 16:00:37 +08:00
tx.MustExec(`CREATE TABLE IF NOT EXISTS tag(
id INTEGER NOT NULL,
name TEXT NOT NULL,
CONSTRAINT tag_PK PRIMARY KEY(id),
CONSTRAINT tag_name_UNIQUE UNIQUE(name))`)
2018-01-29 16:00:37 +08:00
tx.MustExec(`CREATE TABLE IF NOT EXISTS bookmark_tag(
bookmark_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
CONSTRAINT bookmark_tag_PK PRIMARY KEY(bookmark_id, tag_id),
CONSTRAINT bookmark_id_FK FOREIGN KEY(bookmark_id) REFERENCES bookmark(id),
CONSTRAINT tag_id_FK FOREIGN KEY(tag_id) REFERENCES tag(id))`)
2018-01-30 17:20:18 +08:00
tx.MustExec(`CREATE VIRTUAL TABLE IF NOT EXISTS bookmark_content USING fts4(title, content, html)`)
err = tx.Commit()
checkError(err)
return &SQLiteDatabase{*db}, err
}
2018-02-20 17:48:02 +08:00
// CreateBookmark saves new bookmark to database. Returns new ID and error if any happened.
2018-05-19 23:43:15 +08:00
func (db *SQLiteDatabase) CreateBookmark(bookmark model.Bookmark) (bookmarkID int, err error) {
2018-01-30 15:57:36 +08:00
// Check URL and title
2018-01-30 17:16:25 +08:00
if bookmark.URL == "" {
return -1, fmt.Errorf("URL must not be empty")
2018-01-30 15:57:36 +08:00
}
2018-01-30 17:16:25 +08:00
if bookmark.Title == "" {
return -1, fmt.Errorf("Title must not be empty")
2018-01-30 15:57:36 +08:00
}
2018-05-18 17:18:38 +08:00
// Set default ID and modified time
if bookmark.ID == 0 {
bookmark.ID, err = db.GetNewID("bookmark")
if err != nil {
return -1, err
}
}
2018-02-25 17:04:12 +08:00
if bookmark.Modified == "" {
bookmark.Modified = time.Now().UTC().Format("2006-01-02 15:04:05")
}
// Prepare transaction
tx, err := db.Beginx()
if err != nil {
2018-01-30 17:16:25 +08:00
return -1, err
}
// Make sure to rollback if panic ever happened
defer func() {
if r := recover(); r != nil {
panicErr, _ := r.(error)
tx.Rollback()
2018-01-30 17:16:25 +08:00
bookmarkID = -1
err = panicErr
}
}()
// Save article to database
2018-05-18 17:18:38 +08:00
tx.MustExec(`INSERT INTO bookmark (
id, url, title, image_url, excerpt, author,
2018-02-25 17:04:12 +08:00
min_read_time, max_read_time, modified)
2018-05-18 17:18:38 +08:00
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
bookmark.ID,
2018-01-30 17:16:25 +08:00
bookmark.URL,
bookmark.Title,
bookmark.ImageURL,
bookmark.Excerpt,
bookmark.Author,
bookmark.MinReadTime,
2018-02-25 17:04:12 +08:00
bookmark.MaxReadTime,
bookmark.Modified)
// Save bookmark content
2018-01-29 16:00:37 +08:00
tx.MustExec(`INSERT INTO bookmark_content
2018-01-30 17:20:18 +08:00
(docid, title, content, html) VALUES (?, ?, ?, ?)`,
2018-05-18 17:18:38 +08:00
bookmark.ID, bookmark.Title, bookmark.Content, bookmark.HTML)
// Save tags
stmtGetTag, err := tx.Preparex(`SELECT id FROM tag WHERE name = ?`)
checkError(err)
stmtInsertTag, err := tx.Preparex(`INSERT INTO tag (name) VALUES (?)`)
checkError(err)
2018-01-29 16:00:37 +08:00
stmtInsertBookmarkTag, err := tx.Preparex(`INSERT OR IGNORE INTO bookmark_tag (tag_id, bookmark_id) VALUES (?, ?)`)
checkError(err)
2018-01-30 17:16:25 +08:00
for _, tag := range bookmark.Tags {
tagName := strings.ToLower(tag.Name)
tagName = strings.TrimSpace(tagName)
2018-05-19 23:43:15 +08:00
tagID := -1
2018-01-30 17:16:25 +08:00
err = stmtGetTag.Get(&tagID, tagName)
2018-01-29 16:00:37 +08:00
checkError(err)
if tagID == -1 {
2018-01-30 17:16:25 +08:00
res := stmtInsertTag.MustExec(tagName)
2018-05-19 23:43:15 +08:00
tagID64, err := res.LastInsertId()
checkError(err)
2018-05-19 23:43:15 +08:00
tagID = int(tagID64)
}
2018-05-18 17:18:38 +08:00
stmtInsertBookmarkTag.Exec(tagID, bookmark.ID)
}
// Commit transaction
err = tx.Commit()
checkError(err)
2018-05-18 17:18:38 +08:00
bookmarkID = bookmark.ID
2018-01-30 17:16:25 +08:00
return bookmarkID, err
}
2018-05-19 23:43:15 +08:00
// GetBookmarks fetch list of bookmarks based on submitted ids.
func (db *SQLiteDatabase) GetBookmarks(withContent bool, ids ...int) ([]model.Bookmark, error) {
2018-05-19 14:36:51 +08:00
// Create query
query := `SELECT
b.id, b.url, b.title, b.image_url, b.excerpt, b.author,
b.min_read_time, b.max_read_time, b.modified, bc.content <> "" has_content
FROM bookmark b
LEFT JOIN bookmark_content bc ON bc.docid = b.id`
if withContent {
query = `SELECT
b.id, b.url, b.title, b.image_url, b.excerpt, b.author,
2018-05-26 17:44:53 +08:00
b.min_read_time, b.max_read_time, b.modified, bc.content, bc.html,
bc.content <> "" has_content
2018-05-19 14:36:51 +08:00
FROM bookmark b
LEFT JOIN bookmark_content bc ON bc.docid = b.id`
}
2018-01-29 16:00:37 +08:00
// Prepare where clause
args := []interface{}{}
whereClause := " WHERE 1"
2018-05-19 23:43:15 +08:00
if len(ids) > 0 {
2018-05-19 14:36:51 +08:00
whereClause = " WHERE b.id IN ("
2018-05-19 23:43:15 +08:00
for _, id := range ids {
args = append(args, id)
2018-01-29 21:45:27 +08:00
whereClause += "?,"
}
2018-01-29 16:00:37 +08:00
whereClause = whereClause[:len(whereClause)-1]
whereClause += ")"
}
// Fetch bookmarks
2018-05-19 14:36:51 +08:00
query += whereClause
bookmarks := []model.Bookmark{}
2018-05-19 23:43:15 +08:00
err := db.Select(&bookmarks, query, args...)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
2018-05-19 14:36:51 +08:00
// Fetch tags for each bookmarks
stmtGetTags, err := db.Preparex(`SELECT t.id, t.name
FROM bookmark_tag bt LEFT JOIN tag t ON bt.tag_id = t.id
WHERE bt.bookmark_id = ? ORDER BY t.name`)
if err != nil {
return nil, err
}
defer stmtGetTags.Close()
2018-02-03 16:02:28 +08:00
for i, book := range bookmarks {
book.Tags = []model.Tag{}
err = stmtGetTags.Select(&book.Tags, book.ID)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
2018-02-03 16:02:28 +08:00
bookmarks[i] = book
}
return bookmarks, nil
}
2018-01-29 08:40:29 +08:00
2018-05-19 23:43:15 +08:00
// DeleteBookmarks removes all record with matching ids from database.
func (db *SQLiteDatabase) DeleteBookmarks(ids ...int) (err error) {
2018-01-29 08:40:29 +08:00
// Create args and where clause
args := []interface{}{}
whereClause := " WHERE 1"
2018-05-19 23:43:15 +08:00
if len(ids) > 0 {
2018-01-29 08:40:29 +08:00
whereClause = " WHERE id IN ("
2018-05-19 23:43:15 +08:00
for _, id := range ids {
args = append(args, id)
2018-01-29 21:45:27 +08:00
whereClause += "?,"
2018-01-29 08:40:29 +08:00
}
whereClause = whereClause[:len(whereClause)-1]
whereClause += ")"
}
// Begin transaction
tx, err := db.Beginx()
if err != nil {
2018-05-19 23:43:15 +08:00
return err
2018-01-29 08:40:29 +08:00
}
// Make sure to rollback if panic ever happened
defer func() {
if r := recover(); r != nil {
panicErr, _ := r.(error)
tx.Rollback()
err = panicErr
}
}()
// Delete bookmarks
whereTagClause := strings.Replace(whereClause, "id", "bookmark_id", 1)
whereContentClause := strings.Replace(whereClause, "id", "docid", 1)
2018-01-30 14:31:10 +08:00
tx.MustExec("DELETE FROM bookmark "+whereClause, args...)
tx.MustExec("DELETE FROM bookmark_tag "+whereTagClause, args...)
2018-01-29 16:00:37 +08:00
tx.MustExec("DELETE FROM bookmark_content "+whereContentClause, args...)
2018-01-29 08:40:29 +08:00
// Commit transaction
err = tx.Commit()
checkError(err)
2018-03-05 12:02:36 +08:00
return err
2018-01-29 08:40:29 +08:00
}
2018-01-29 21:45:27 +08:00
2018-02-03 21:20:10 +08:00
// SearchBookmarks search bookmarks by the keyword or tags.
func (db *SQLiteDatabase) SearchBookmarks(orderLatest bool, keyword string, tags ...string) ([]model.Bookmark, error) {
2018-05-19 14:36:51 +08:00
// Prepare query
2018-01-29 21:45:27 +08:00
args := []interface{}{}
2018-05-19 14:36:51 +08:00
query := `SELECT
b.id, b.url, b.title, b.image_url, b.excerpt, b.author,
b.min_read_time, b.max_read_time, b.modified, bc.content <> "" has_content
FROM bookmark b
LEFT JOIN bookmark_content bc ON bc.docid = b.id
WHERE 1`
2018-01-29 21:45:27 +08:00
// Create where clause for keyword
2018-05-19 14:36:51 +08:00
keyword = strings.TrimSpace(keyword)
2018-01-29 21:45:27 +08:00
if keyword != "" {
2018-05-21 15:03:08 +08:00
query += ` AND (b.url LIKE ? OR b.id IN (
SELECT docid id FROM bookmark_content
WHERE title MATCH ? OR content MATCH ?))`
2018-02-22 18:13:55 +08:00
args = append(args, "%"+keyword+"%", keyword, keyword)
2018-01-29 21:45:27 +08:00
}
// Create where clause for tags
if len(tags) > 0 {
2018-05-19 14:36:51 +08:00
whereTagClause := ` AND b.id IN (
2018-02-24 15:01:52 +08:00
SELECT bookmark_id FROM bookmark_tag
2018-01-29 21:45:27 +08:00
WHERE tag_id IN (SELECT id FROM tag WHERE name IN (`
for _, tag := range tags {
args = append(args, tag)
whereTagClause += "?,"
}
whereTagClause = whereTagClause[:len(whereTagClause)-1]
2018-02-24 15:01:52 +08:00
whereTagClause += `)) GROUP BY bookmark_id HAVING COUNT(bookmark_id) >= ?)`
args = append(args, len(tags))
2018-01-29 21:45:27 +08:00
2018-05-19 14:36:51 +08:00
query += whereTagClause
2018-01-29 21:45:27 +08:00
}
2018-05-19 14:36:51 +08:00
// Set order clause
if orderLatest {
query += ` ORDER BY id DESC`
}
2018-05-19 14:36:51 +08:00
// Fetch bookmarks
2018-01-29 21:45:27 +08:00
bookmarks := []model.Bookmark{}
err := db.Select(&bookmarks, query, args...)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
// Fetch tags for each bookmarks
stmtGetTags, err := db.Preparex(`SELECT t.id, t.name
FROM bookmark_tag bt LEFT JOIN tag t ON bt.tag_id = t.id
WHERE bt.bookmark_id = ? ORDER BY t.name`)
if err != nil {
return nil, err
}
defer stmtGetTags.Close()
for i := range bookmarks {
tags := []model.Tag{}
err = stmtGetTags.Select(&tags, bookmarks[i].ID)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
bookmarks[i].Tags = tags
}
return bookmarks, nil
}
2018-01-31 11:59:27 +08:00
2018-02-03 21:20:10 +08:00
// UpdateBookmarks updates the saved bookmark in database.
2018-04-28 22:02:36 +08:00
func (db *SQLiteDatabase) UpdateBookmarks(bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {
2018-02-03 16:02:28 +08:00
// Prepare transaction
tx, err := db.Beginx()
if err != nil {
2018-02-26 18:00:14 +08:00
return []model.Bookmark{}, err
2018-02-03 16:02:28 +08:00
}
2018-01-31 11:59:27 +08:00
2018-02-03 16:02:28 +08:00
// Make sure to rollback if panic ever happened
defer func() {
if r := recover(); r != nil {
panicErr, _ := r.(error)
tx.Rollback()
2018-02-26 18:00:14 +08:00
result = []model.Bookmark{}
2018-02-03 16:02:28 +08:00
err = panicErr
}
}()
2018-01-31 11:59:27 +08:00
2018-02-03 16:02:28 +08:00
// Prepare statement
2018-03-12 16:17:20 +08:00
stmtUpdateBookmark, err := tx.Preparex(`UPDATE bookmark SET
2018-02-03 16:02:28 +08:00
url = ?, title = ?, image_url = ?, excerpt = ?, author = ?,
min_read_time = ?, max_read_time = ?, modified = ? WHERE id = ?`)
2018-02-03 16:02:28 +08:00
checkError(err)
2018-01-31 11:59:27 +08:00
2018-03-12 16:17:20 +08:00
stmtUpdateBookmarkContent, err := tx.Preparex(`UPDATE bookmark_content SET
2018-02-03 16:02:28 +08:00
title = ?, content = ?, html = ? WHERE docid = ?`)
checkError(err)
stmtGetTag, err := tx.Preparex(`SELECT id FROM tag WHERE name = ?`)
checkError(err)
stmtInsertTag, err := tx.Preparex(`INSERT INTO tag (name) VALUES (?)`)
checkError(err)
stmtInsertBookmarkTag, err := tx.Preparex(`INSERT OR IGNORE INTO bookmark_tag (tag_id, bookmark_id) VALUES (?, ?)`)
checkError(err)
stmtDeleteBookmarkTag, err := tx.Preparex(`DELETE FROM bookmark_tag WHERE bookmark_id = ? AND tag_id = ?`)
checkError(err)
2018-02-26 18:00:14 +08:00
result = []model.Bookmark{}
2018-02-03 16:02:28 +08:00
for _, book := range bookmarks {
2018-05-19 16:28:17 +08:00
// Save bookmark
2018-02-03 16:02:28 +08:00
stmtUpdateBookmark.MustExec(
book.URL,
book.Title,
book.ImageURL,
book.Excerpt,
book.Author,
book.MinReadTime,
book.MaxReadTime,
book.Modified,
2018-02-03 16:02:28 +08:00
book.ID)
2018-05-19 16:28:17 +08:00
// Save bookmark content
2018-02-03 16:02:28 +08:00
stmtUpdateBookmarkContent.MustExec(
book.Title,
book.Content,
book.HTML,
book.ID)
2018-05-19 16:28:17 +08:00
// Save bookmark tags
2018-02-26 18:00:14 +08:00
newTags := []model.Tag{}
2018-02-03 16:02:28 +08:00
for _, tag := range book.Tags {
if tag.Deleted {
stmtDeleteBookmarkTag.MustExec(book.ID, tag.ID)
continue
2018-01-31 11:59:27 +08:00
}
2018-02-03 16:02:28 +08:00
if tag.ID == 0 {
2018-05-19 23:43:15 +08:00
tagID := -1
2018-02-03 16:02:28 +08:00
err = stmtGetTag.Get(&tagID, tag.Name)
checkError(err)
2018-01-31 11:59:27 +08:00
2018-02-03 16:02:28 +08:00
if tagID == -1 {
res := stmtInsertTag.MustExec(tag.Name)
2018-05-19 23:43:15 +08:00
tagID64, err := res.LastInsertId()
2018-02-03 16:02:28 +08:00
checkError(err)
2018-05-19 23:43:15 +08:00
tagID = int(tagID64)
2018-02-03 16:02:28 +08:00
}
2018-01-31 11:59:27 +08:00
2018-02-03 16:02:28 +08:00
stmtInsertBookmarkTag.Exec(tagID, book.ID)
}
2018-02-26 18:00:14 +08:00
newTags = append(newTags, tag)
2018-01-31 11:59:27 +08:00
}
2018-02-26 18:00:14 +08:00
book.Tags = newTags
result = append(result, book)
2018-01-31 11:59:27 +08:00
}
2018-02-03 16:02:28 +08:00
// Commit transaction
err = tx.Commit()
checkError(err)
2018-01-31 11:59:27 +08:00
2018-02-26 18:00:14 +08:00
return result, err
2018-01-31 11:59:27 +08:00
}
2018-02-20 17:48:02 +08:00
// CreateAccount saves new account to database. Returns new ID and error if any happened.
func (db *SQLiteDatabase) CreateAccount(username, password string) (err error) {
// Hash password with bcrypt
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
return err
}
// Insert account to database
_, err = db.Exec(`INSERT INTO account
(username, password) VALUES (?, ?)`,
username, hashedPassword)
2018-03-12 19:01:14 +08:00
return err
2018-02-20 17:48:02 +08:00
}
2018-04-28 22:02:36 +08:00
// GetAccount fetch account with matching username
func (db *SQLiteDatabase) GetAccount(username string) (model.Account, error) {
account := model.Account{}
err := db.Get(&account,
`SELECT id, username, password FROM account WHERE username = ?`,
username)
return account, err
}
// GetAccounts fetch list of accounts with matching keyword
func (db *SQLiteDatabase) GetAccounts(keyword string) ([]model.Account, error) {
// Create query
2018-02-20 17:48:02 +08:00
args := []interface{}{}
2018-04-28 22:02:36 +08:00
query := `SELECT id, username, password FROM account`
if keyword == "" {
query += " WHERE 1"
} else {
query += " WHERE username LIKE ?"
args = append(args, "%"+keyword+"%")
2018-02-20 17:48:02 +08:00
}
2018-04-28 22:02:36 +08:00
2018-02-20 17:48:02 +08:00
query += ` ORDER BY username`
2018-04-28 22:02:36 +08:00
// Fetch list account
2018-02-20 17:48:02 +08:00
accounts := []model.Account{}
err := db.Select(&accounts, query, args...)
return accounts, err
}
// DeleteAccounts removes all record with matching usernames
func (db *SQLiteDatabase) DeleteAccounts(usernames ...string) error {
// Prepare where clause
args := []interface{}{}
whereClause := " WHERE 1"
if len(usernames) > 0 {
whereClause = " WHERE username IN ("
for _, username := range usernames {
args = append(args, username)
whereClause += "?,"
}
whereClause = whereClause[:len(whereClause)-1]
whereClause += ")"
}
// Delete usernames
2018-02-22 17:48:36 +08:00
_, err := db.Exec(`DELETE FROM account `+whereClause, args...)
2018-02-20 17:48:02 +08:00
return err
}
2018-03-06 16:06:20 +08:00
2018-03-10 11:39:38 +08:00
// GetTags fetch list of tags and their frequency
2018-03-06 16:06:20 +08:00
func (db *SQLiteDatabase) GetTags() ([]model.Tag, error) {
tags := []model.Tag{}
2018-03-10 11:39:38 +08:00
query := `SELECT bt.tag_id id, t.name, COUNT(bt.tag_id) n_bookmarks
FROM bookmark_tag bt
LEFT JOIN tag t ON bt.tag_id = t.id
GROUP BY bt.tag_id ORDER BY t.name`
2018-03-06 16:06:20 +08:00
err := db.Select(&tags, query)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
2018-03-10 11:39:38 +08:00
2018-03-06 16:06:20 +08:00
return tags, nil
}
2018-03-12 19:01:14 +08:00
2018-05-18 17:18:38 +08:00
// GetNewID creates new ID for specified table
2018-05-19 23:43:15 +08:00
func (db *SQLiteDatabase) GetNewID(table string) (int, error) {
var tableID int
2018-05-18 17:18:38 +08:00
query := fmt.Sprintf(`SELECT IFNULL(MAX(id) + 1, 1) FROM %s`, table)
err := db.Get(&tableID, query)
if err != nil && err != sql.ErrNoRows {
return -1, err
}
return tableID, nil
}