split into packages

This commit is contained in:
ston1th 2018-02-09 22:38:47 +01:00
commit c5d41b0965
26 changed files with 328 additions and 449 deletions

File diff suppressed because one or more lines are too long

328
index.go
View file

@ -1,328 +0,0 @@
// Copyright (C) 2017 Marius Schellenberger
package main
import (
"fmt"
"html/template"
"os"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/document"
)
const (
maxResult = 1e6
maxSearchResult = 101
noResults = "<b>No search results</b>"
indexName = "Index"
welcome = `# Welcome to GoWiki
This is the [Index](/wiki/Index) page.
You can customize it how you like.`
)
type Article struct {
Title string `json:"title"`
LinkTitle string `json:"-"`
Search string `json:"search"`
Index template.HTML `json:"index"`
Text template.HTML `json:"text"`
MD string `json:"md"`
Created string `json:"created"`
Updated string `json:"updated"`
Public bool `json:"public"`
}
func notExists(title string) error {
return indexErr("article '" + title + "' does not esist")
}
func indexErr(i interface{}) error {
return fmt.Errorf("index: %s", i)
}
type Index struct {
index bleve.Index
cache *IndexCache
}
func NewIndex(path string) (i *Index, err error) {
var bi bleve.Index
if _, err = os.Stat(path); os.IsNotExist(err) {
mapping := bleve.NewIndexMapping()
bi, err = bleve.New(path, mapping)
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
_, err = i.Index(indexName, welcome, defUser, true)
if err != nil {
err = indexErr(err)
return
}
} else {
bi, err = bleve.Open(path)
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
}
i.cache = NewIndexCache()
return
}
func NewDumpIndex(path string) (i *Index, err error) {
bi, err := bleve.OpenUsing(path, map[string]interface{}{"read_only": true})
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
return
}
func (i *Index) Close() error {
return i.index.Close()
}
func (i *Index) Restore(dumpArt *DumpArticle) error {
art := render(dumpArt.MD).Article()
art.Title = dumpArt.Title
art.Created = dumpArt.Created
art.Updated = dumpArt.Updated
art.Public = dumpArt.Public
return i.index.Index(dumpArt.LinkTitle, art)
}
func (i *Index) Index(title, text, user string, public bool) (string, error) {
if user == "" {
return "", indexErr("user cannot be empty")
}
newTitle, title := makeLinkTitle(title)
if i.exists(false, newTitle) {
return "", indexErr("article '" + title + "' already exists")
}
art := render(text).Article()
art.Title = title
art.Created = user + " " + now()
art.Public = public
err := i.index.Index(newTitle, art)
i.cache.Add(newTitle, art)
return newTitle, err
}
func (i *Index) exists(public bool, title string) bool {
_, err := i.Get(public, title)
return err == nil
}
func (i *Index) Delete(title string) error {
if title == indexName {
return indexErr("page '" + indexName + "' cannot be deleted")
}
if !i.exists(false, title) {
return notExists(title)
}
err := i.index.Delete(title)
if err != nil {
return err
}
i.cache.Delete(title)
return err
}
func (i *Index) Update(title, text, user string, public bool) error {
if user == "" {
return indexErr("user cannot be empty")
}
if title == indexName && !public {
return indexErr("page '" + indexName + "' must stay public")
}
if !i.exists(false, title) {
return notExists(title)
}
art, err := i.Get(false, title)
if err != nil {
return err
}
rend := render(text).Article()
art.Updated = user + " " + now()
art.Search = rend.Search
art.Index = rend.Index
art.Text = rend.Text
art.MD = rend.MD
art.Public = public
err = i.index.Index(title, art)
if err != nil {
return err
}
i.cache.Add(title, &art)
return err
}
func (i *Index) Get(public bool, title string) (art Article, err error) {
a := i.cache.Get(title)
switch {
case a != nil:
art = *a
default:
var doc *document.Document
doc, err = i.index.Document(title)
if err != nil {
return
}
if doc == nil || doc.Fields == nil {
err = notExists(title)
return
}
for _, field := range doc.Fields {
switch f := field.(type) {
case *document.TextField:
switch field.Name() {
case "title":
art.Title = string(f.Value())
case "index":
art.Index = template.HTML(string(f.Value()))
case "text":
art.Text = template.HTML(string(f.Value()))
case "md":
art.MD = string(f.Value())
case "created":
art.Created = string(f.Value())
case "updated":
art.Updated = string(f.Value())
}
case *document.BooleanField:
if field.Name() == "public" {
p, e := f.Boolean()
if e != nil {
err = e
return
}
art.Public = p
}
}
}
}
art.LinkTitle = title
if public && !art.Public {
err = notExists(title)
}
return
}
func (i *Index) Search(public bool, search string) (html string, err error) {
if search == "" {
html = noResults
return
}
var (
res *bleve.SearchResult
req *bleve.SearchRequest
)
if public {
req = bleve.NewSearchRequest(bleve.NewConjunctionQuery([]bleve.Query{
bleve.NewBoolFieldQuery(true).SetField("public"),
bleve.NewQueryStringQuery("title:" + search + " search:" + search),
}))
} else {
req = bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
}
req.Highlight = bleve.NewHighlightWithStyle("html")
req.Size = maxSearchResult
res, err = i.index.Search(req)
if err != nil {
return
}
if res.Total == 0 {
html = noResults
return
} else if res.Total == maxSearchResult {
err = indexErr("too many search results")
return
}
for _, hit := range res.Hits {
if hit == nil {
continue
}
art, err := i.Get(false, hit.ID)
if err != nil {
continue
}
html += `<a href="/wiki/` + hit.ID + `"><b>` + art.Title + "</b></a>"
for fragField, frags := range hit.Fragments {
if fragField == "title" || fragField == "search" {
html += `<pre style="white-space: pre-wrap">`
for _, f := range frags {
html += f
}
html += "</pre>"
break
}
}
html += "<br>"
}
return
}
type Title struct {
Title string
LinkTitle string
}
func (i *Index) GetAll(public bool) (t []Title, err error) {
var (
res *bleve.SearchResult
req *bleve.SearchRequest
)
if public {
req = bleve.NewSearchRequest(bleve.NewBoolFieldQuery(true).SetField("public"))
} else {
req = bleve.NewSearchRequest(bleve.NewMatchAllQuery())
}
req.Size = maxResult
res, err = i.index.Search(req)
if err != nil {
return
}
for _, v := range res.Hits {
if v == nil {
continue
}
if v.ID == indexName {
continue
}
doc, err := i.index.Document(v.ID)
if err != nil || doc == nil || doc.Fields == nil {
continue
}
for _, field := range doc.Fields {
if field.Name() == "title" {
f, ok := field.(*document.TextField)
if ok {
t = append(t, Title{string(f.Value()), v.ID})
break
}
}
}
}
return
}
func (i *Index) Dump() (titles []string, err error) {
req := bleve.NewSearchRequest(bleve.NewMatchAllQuery())
req.Size = maxResult
res, err := i.index.Search(req)
if err != nil {
return
}
for _, v := range res.Hits {
if v == nil {
continue
}
titles = append(titles, v.ID)
}
return
}

