gosuki/database/database.go

408 lines
7.5 KiB
Go
Raw Normal View History

2022-09-20 10:11:16 +00:00
// TODO: handle `modified` time
// sqlite database management
2018-11-09 17:25:50 +00:00
package database
import (
"database/sql"
"errors"
2018-11-09 17:25:50 +00:00
"fmt"
2018-12-04 03:34:30 +00:00
"net/url"
2018-11-09 17:25:50 +00:00
"strings"
2023-02-18 23:13:23 +00:00
"git.blob42.xyz/gomark/gosuki/logging"
"git.blob42.xyz/gomark/gosuki/tree"
2022-09-20 10:11:16 +00:00
2018-11-23 03:40:10 +00:00
"github.com/jmoiron/sqlx"
2019-02-22 18:52:13 +00:00
sqlite3 "github.com/mattn/go-sqlite3"
2018-11-09 17:25:50 +00:00
"github.com/sp4ke/hashmap"
)
var (
_sql3conns []*sqlite3.SQLiteConn // Only used for backup hook
backupHookRegistered bool // set to true once the backup hook is registered
2019-03-01 18:41:44 +00:00
DefaultDBPath = "./"
2018-11-09 17:25:50 +00:00
)
type Index = *hashmap.RBTree
type Node = tree.Node
var log = logging.GetLogger("DB")
const (
2023-02-18 23:13:23 +00:00
DBFileName = "gosuki.db"
2018-12-04 03:34:30 +00:00
DBTypeFileDSN = "file:%s"
DriverBackupMode = "sqlite_hook_backup"
DriverDefault = "sqlite3"
2023-02-18 23:13:23 +00:00
GosukiMainTable = "bookmarks"
2018-11-09 17:25:50 +00:00
)
2018-12-04 03:34:30 +00:00
type DBType int
const (
DBTypeInMemory DBType = iota
DBTypeRegularFile
)
2023-02-18 23:13:23 +00:00
// Differentiate between gosukidb.sqlite and other sqlite DBs
2018-12-04 03:34:30 +00:00
const (
2023-02-18 23:13:23 +00:00
DBGosuki DBType = iota
2018-12-04 03:34:30 +00:00
DBForeign
)
2022-10-07 20:17:00 +00:00
// Database schemas used for the creation of new databases
2018-11-09 17:25:50 +00:00
const (
// metadata: name or title of resource
2018-11-13 17:21:02 +00:00
// modified: time.Now().Unix()
//
// flags: designed to be extended in future using bitwise masks
// Masks:
// 0b00000001: set title immutable ((do not change title when updating the bookmarks from the web ))
2023-02-18 23:13:23 +00:00
QCreateGosukiDBSchema = `
CREATE TABLE if not exists bookmarks (
2018-11-09 17:25:50 +00:00
id integer PRIMARY KEY,
URL text NOT NULL UNIQUE,
metadata text default '',
tags text default '',
desc text default '',
modified integer default (strftime('%s')),
flags integer default 0
)
`
2018-11-09 17:25:50 +00:00
)
2018-12-04 03:34:30 +00:00
type DsnOptions map[string]string
type DBError struct {
// Database object where error occured
DBName string
// Error that occured
Err error
}
func DBErr(dbName string, err error) DBError {
return DBError{Err: err}
}
2018-12-03 18:55:28 +00:00
func (e DBError) Error() string {
return fmt.Sprintf("<%s>: %s", e.DBName, e.Err)
}
var (
ErrVfsLocked = errors.New("vfs locked")
)
type Opener interface {
Open(driver string, dsn string) error
}
type SQLXOpener interface {
Opener
Get() *sqlx.DB
}
type SQLXDBOpener struct {
handle *sqlx.DB
}
func (o *SQLXDBOpener) Open(driver string, dataSourceName string) error {
var err error
o.handle, err = sqlx.Open(driver, dataSourceName)
if err != nil {
return err
}
return nil
}
func (o *SQLXDBOpener) Get() *sqlx.DB {
return o.handle
}
2018-11-09 17:25:50 +00:00
// DB encapsulates an sql.DB struct. All interactions with memory/buffer and
// disk databases are done through the DB object
type DB struct {
Name string
Path string
2018-11-23 03:40:10 +00:00
Handle *sqlx.DB
2018-11-09 17:25:50 +00:00
EngineMode string
2018-11-13 17:21:02 +00:00
AttachedTo []string
Type DBType
2018-12-04 03:34:30 +00:00
filePath string
SQLXOpener
2018-12-04 03:34:30 +00:00
LockChecker
2018-11-09 17:25:50 +00:00
}
func (db *DB) open() error {
var err error
err = db.SQLXOpener.Open(db.EngineMode, db.Path)
if err != nil {
return err
}
db.Handle = db.SQLXOpener.Get()
2018-12-04 03:34:30 +00:00
err = db.Handle.Ping()
if err != nil {
return err
}
2018-12-04 03:34:30 +00:00
log.Debugf("<%s> opened at <%s> with driver <%s>",
db.Name,
db.Path,
db.EngineMode)
return nil
}
2018-12-04 03:34:30 +00:00
func (db *DB) Locked() (bool, error) {
return db.LockChecker.Locked()
}
// dbPath is empty string ("") when using in memory sqlite db
// Call to Init() required before using
2022-11-07 19:07:13 +00:00
func NewDB(name string, dbPath string, dbFormat string, opts ...DsnOptions) *DB {
2018-12-04 03:34:30 +00:00
var path string
var dbType DBType
// Use name as path for in memory database
if dbPath == "" {
path = fmt.Sprintf(dbFormat, name)
dbType = DBTypeInMemory
} else {
path = fmt.Sprintf(dbFormat, dbPath)
dbType = DBTypeRegularFile
2018-11-09 17:25:50 +00:00
}
2018-12-04 03:34:30 +00:00
// Handle DSN options
if len(opts) > 0 {
dsn := url.Values{}
for _, o := range opts {
for k, v := range o {
dsn.Set(k, v)
}
}
2018-11-09 17:25:50 +00:00
2018-12-04 03:34:30 +00:00
// Test if path has already query params
pos := strings.IndexRune(path, '?')
// Path already has query params
if pos >= 1 {
path = fmt.Sprintf("%s&%s", path, dsn.Encode()) //append
} else {
path = fmt.Sprintf("%s?%s", path, dsn.Encode())
}
2018-11-20 17:33:37 +00:00
2018-12-04 03:34:30 +00:00
}
2018-11-20 17:33:37 +00:00
return &DB{
Name: name,
2018-12-04 03:34:30 +00:00
Path: path,
Handle: nil,
EngineMode: DriverDefault,
SQLXOpener: &SQLXDBOpener{},
2018-12-04 03:34:30 +00:00
Type: dbType,
filePath: dbPath,
LockChecker: &VFSLockChecker{
path: dbPath,
},
2018-11-09 17:25:50 +00:00
}
2018-12-04 03:34:30 +00:00
}
2022-10-07 20:17:00 +00:00
// TODO: Should check if DB is locked
// We should export Open() in its own method and wrap
// with interface so we can mock it and test the lock status in Init()
2023-02-18 23:13:23 +00:00
// Initialize a sqlite database with Gosuki Schema if not already done
func (db *DB) Init() (*DB, error) {
2018-11-09 17:25:50 +00:00
var err error
if db.Handle != nil {
log.Warningf("%s: already initialized", db)
return db, nil
2018-11-09 17:25:50 +00:00
}
2018-12-04 03:34:30 +00:00
// Detect if database file is locked
if db.Type == DBTypeRegularFile {
locked, err := db.Locked()
if err != nil {
return nil, DBError{DBName: db.Name, Err: err}
2018-12-04 03:34:30 +00:00
}
if locked {
2018-12-04 17:06:30 +00:00
return nil, ErrVfsLocked
2018-12-04 03:34:30 +00:00
}
}
2018-12-03 18:55:28 +00:00
// Open database
err = db.open()
2018-12-03 18:55:28 +00:00
sqlErr, _ := err.(sqlite3.Error)
2018-12-04 03:34:30 +00:00
// Secondary lock check provided by sqlx Ping() method
2018-12-03 18:55:28 +00:00
if err != nil && sqlErr.Code == sqlite3.ErrBusy {
2018-12-04 17:06:30 +00:00
return nil, ErrVfsLocked
2018-12-04 03:34:30 +00:00
2018-12-03 18:55:28 +00:00
}
// Return all other errors
2018-11-09 17:25:50 +00:00
if err != nil {
return nil, DBError{DBName: db.Name, Err: err}
2018-11-09 17:25:50 +00:00
}
2018-12-04 03:34:30 +00:00
return db, nil
}
func (db *DB) InitSchema() error {
2018-11-09 17:25:50 +00:00
// Populate db schema
tx, err := db.Handle.Begin()
if err != nil {
return DBError{DBName: db.Name, Err: err}
2018-11-09 17:25:50 +00:00
}
2023-02-18 23:13:23 +00:00
stmt, err := tx.Prepare(QCreateGosukiDBSchema)
2018-11-09 17:25:50 +00:00
if err != nil {
return DBError{DBName: db.Name, Err: err}
2018-11-09 17:25:50 +00:00
}
if _, err = stmt.Exec(); err != nil {
return DBError{DBName: db.Name, Err: err}
2018-11-09 17:25:50 +00:00
}
if err = tx.Commit(); err != nil {
return DBError{DBName: db.Name, Err: err}
2018-11-09 17:25:50 +00:00
}
log.Debugf("<%s> initialized", db.Name)
2018-12-04 03:34:30 +00:00
return nil
2018-11-09 17:25:50 +00:00
}
func (db *DB) AttachTo(attached *DB) {
2018-11-09 17:25:50 +00:00
stmtStr := fmt.Sprintf("ATTACH DATABASE '%s' AS '%s'",
attached.Path,
attached.Name)
_, err := db.Handle.Exec(stmtStr)
2018-11-13 17:21:02 +00:00
2018-11-09 17:25:50 +00:00
if err != nil {
log.Error(err)
}
2018-11-13 17:21:02 +00:00
db.AttachedTo = append(db.AttachedTo, attached.Name)
2018-11-09 17:25:50 +00:00
}
func (db *DB) Close() error {
log.Debugf("Closing DB <%s>", db.Name)
2018-12-04 03:34:30 +00:00
if db.Handle == nil {
2023-09-06 12:26:17 +00:00
log.Debugf("<%s> db handle is nil, already closed ?", db.Name)
2018-12-04 03:34:30 +00:00
return nil
}
2018-11-09 17:25:50 +00:00
err := db.Handle.Close()
if err != nil {
return err
}
return nil
}
func (db *DB) IsEmpty() (bool, error) {
var count int
row := db.Handle.QueryRow("select count(*) from bookmarks")
err := row.Scan(&count)
if err != nil {
return false, err
}
if count > 0 {
return false, nil
}
return true, nil
}
func (db *DB) CountRows(table string) int {
var count int
row := db.Handle.QueryRow("select count(*) from ?", table)
err := row.Scan(&count)
if err != nil {
log.Error(err)
}
return count
}
2018-11-09 17:25:50 +00:00
// Struct represetning the schema of `bookmarks` db.
// The order in the struct respects the columns order
type SBookmark struct {
id int
Url string
metadata string
tags string
desc string
modified int64
flags int
}
// Scans a row into `SBookmark` schema
func ScanBookmarkRow(row *sql.Rows) (*SBookmark, error) {
scan := new(SBookmark)
err := row.Scan(
&scan.id,
&scan.Url,
&scan.metadata,
&scan.tags,
&scan.desc,
&scan.modified,
&scan.flags,
)
if err != nil {
return nil, err
}
return scan, nil
}
//TODO: doc
2018-11-23 03:40:10 +00:00
func flushSqliteCon(con *sqlx.DB) {
2018-11-09 17:25:50 +00:00
con.Close()
_sql3conns = _sql3conns[:len(_sql3conns)-1]
2018-11-13 16:11:16 +00:00
log.Debugf("Flushed sqlite conns -> %v", _sql3conns)
2018-11-09 17:25:50 +00:00
}
func registerSqliteHooks() {
// sqlite backup hook
log.Debugf("backup_hook: registering driver %s", DriverBackupMode)
2018-11-09 17:25:50 +00:00
// Register the hook
sql.Register(DriverBackupMode,
2018-11-09 17:25:50 +00:00
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
//log.Debugf("[ConnectHook] registering new connection")
_sql3conns = append(_sql3conns, conn)
//log.Debugf("%v", _sql3conns)
return nil
},
})
}
func init() {
2019-03-01 18:41:44 +00:00
initCache()
2018-11-09 17:25:50 +00:00
registerSqliteHooks()
}