346 lines
7.6 KiB
Go
346 lines
7.6 KiB
Go
package yatwiki
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
type WikiDB struct {
|
|
db *sql.DB
|
|
compressionLevel int
|
|
}
|
|
|
|
func NewWikiDB(dbFilePath string, compressionLevel int) (*WikiDB, error) {
|
|
db, err := sql.Open("sqlite3", dbFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wdb := WikiDB{
|
|
db: db,
|
|
compressionLevel: compressionLevel,
|
|
}
|
|
|
|
err = wdb.assertSchema()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("assertSchema: %s", err.Error())
|
|
}
|
|
|
|
return &wdb, nil
|
|
}
|
|
|
|
func (this *WikiDB) multiTx(stmts ...string) (err error) {
|
|
tx, err := this.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, stmt := range stmts {
|
|
_, err := tx.Exec(stmt)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (this *WikiDB) assertSchema() error {
|
|
_, err := this.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS schema (
|
|
id INTEGER PRIMARY KEY
|
|
);`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Look up current value from schema table
|
|
schemaLookup := this.db.QueryRow(`SELECT MAX(id) mid FROM schema`)
|
|
currentSchema := int64(0)
|
|
if err = schemaLookup.Scan(¤tSchema); err == nil {
|
|
// That's fine
|
|
}
|
|
|
|
log.Printf("Found DB version %d\n", currentSchema)
|
|
|
|
//
|
|
|
|
if currentSchema == 0 {
|
|
// Schema 0 ==> Schema 1
|
|
log.Println("Upgrading to DB version 1")
|
|
|
|
err := this.multiTx(
|
|
`CREATE TABLE IF NOT EXISTS articles (
|
|
id INTEGER PRIMARY KEY,
|
|
article INTEGER,
|
|
modified INTEGER,
|
|
body BLOB,
|
|
author TEXT
|
|
);`,
|
|
`CREATE TABLE IF NOT EXISTS titles (
|
|
id INTEGER PRIMARY KEY,
|
|
title TEXT
|
|
);`,
|
|
`CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)`,
|
|
`CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article)`,
|
|
`INSERT INTO schema (id) VALUES (1);`,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema = 1
|
|
}
|
|
|
|
//
|
|
|
|
if currentSchema == 1 {
|
|
log.Println("Upgrading to DB version 2")
|
|
|
|
err := this.multiTx(
|
|
`ALTER TABLE titles ADD COLUMN is_deleted INTEGER DEFAULT 0;`,
|
|
`INSERT INTO schema (id) VALUES (2);`,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currentSchema = 1
|
|
}
|
|
|
|
//
|
|
|
|
return nil
|
|
}
|
|
|
|
type TitleInfo struct {
|
|
TitleID int64
|
|
Title string
|
|
IsDeleted bool
|
|
}
|
|
|
|
type Article struct {
|
|
TitleInfo
|
|
ArticleID int64
|
|
Modified int64
|
|
Body []byte
|
|
Author string
|
|
}
|
|
|
|
func (this *WikiDB) GetArticleById(articleId int) (*Article, error) {
|
|
row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE id = ?`, articleId)
|
|
return this.parseArticle(row)
|
|
}
|
|
|
|
func (this *WikiDB) GetRevision(revId int) (*Article, error) {
|
|
row := this.db.QueryRow(`SELECT articles.*, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id WHERE articles.id = ?`, revId)
|
|
return this.parseArticleWithTitle(row)
|
|
}
|
|
|
|
func (this *WikiDB) GetLatestVersion(title string) (*Article, error) {
|
|
row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, this.normaliseTitle(title))
|
|
return this.parseArticle(row)
|
|
}
|
|
|
|
type ArticleAlteredError struct {
|
|
got, expected int64
|
|
}
|
|
|
|
func (aae ArticleAlteredError) Error() string {
|
|
return fmt.Sprintf("Warning: Your changes were not based on the most recent version of the page (r%d ≠ r%d). No changes were saved.", aae.got, aae.expected)
|
|
}
|
|
|
|
func (this *WikiDB) normaliseTitle(title string) string {
|
|
return strings.ToLower(strings.Trim(title, " \r\n\t"))
|
|
}
|
|
|
|
func (this *WikiDB) SaveArticle(title, author, body string, expectBaseRev int64) error {
|
|
isNewArticle := false
|
|
a, err := this.GetLatestVersion(title)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
isNewArticle = true
|
|
} else {
|
|
return fmt.Errorf("Couldn't check for existing article title: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
if !isNewArticle && a.ArticleID != expectBaseRev {
|
|
return ArticleAlteredError{got: expectBaseRev, expected: a.ArticleID}
|
|
}
|
|
|
|
zBody, err := gzdeflate([]byte(body), this.compressionLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var titleId int64
|
|
if isNewArticle {
|
|
titleInsert, err := this.db.Exec(`INSERT INTO titles (title) VALUES (?)`, this.normaliseTitle(title))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
titleId, err = titleInsert.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
titleId = a.TitleID
|
|
}
|
|
|
|
_, err = this.db.Exec(`INSERT INTO articles (article, modified, body, author) VALUES (?, ?, ?, ?)`, titleId, time.Now().Unix(), zBody, author)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update is-deleted flag
|
|
isDeleted := 0
|
|
if body == bbcodeIsDeleted {
|
|
isDeleted = 1
|
|
}
|
|
_, err = this.db.Exec(`UPDATE titles SET is_deleted = ? WHERE id = ?`, isDeleted, titleId)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (this *WikiDB) GetRevisionHistory(title string) ([]Article, error) {
|
|
rows, err := this.db.Query(
|
|
`SELECT articles.id, articles.modified, articles.author FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC`,
|
|
this.normaliseTitle(title),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
ret := make([]Article, 0)
|
|
for rows.Next() {
|
|
a := Article{}
|
|
err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret = append(ret, a)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (this *WikiDB) GetNextOldestRevision(revision int) (int, error) {
|
|
row := this.db.QueryRow(
|
|
`SELECT articles.id FROM articles WHERE articles.article = (SELECT article FROM articles WHERE id = ?) AND id < ? ORDER BY id DESC LIMIT 1`,
|
|
revision, revision,
|
|
)
|
|
|
|
var ret int
|
|
err := row.Scan(&ret)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (this *WikiDB) GetRecentChanges(offset int, limit int) ([]Article, error) {
|
|
rows, err := this.db.Query(
|
|
`SELECT articles.id, articles.modified, articles.author, titles.title, titles.is_deleted FROM articles JOIN titles ON articles.article=titles.id ORDER BY modified DESC ` +
|
|
fmt.Sprintf(`LIMIT %d OFFSET %d`, limit, offset),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
ret := make([]Article, 0, limit)
|
|
for rows.Next() {
|
|
a := Article{}
|
|
err := rows.Scan(&a.ArticleID, &a.Modified, &a.Author, &a.Title, &a.IsDeleted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret = append(ret, a)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (this *WikiDB) TotalRevisions() (int64, error) {
|
|
row := this.db.QueryRow(`SELECT COUNT(*) c FROM articles`)
|
|
var ret int64
|
|
err := row.Scan(&ret)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (this *WikiDB) ListTitles(includeDeleted bool) ([]TitleInfo, error) {
|
|
stmt := `SELECT id, title, is_deleted FROM titles`
|
|
if !includeDeleted {
|
|
stmt += ` WHERE is_deleted = 0`
|
|
}
|
|
stmt += ` ORDER BY title ASC`
|
|
|
|
rows, err := this.db.Query(stmt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
ret := make([]TitleInfo, 0)
|
|
for rows.Next() {
|
|
var title TitleInfo
|
|
err = rows.Scan(&title.TitleID, &title.Title, &title.IsDeleted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret = append(ret, title)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) {
|
|
a := Article{}
|
|
var gzBody []byte
|
|
err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decompressed, err := gzinflate(gzBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.Body = decompressed
|
|
|
|
return &a, nil
|
|
}
|
|
|
|
func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*Article, error) {
|
|
a := Article{}
|
|
var gzBody []byte
|
|
err := row.Scan(&a.ArticleID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title, &a.IsDeleted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decompressed, err := gzinflate(gzBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.Body = decompressed
|
|
|
|
return &a, nil
|
|
}
|
|
|
|
func (this *WikiDB) Close() {
|
|
this.db.Close()
|
|
}
|