View file

@ -4,14 +4,10 @@
package main
import "github.com/urfave/cli"
import "git.giftfish.de/ston1th/gowiki/pkg/cmd"
var version string
func main() {
app := cli.NewApp()
app.Name = "gowiki"
app.Usage = "a simple wiki engine"
app.Version = version
run(app)
cmd.Run(version)
}

View file

@ -12,6 +12,8 @@ import (
"os/signal"
"syscall"
"time"
"git.giftfish.de/ston1th/gowiki/pkg/log"
)
const (
@ -41,7 +43,7 @@ func server(conf serverConfig) (err error) {
return
}
newLogger(conf.LogFile)
log.NewLogger(conf.LogFile)
l, err := godrop.GetListener()
if err != nil {
@ -60,6 +62,7 @@ func server(conf serverConfig) (err error) {
log.Println("caught " + sig.String())
go func() {
time.Sleep(time.Second * 20)
//TODO error needed?
log.Fatal(errors.New("stop timed out: killing"))
}()
err = srv.Stop()
@ -75,7 +78,11 @@ var (
conf serverConfig
)
func run(app *cli.App) {
func Run(version string) {
app := cli.NewApp()
app.Name = "gowiki"
app.Usage = "a simple wiki engine"
app.Version = version
app.Commands = []cli.Command{
{
Name: "server",

71
pkg/db/db.go Normal file
View file

@ -0,0 +1,71 @@
// Copyright (C) 2017 Marius Schellenberger
package db
import (
"bytes"
"errors"
"github.com/boltdb/bolt"
)
const (
dbFile = "gowiki.db"
defUser = "admin"
defPassword = "gowiki"
userTable = "user"
pageTable = "page"
bcryptCost = 13
indexName = "Index"
welcome = `# Welcome to GoWiki
This is the [Index](/wiki/Index) page.
You can customize it how you like.`
)
const (
maxResult = 1e6
maxSearchResult = 101
)
var (
errUserNotExists = dbErr("user does not exist")
errUserExists = dbErr("user already exist")
)
func dbErr(i interface{}) error {
return fmt.Errorf("db: %s", i)
}
type BoltStore struct {
Marshaler
db *bolt.DB
}
func NewBoltStore() (bs *BoltStore, err error) {
db, err := bolt.Open(dbFile, 0666, nil)
if err != nil {
return
}
if err = db.Update(func(tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(userTable)); err != nil {
return err
}
if _, err := tx.CreateBucketIfNotExists([]byte(pagesTable)); err != nil {
return err
}
return nil
}); err != nil {
return
}
bs = &BoltStore{NewGOB(), db}
if !bs.UserExists(defUser) {
err = bs.CreateUser(defUser, defPassword, true)
if err != nil {
return
}
}
bs.UnlockUser(defUser)
return
}
func (bs *BoltStore) Close() error {
return bs.db.Close()
}

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017 Marius Schellenberger
package main
package db
import (
"encoding/json"

14
pkg/db/helper.go Normal file
View file

@ -0,0 +1,14 @@
package db
import "golang.org/x/crypto/sha3"
func validatePassword(pw string) (b []byte) {
b = []byte(pw)
if len(pw) <= 56 {
return
}
hash := sha3.New384()
hash.Write(b)
b = hash.Sum(nil)
return
}

25
pkg/db/marshal.go Normal file
View file

@ -0,0 +1,25 @@
package db
import "encoding/gob"
type Marshaler interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}
type gobMarshaler struct{}
func NewGOB() Marshaler {
return gobMarshaler{}
}
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
buf := new(bytes.Buffer)
err = gob.NewEncoder(buf).Encode(v)
b = buf.Bytes()
return
}
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
}

42
pkg/db/page.go Normal file
View file

@ -0,0 +1,42 @@
package db
type Article struct {
Title string `json:"title"`
LinkTitle string `json:"-"`
Search string `json:"search"`
Index template.HTML `json:"index"`
Text template.HTML `json:"text"`
MD string `json:"md"`
Created string `json:"created"`
Updated string `json:"updated"`
Public bool `json:"public"`
}
type Permission int
const (
Public Permission = iota
Internal
Private
)
type Page struct {
Title string `json:"title"`
Markdown string `json:"markdown"`
Index template.HTML `json:"index"`
Text template.HTML `json:"text"`
Created string `json:"created"`
Updated string `json:"updated"`
Perm Permission `json:"perm"`
}
func (bs *BoltStore) GetPage(title string) (p Page, err error) {
err = bs.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket([]byte(pageTable)).Get([]byte(username))
if v == nil {
return errUserNotExists
}
return bs.Unmarshal(v, &u)
})
return
}

View file

@ -1,4 +1,4 @@
package main
package db
import (
"bytes"

View file

@ -1,88 +1,10 @@
// Copyright (C) 2017 Marius Schellenberger
package main
package db
import (
"bytes"
"encoding/gob"
"errors"
"github.com/boltdb/bolt"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/sha3"
)
type User struct {
Username string `json:"username"`
Password string `json:"password"`
Admin bool `json"admin"`
Locked int `json:"locked"`
}
func validatePassword(pw string) (b []byte) {
b = []byte(pw)
if len(pw) <= 56 {
return
}
hash := sha3.New384()
hash.Write(b)
b = hash.Sum(nil)
return
}
const (
dbFile = "gowiki.db"
defUser = "admin"
defPassword = "gowiki"
userTable = "user"
snippetTable = "snippet"
bcryptCost = 13
)
var (
errUserNotExists = dbErr("user does not exist")
errUserExists = dbErr("user already exist")
)
func dbErr(i interface{}) error {
return fmt.Errorf("db: %s", i)
}
type BoltStore struct {
Marshaler
db *bolt.DB
}
func NewBoltStore() (bs *BoltStore, err error) {
db, err := bolt.Open(dbFile, 0666, nil)
if err != nil {
return
}
if err = db.Update(func(tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(userTable)); err != nil {
return err
}
if _, err := tx.CreateBucketIfNotExists([]byte(snippetTable)); err != nil {
return err
}
return nil
}); err != nil {
return
}
bs = &BoltStore{NewGOB(), db}
if !bs.UserExists(defUser) {
err = bs.CreateUser(defUser, defPassword, true)
if err != nil {
return
}
}
bs.UnlockUser(defUser)
return
}
func (bs *BoltStore) Close() error {
return bs.db.Close()
}
func (bs *BoltStore) GetUsers() (users []User, err error) {
users, err = bs.DumpUsers()
for i := range users {
@ -300,25 +222,3 @@ func (bs *BoltStore) DeleteUser(username string) error {
return tx.Bucket([]byte(userTable)).Delete([]byte(username))
})
}
type Marshaler interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}
type gobMarshaler struct{}
func NewGOB() Marshaler {
return gobMarshaler{}
}
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
buf := new(bytes.Buffer)
err = gob.NewEncoder(buf).Encode(v)
b = buf.Bytes()
return
}
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
}

