yatwiki/DB.go

283 lines
6.1 KiB
Go

package yatwiki
import (
"database/sql"
"fmt"
"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) assertSchema() error {
_, err := this.db.Exec(`
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
article INTEGER,
modified INTEGER,
body BLOB,
author TEXT
);`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE TABLE IF NOT EXISTS titles (
id INTEGER PRIMARY KEY,
title TEXT
);`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified)
`)
if err != nil {
return err
}
_, err = this.db.Exec(`
CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article)
`)
if err != nil {
return err
}
return nil
}
type Article struct {
ID int64
TitleID int64
Modified int64
Body []byte
Author string
Title 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 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`, 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.ID != expectBaseRev {
return ArticleAlteredError{got: expectBaseRev, expected: a.ID}
}
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
}
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.ID, &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 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.ID, &a.Modified, &a.Author, &a.Title)
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() ([]string, error) {
rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
ret := make([]string, 0)
for rows.Next() {
var title string
err = rows.Scan(&title)
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.ID, &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.ID, &a.TitleID, &a.Modified, &gzBody, &a.Author, &a.Title)
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()
}