wiki/file.go

745 lines
16 KiB
Go
Raw Normal View History

2021-08-07 17:13:10 +00:00
/*
* Wiki - A wiki with editor
* Copyright (c) 2021-2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2018-11-24 12:34:51 +00:00
package main
import (
"bufio"
"bytes"
2019-08-25 10:30:00 +00:00
"encoding/json"
2021-01-17 13:55:10 +00:00
"errors"
2019-02-19 06:34:52 +00:00
"fmt"
2018-11-24 12:34:51 +00:00
"html"
"html/template"
"io/ioutil"
"log"
2018-11-24 12:34:51 +00:00
"os"
"os/exec"
"path/filepath"
"strings"
2019-02-18 19:47:31 +00:00
"time"
2019-08-25 10:30:00 +00:00
"github.com/blevesearch/bleve"
2019-08-25 10:30:00 +00:00
"github.com/sergi/go-diff/diffmatchpatch"
2018-11-24 12:34:51 +00:00
)
const (
2020-10-21 18:49:23 +00:00
DocumentsFile = "_documents.json"
LinksFile = "_links.json"
)
2021-01-17 13:55:10 +00:00
var BlockNotFound = errors.New("block not found")
2020-05-30 22:56:10 +00:00
type saveMessage struct {
p string
page Page
summary string
author string
}
2020-10-21 18:49:23 +00:00
// ListItemV2 is way to convert from old structure to new structure
type ListItemV2 struct {
ID ID
Indented int
Text string
}
func (v2 ListItemV2) ListItem() ListItem {
return ListItem{
v2.ID.StrID,
v2.Indented,
v2.Text,
2020-10-28 19:01:25 +00:00
false,
2020-10-21 18:49:23 +00:00
}
}
// ListItem is a simplification of the information that was saved by the editor
type ListItem struct {
ID string
Indented int
Text string
2020-10-28 19:01:25 +00:00
Fleeting bool `json:"fleeting,omitempty"`
2020-10-21 18:49:23 +00:00
}
type ActualListItem struct {
ID string `json:"id"`
Indented int `json:"indented"`
Text string `json:"text"`
Fold string `json:"fold"`
Hidden bool `json:"hidden"`
2020-10-28 19:01:25 +00:00
Fleeting bool `json:"fleeting,omitempty"`
2020-10-21 18:49:23 +00:00
}
2018-11-24 12:34:51 +00:00
type FilePages struct {
dirname string
2020-05-30 22:56:10 +00:00
saveC chan saveMessage
index bleve.Index
2021-08-18 19:01:18 +00:00
blockRepo BlockRepository
2018-11-24 12:34:51 +00:00
}
2021-01-17 13:55:10 +00:00
type BlockResponse struct {
PageID string
ParentID string
Texts map[string]string
Children map[string][]string
Parents []string
}
2021-08-18 19:01:18 +00:00
func NewFilePages(dirname string, index bleve.Index) *FilePages {
blockRepo, err := NewBlockRepo(dirname)
2020-10-21 18:49:23 +00:00
if err != nil {
2021-08-18 19:01:18 +00:00
log.Fatal(err)
2020-10-21 18:49:23 +00:00
}
2021-08-18 19:01:18 +00:00
fp := &FilePages{
dirname: dirname,
saveC: make(chan saveMessage),
index: index,
blockRepo: blockRepo,
}
2020-10-21 18:49:23 +00:00
go func() {
2020-05-30 22:56:10 +00:00
for msg := range fp.saveC {
err := fp.save(msg)
if err != nil {
log.Println(err)
}
2020-05-30 22:56:10 +00:00
}
}()
2021-08-07 18:18:30 +00:00
2018-11-24 12:34:51 +00:00
return fp
}
2020-10-21 18:49:23 +00:00
func convertBlocksToListItems(current string, blocks BlockResponse, indent int) []ActualListItem {
2020-10-28 19:19:38 +00:00
listItems := []ActualListItem{}
2020-10-21 18:49:23 +00:00
for _, child := range blocks.Children[current] {
l := convertBlocksToListItems(child, blocks, indent+1)
listItems = append(listItems,
ActualListItem{
ID: child,
Indented: indent,
Text: blocks.Texts[child],
2021-08-07 18:18:30 +00:00
Fold: "open",
2020-10-21 18:49:23 +00:00
Hidden: false,
})
listItems = append(listItems, l...)
}
return listItems
}
2021-01-17 14:38:48 +00:00
type titleOption struct {
date bool
timeObj time.Time
}
// 1_januari_2021
// 2021-01-01
2021-01-17 13:55:10 +00:00
func (fp *FilePages) Get(name string) Page {
2020-10-31 22:28:47 +00:00
var sw stopwatch
2021-01-17 13:55:10 +00:00
sw.Start("Get " + name)
defer sw.Stop()
2021-01-17 14:38:48 +00:00
var names []string
2021-01-17 20:31:26 +00:00
var to titleOption
pageNameDate, err := ParseDatePageName(name)
2021-08-07 18:18:30 +00:00
2021-01-17 14:38:48 +00:00
if err == nil {
2021-01-17 20:31:26 +00:00
to.date = true
to.timeObj = pageNameDate
names = append(names, name, pageNameDate.Format("2006-01-02"))
} else if t, err := time.Parse("2006-01-02", name); err == nil {
to.date = true
to.timeObj = t
names = append(names, formatDatePageName(to.timeObj), name)
2021-01-17 16:36:17 +00:00
} else {
names = append(names, name)
2021-01-17 14:38:48 +00:00
}
2021-01-17 16:36:17 +00:00
2021-01-17 13:55:10 +00:00
for _, name := range names {
2021-08-18 19:01:18 +00:00
blocks, err := fp.blockRepo.GetBlocks(name)
2021-01-17 13:55:10 +00:00
if err != nil && errors.Is(err, BlockNotFound) {
continue
}
2021-01-17 20:31:26 +00:00
return fp.blocksBackendGet(name, blocks, to)
2021-01-17 13:55:10 +00:00
}
2021-01-17 16:36:17 +00:00
page, err := fp.oldPagesBackend(name)
if err != nil {
2021-08-15 19:34:34 +00:00
log.Println("Deprecated:", err)
2021-01-17 20:31:26 +00:00
return fp.blocksBackendGet(name, BlockResponse{ParentID: "root", PageID: name}, to)
2021-01-17 16:36:17 +00:00
}
return page
}
2021-01-17 14:38:48 +00:00
func (fp *FilePages) blocksBackendGet(name string, blocks BlockResponse, option titleOption) Page {
2020-10-21 18:49:23 +00:00
buf := bytes.Buffer{}
current := blocks.PageID
listItems := convertBlocksToListItems(current, blocks, 0)
if listItems == nil {
listItems = []ActualListItem{}
}
err := json.NewEncoder(&buf).Encode(&listItems)
if err != nil {
log.Printf("while encoding blocks: %s", err)
}
2020-10-31 22:28:47 +00:00
2021-01-17 13:16:06 +00:00
refs, err := getBackrefs(fp, name)
if err != nil {
refs = nil
}
2021-01-17 14:38:48 +00:00
title := formatTitle(blocks.Texts[name], option)
2021-01-17 16:36:17 +00:00
if title == "" {
title = cleanTitle(name)
}
2021-01-17 14:38:48 +00:00
2020-05-07 13:20:36 +00:00
return Page{
Name: name,
2021-01-17 14:38:48 +00:00
Title: title,
2020-10-21 18:49:23 +00:00
Content: buf.String(),
2020-05-07 13:20:36 +00:00
Refs: refs,
2020-10-21 18:49:23 +00:00
Blocks: blocks,
Parent: blocks.ParentID,
2020-05-07 13:20:36 +00:00
}
2018-11-24 12:34:51 +00:00
}
2021-01-17 14:38:48 +00:00
func formatTitle(title string, option titleOption) string {
if option.date {
return formatDateTitle(option.timeObj)
}
return title
}
2021-01-17 16:36:17 +00:00
func (fp *FilePages) oldPagesBackend(title string) (Page, error) {
name := strings.Replace(title, " ", "_", -1)
title = strings.Replace(title, "_", " ", -1)
2021-01-17 13:16:06 +00:00
refs, err := getBackrefs(fp, name)
if err != nil {
refs = nil
}
f, err := os.Open(filepath.Join(fp.dirname, name))
if err != nil {
return Page{
Title: title,
Name: name,
Content: "",
Refs: refs,
2021-08-15 19:34:34 +00:00
}, fmt.Errorf("in old pages backend: %w", err)
}
defer f.Close()
body, err := ioutil.ReadAll(f)
if err != nil {
return Page{
Name: name,
Title: title,
Content: "",
Refs: refs,
2021-08-15 19:34:34 +00:00
}, fmt.Errorf("while reading %q in old pages backend: %w", name, err)
}
return Page{
Name: name,
Title: title,
Content: string(body),
Refs: refs,
2021-01-17 16:36:17 +00:00
}, nil
}
2019-02-19 06:34:52 +00:00
func (fp *FilePages) Save(p string, page Page, summary, author string) error {
2020-05-30 22:56:10 +00:00
fp.saveC <- saveMessage{p, page, summary, author}
return nil
}
func (fp *FilePages) save(msg saveMessage) error {
var sw stopwatch
2020-05-30 22:56:10 +00:00
p := msg.p
page := msg.page
summary := msg.summary
author := msg.author
2021-08-12 22:41:26 +00:00
if p[0] != '_' {
page.Name = strings.Replace(p, " ", "_", -1)
page.Title = strings.Replace(p, "_", " ", -1)
} else {
page.Name = p
page.Title = p
}
sw.Start("create blocks")
2021-08-18 19:01:18 +00:00
err := fp.saveBlocksFromPage(fp.dirname, page)
if err != nil {
log.Println(err)
}
sw.Stop()
2020-10-21 18:49:23 +00:00
if p[0] != '_' {
sw.Start("prepare")
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
2019-08-25 10:30:00 +00:00
if err != nil {
return err
}
2020-10-21 18:49:23 +00:00
defer f.Close()
if page.Content[0] == '{' || page.Content[0] == '[' {
var buf bytes.Buffer
err = json.Indent(&buf, []byte(page.Content), "", " ")
if err != nil {
return err
}
_, err = buf.WriteTo(f)
if err != nil {
return err
}
} else {
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
}
sw.Stop()
sw.Start("backrefs")
err = processBackrefs(fp.dirname, page)
2019-08-25 10:30:00 +00:00
if err != nil {
2020-10-21 18:49:23 +00:00
return fmt.Errorf("while processing backrefs: %s", err)
2019-08-25 10:30:00 +00:00
}
2020-10-21 18:49:23 +00:00
sw.Stop()
sw.Start("git")
err = saveWithGit(fp, p, summary, author)
if err != nil {
2021-08-07 18:18:30 +00:00
log.Printf("Error while saving to git: %s", err)
2020-10-21 18:49:23 +00:00
// return fmt.Errorf("while saving to git: %w", err)
}
sw.Stop()
sw.Start("index")
2021-08-18 19:01:18 +00:00
searchObjects, err := createSearchObjects(fp, page.Name)
2020-10-21 18:49:23 +00:00
if err != nil {
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
}
for _, so := range searchObjects {
if fp.index != nil {
err = fp.index.Index(so.ID, so)
if err != nil {
return fmt.Errorf("while indexing %s: %w", page.Name, err)
}
}
}
sw.Stop()
sw.Start("links")
err = saveLinksIncremental(fp.dirname, page.Title)
sw.Stop()
2019-08-25 10:30:00 +00:00
}
2020-10-21 18:49:23 +00:00
return nil
2020-10-21 18:49:23 +00:00
}
2020-05-07 13:20:36 +00:00
2020-10-21 18:49:23 +00:00
func saveWithNewIDs(dirname string, listItems []ListItemV2, pageName string) ([]ListItem, error) {
var newListItems []ListItem
for _, item := range listItems {
newItem := ListItem{
ID: item.ID.NewID(),
Indented: item.Indented,
Text: item.Text,
}
newListItems = append(newListItems, newItem)
}
return newListItems, nil
}
2021-08-18 19:01:18 +00:00
func (fp* FilePages) saveBlocksFromPage(dirname string, page Page) error {
2021-08-12 22:41:26 +00:00
log.Printf("Processing: %q\n", page.Name)
2020-10-21 18:49:23 +00:00
var listItems []ListItem
err := json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
if err != nil {
2020-10-28 19:01:25 +00:00
log.Println("decoding default failed: %w", err)
2020-10-21 18:49:23 +00:00
var listItemsV2 []ListItemV2
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItemsV2)
listItems, err = saveWithNewIDs(dirname, listItemsV2, page.Name)
if err != nil {
return fmt.Errorf("while rewriting %s to use new ids: %w", page.Name, err)
}
}
2020-10-21 18:49:23 +00:00
blocks := make(map[string]Block)
prevList := make(map[string]ListItem)
root := "root"
2021-08-18 19:01:18 +00:00
parentBlock, err := fp.blockRepo.GetBlock(page.Name)
2020-07-01 14:40:10 +00:00
if err != nil {
2020-10-21 18:49:23 +00:00
log.Println(err)
} else {
root = parentBlock.Parent
}
title := page.Title
if page.Name[0] == '_' {
title = parentBlock.Text
2020-07-01 14:40:10 +00:00
}
2020-10-21 18:49:23 +00:00
2021-08-12 22:41:26 +00:00
rootParentID := page.Name
2020-10-21 18:49:23 +00:00
var parent = ListItem{
Text: title,
Indented: -1,
ID: page.Name,
}
prevList[parent.ID] = parent
blocks[parent.ID] = Block{
Text: title,
2020-10-28 19:19:38 +00:00
Children: []string{},
2020-10-21 18:49:23 +00:00
Parent: root,
}
var prev = &parent
for i, item := range listItems {
2020-10-28 19:01:25 +00:00
if item.Fleeting {
continue
}
2020-10-21 18:49:23 +00:00
prevList[item.ID] = item
if item.Indented > prev.Indented {
parent = *prev
} else if item.Indented == prev.Indented {
// nothing
} else if item.Indented <= parent.Indented {
for item.Indented <= parent.Indented {
if block, e := blocks[parent.ID]; e {
parent = prevList[block.Parent]
}
}
}
2021-08-12 22:41:26 +00:00
blocks[item.ID] = Block{
Text: item.Text,
Children: []string{},
Parent: parent.ID,
}
2020-10-21 18:49:23 +00:00
if block, e := blocks[parent.ID]; e {
block.Children = append(block.Children, item.ID)
blocks[parent.ID] = block
} else {
log.Println("Block missing")
}
prev = &listItems[i]
}
2021-08-18 19:01:18 +00:00
// TODO: find out if this is still necessary
2021-08-12 22:41:26 +00:00
log.Printf("Loading parent block: %q", rootParentID)
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, rootParentID))
2020-10-21 18:49:23 +00:00
if err == nil {
var parentBlock Block
err = json.NewDecoder(f).Decode(&parentBlock)
if err == nil {
2021-08-12 22:41:26 +00:00
if pb, e := blocks[rootParentID]; e {
2020-10-21 18:49:23 +00:00
pb.Text = parentBlock.Text
pb.Parent = parentBlock.Parent
2021-08-12 22:41:26 +00:00
blocks[rootParentID] = pb
log.Printf("Text=%q, Parent=%q", parentBlock.Text, parentBlock.Parent)
2020-10-21 18:49:23 +00:00
}
}
f.Close()
} else {
log.Println(err)
err = nil
}
for id, block := range blocks {
2021-08-18 19:15:41 +00:00
err := fp.blockRepo.SaveBlock(id, block)
2020-10-21 18:49:23 +00:00
if err != nil {
log.Println(err)
}
}
2020-10-21 18:49:23 +00:00
2020-05-30 22:56:10 +00:00
return err
2018-11-24 12:34:51 +00:00
}
func saveLinksIncremental(dirname, title string) error {
type Document struct {
Title string `json:"title"`
}
var results []Document
f, err := os.Open(filepath.Join(dirname, LinksFile))
if err != nil {
return err
}
err = json.NewDecoder(f).Decode(&results)
if err != nil {
return err
}
f.Close()
titles := make(map[string]bool)
for _, r := range results {
titles[r.Title] = true
}
// Add new? title
titles[title] = true
results = nil
for t, _ := range titles {
results = append(results, Document{t})
}
f, err = os.Create(filepath.Join(dirname, LinksFile))
err = json.NewEncoder(f).Encode(&results)
if err != nil {
return err
}
f.Close()
return nil
}
2020-10-21 18:49:23 +00:00
func saveLinks(mp PagesRepository) error {
type Document struct {
Title string `json:"title"`
}
var results []Document
2020-10-21 18:49:23 +00:00
pages, err := mp.AllPages()
if err != nil {
return err
}
for _, page := range pages {
results = append(results, Document{page.Title})
}
2020-10-21 18:49:23 +00:00
f, err := os.Create(filepath.Join(mp.(*FilePages).dirname, LinksFile))
if err != nil {
return err
}
defer f.Close()
err = json.NewEncoder(f).Encode(&results)
if err != nil {
return err
}
return nil
}
2019-02-19 06:34:52 +00:00
func saveWithGit(fp *FilePages, p string, summary, author string) error {
2018-11-24 12:34:51 +00:00
cmd := exec.Command("git", "add", ".")
cmd.Dir = fp.dirname
err := cmd.Run()
if err != nil {
return fmt.Errorf("while adding page %s: %s", p, err)
}
2018-11-24 12:34:51 +00:00
cmd = exec.Command("git", "commit", "-m", "Changes to "+p+" by "+author+"\n\n"+summary)
cmd.Dir = fp.dirname
2020-10-21 18:49:23 +00:00
// cmd.Stderr = os.Stderr
// cmd.Stdout = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("while commiting page %s: %s", p, err)
}
2019-02-19 06:34:52 +00:00
return nil
2018-11-24 12:34:51 +00:00
}
func (fp *FilePages) Exist(p string) bool {
pageName := filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1))
_, err := os.Stat(pageName)
2018-11-24 12:34:51 +00:00
if err != nil {
2021-08-15 19:36:08 +00:00
return !os.IsNotExist(err)
2018-11-24 12:34:51 +00:00
}
2021-08-15 19:36:08 +00:00
return true
2018-11-24 12:34:51 +00:00
}
func DiffPrettyHtml(diffs []diffmatchpatch.Diff) string {
var buff bytes.Buffer
for _, diff := range diffs {
// text := strings.Replace(html.EscapeString(diff.Text), "\n", "<span class=\"lighter\">&para;</span><br>", -1)
text := html.EscapeString(diff.Text)
switch diff.Type {
case diffmatchpatch.DiffInsert:
_, _ = buff.WriteString("<ins style=\"background:#e6ffe6;\">")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</ins>")
case diffmatchpatch.DiffDelete:
_, _ = buff.WriteString("<del style=\"background:#ffe6e6;\">")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</del>")
case diffmatchpatch.DiffEqual:
_, _ = buff.WriteString("<span>")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</span>")
}
}
return buff.String()
}
func (fp *FilePages) PageHistory(p string) ([]Revision, error) {
page := strings.Replace(p, " ", "_", -1)
cmd := exec.Command("git", "log", "--pretty=oneline", "--no-decorate", "--color=never", page)
cmd.Dir = fp.dirname
output, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
defer output.Close()
err = cmd.Start()
if err != nil {
2020-05-16 19:25:07 +00:00
return nil, fmt.Errorf("while starting: %s", err)
2018-11-24 12:34:51 +00:00
}
buf := bufio.NewScanner(output)
var revisions []Revision
for buf.Scan() {
line := buf.Text()
start := strings.Index(line, " ")
commitId := line[0:start]
rest := line[start+1:]
pageText := gitRevision(fp.dirname, page, commitId)
revisions = append(revisions, Revision{
Version: commitId,
Page: DiffPage{Content: pageText},
Summary: rest,
})
}
dmp := diffmatchpatch.New()
prevText := ""
for i := len(revisions) - 1; i >= 0; i-- {
diffs := dmp.DiffMain(prevText, revisions[i].Page.Content, false)
revisions[i].Page.Diff = template.HTML(DiffPrettyHtml(diffs))
prevText = revisions[i].Page.Content
}
if err := cmd.Wait(); err != nil {
2020-05-16 19:25:07 +00:00
return nil, fmt.Errorf("while waiting: %s", err)
2018-11-24 12:34:51 +00:00
}
return revisions, nil
}
func gitRevision(dirname, page, version string) string {
cmd := exec.Command("git", "show", version+":"+page)
cmd.Dir = dirname
buf := bytes.Buffer{}
cmd.Stdout = &buf
cmd.Start()
cmd.Wait()
return buf.String()
}
2019-02-18 19:47:31 +00:00
func (fp *FilePages) RecentChanges() ([]Change, error) {
2019-03-02 13:06:17 +00:00
cmd := exec.Command("git", "log", "--format=--1--%nDate: %aI%n--2--%n%b%n--3--", "--name-only")
2019-02-18 19:47:31 +00:00
cmd.Dir = fp.dirname
buf := bytes.Buffer{}
cmd.Stdout = &buf
err := cmd.Start()
if err != nil {
return nil, err
}
err = cmd.Wait()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(&buf)
2019-03-02 13:06:17 +00:00
state := 0
2019-02-18 19:47:31 +00:00
var changes []Change
var change Change
2019-03-02 13:06:17 +00:00
body := ""
2019-02-18 19:47:31 +00:00
for scanner.Scan() {
line := scanner.Text()
if line == "--1--" {
2019-03-02 13:06:17 +00:00
state = 1
body = ""
2019-02-18 19:47:31 +00:00
continue
}
if line == "--2--" {
2019-03-02 13:06:17 +00:00
state = 2
continue
}
if line == "--3--" {
state = 3
2019-02-18 19:47:31 +00:00
continue
}
2019-03-02 13:06:17 +00:00
if state == 1 && strings.HasPrefix(line, "Date: ") {
2019-02-18 19:47:31 +00:00
line = line[6:]
changeTime, err := time.Parse(time.RFC3339, line)
if err != nil {
return changes, err
}
change.Date = changeTime
continue
}
2019-03-02 13:06:17 +00:00
if state == 2 {
if line == "" {
continue
}
body = body + line
continue
}
if state == 3 {
2019-02-18 19:47:31 +00:00
if line == "" {
continue
}
change.Page = line
}
2019-03-02 13:06:17 +00:00
change.Body = body
2019-02-18 19:47:31 +00:00
changes = append(changes, change)
}
return changes, nil
}
2020-05-07 13:20:36 +00:00
func (fp *FilePages) AllPages() ([]Page, error) {
2020-10-21 18:49:23 +00:00
log.Println("AllPages", fp.dirname)
2020-05-07 13:20:36 +00:00
files, err := ioutil.ReadDir(fp.dirname)
if err != nil {
return nil, err
}
var pages []Page
for _, file := range files {
2020-07-01 14:40:10 +00:00
if file.Name()[0] == '.' || file.Name()[0] == '_' {
2020-05-17 13:09:18 +00:00
continue
}
if file.Name() == "backrefs.json" {
continue
}
2020-05-07 13:20:36 +00:00
pages = append(pages, fp.Get(file.Name()))
}
return pages, nil
}