112
pkg/index/index.go Normal file
View file

@ -0,0 +1,112 @@
// Copyright (C) 2017 Marius Schellenberger
package main
import (
"fmt"
"os"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/document"
)
type IndexPage struct {
Title string `json:"title"`
Search string `json:"search"`
}
func indexErr(i interface{}) error {
return fmt.Errorf("index: %s", i)
}
type Index struct {
i bleve.Index
}
func NewIndex(path string) (i *Index, err error) {
var bi bleve.Index
if _, err = os.Stat(path); os.IsNotExist(err) {
mapping := bleve.NewIndexMapping()
bi, err = bleve.New(path, mapping)
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
_, err = i.Index(indexName, welcome, defUser, true)
if err != nil {
err = indexErr(err)
return
}
} else {
bi, err = bleve.Open(path)
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
}
i.cache = NewIndexCache()
return
}
func NewDumpIndex(path string) (i *Index, err error) {
bi, err := bleve.OpenUsing(path, map[string]interface{}{"read_only": true})
if err != nil {
err = indexErr(err)
return
}
i = &Index{index: bi}
return
}
func (i *Index) Close() error {
return i.i.Close()
}
func (i *Index) Add(ip IndexPage) error {
doc, _ := i.i.Document(ip.Title)
if doc != nil {
err := i.Delete(ip.Title)
if err != nil {
return err
}
}
return i.i.Index(ip.Title, ip)
}
func (i *Index) Delete(title string) error {
return i.i.Delete(title)
}
type Result struct {
Title string
Text string
}
func (i *Index) Search(search string) (results []Result, err error) {
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
req.Highlight = bleve.NewHighlightWithStyle("html")
req.Size = maxSearchResult
res, err := i.i.Search(req)
if err != nil {
return
}
for _, hit := range res.Hits {
if hit == nil {
continue
}
title := hit.ID
text := ""
for fragField, frags := range hit.Fragments {
if fragField == "title" || fragField == "search" {
for _, f := range frags {
text += f
}
break
}
}
results = append(results, Result{title, text})
}
return
}

