328 lines
7 KiB
Go
328 lines
7 KiB
Go
// 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
|
|
}
|