split into packages
This commit is contained in:
parent
2b56832843
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
328
index.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
8
main.go
8
main.go
|
|
@ -4,14 +4,10 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "git.giftfish.de/ston1th/gowiki/pkg/cmd"
|
||||||
|
|
||||||
var version string
|
var version string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
cmd.Run(version)
|
||||||
app.Name = "gowiki"
|
|
||||||
app.Usage = "a simple wiki engine"
|
|
||||||
app.Version = version
|
|
||||||
run(app)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -41,7 +43,7 @@ func server(conf serverConfig) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newLogger(conf.LogFile)
|
log.NewLogger(conf.LogFile)
|
||||||
|
|
||||||
l, err := godrop.GetListener()
|
l, err := godrop.GetListener()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -60,6 +62,7 @@ func server(conf serverConfig) (err error) {
|
||||||
log.Println("caught " + sig.String())
|
log.Println("caught " + sig.String())
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 20)
|
time.Sleep(time.Second * 20)
|
||||||
|
//TODO error needed?
|
||||||
log.Fatal(errors.New("stop timed out: killing"))
|
log.Fatal(errors.New("stop timed out: killing"))
|
||||||
}()
|
}()
|
||||||
err = srv.Stop()
|
err = srv.Stop()
|
||||||
|
|
@ -75,7 +78,11 @@ var (
|
||||||
conf serverConfig
|
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{
|
app.Commands = []cli.Command{
|
||||||
{
|
{
|
||||||
Name: "server",
|
Name: "server",
|
||||||
71
pkg/db/db.go
Normal file
71
pkg/db/db.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (C) 2017 Marius Schellenberger
|
// Copyright (C) 2017 Marius Schellenberger
|
||||||
|
|
||||||
package main
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
14
pkg/db/helper.go
Normal file
14
pkg/db/helper.go
Normal 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
25
pkg/db/marshal.go
Normal 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
42
pkg/db/page.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -1,88 +1,10 @@
|
||||||
// Copyright (C) 2017 Marius Schellenberger
|
package db
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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) {
|
func (bs *BoltStore) GetUsers() (users []User, err error) {
|
||||||
users, err = bs.DumpUsers()
|
users, err = bs.DumpUsers()
|
||||||
for i := range users {
|
for i := range users {
|
||||||
|
|
@ -300,25 +222,3 @@ func (bs *BoltStore) DeleteUser(username string) error {
|
||||||
return tx.Bucket([]byte(userTable)).Delete([]byte(username))
|
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
112
pkg/index/index.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,9 @@ func render(text string) *rendered {
|
||||||
}
|
}
|
||||||
|
|
||||||
func spaceReplace(s string) string {
|
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) {
|
func makeLinkTitle(t string) (newTitle string, title string) {
|
||||||
|
|
@ -141,3 +143,12 @@ func closeTag(j int) (ret string) {
|
||||||
}
|
}
|
||||||
return
|
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>"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,3 +21,15 @@ func newLogger(file string) {
|
||||||
logger.SetOutput(f)
|
logger.SetOutput(f)
|
||||||
log = logger.New(f, "", logger.LstdFlags)
|
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...)
|
||||||
|
}
|
||||||
|
|
@ -27,13 +27,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-xs-1 control-label">Public</label>
|
|
||||||
<div class="col-xs-11">
|
<div class="col-xs-11">
|
||||||
{{if .Data.Public}}
|
<div class="custom-control custom-radio">
|
||||||
<input type="checkbox" name="public" value="0" checked>
|
<input type="radio" id="perm1" name="perm" value="0" class="custom-control-input" checked="">
|
||||||
{{else}}
|
<label class="custom-control-label" for="perm1">Public</label>
|
||||||
<input type="checkbox" name="public" value="0">
|
</div>
|
||||||
{{end}}
|
<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>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-xs-1 control-label">Public</label>
|
|
||||||
<div class="col-xs-11">
|
<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>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
||||||
2
todo.txt
Normal file
2
todo.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
move search results check to db
|
||||||
|
move html generation to render
|
||||||
Loading…
Add table
Add a link
Reference in a new issue