View file

@ -67,7 +67,9 @@ func render(text string) *rendered {
}
func spaceReplace(s string) string {
return strings.Replace(s, " ", "+", -1)
return whiteSpace.ReplaceAllString(s, "+")
//TODO remove
//return strings.Replace(s, " ", "+", -1)
}
func makeLinkTitle(t string) (newTitle string, title string) {
@ -141,3 +143,12 @@ func closeTag(j int) (ret string) {
}
return
}
func renderResult(res []Result) (html string) {
//TODO fmt.Sprintf ?
for r := range res {
html += `<a href="/` + r.LinkTitle + `"><b>` + r.Title + "</b></a>" +
`<pre style="white-space: pre-wrap">` + r.Text + "</pre><br>"
}
}

View file

@ -21,3 +21,15 @@ func newLogger(file string) {
logger.SetOutput(f)
log = logger.New(f, "", logger.LstdFlags)
}
func Println(v ...interface{}) {
log.Println(v...)
}
func Printf(fmt string, v ...interface{}) {
log.Printf(fmt, v...)
}
func Fatal(v ...interface{}) {
log.Fatal(v...)
}

View file

@ -27,13 +27,19 @@
</div>
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Public</label>
<div class="col-xs-11">
{{if .Data.Public}}
<input type="checkbox" name="public" value="0" checked>
{{else}}
<input type="checkbox" name="public" value="0">
{{end}}
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="0" class="custom-control-input" checked="">
<label class="custom-control-label" for="perm1">Public</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm2" name="perm" value="1" class="custom-control-input">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="2" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">

View file

@ -20,9 +20,19 @@
</div>
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Public</label>
<div class="col-xs-11">
<input type="checkbox" name="public" value="0">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="0" class="custom-control-input" checked="">
<label class="custom-control-label" for="perm1">Public</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm2" name="perm" value="1" class="custom-control-input">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="2" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">

2
todo.txt Normal file
View file

@ -0,0 +1,2 @@
move search results check to db
move html generation to render