GoWiki/main.go

549 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bufio"
"crypto/subtle"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/search/highlight/highlighter/html"
"github.com/gorilla/mux"
"github.com/kardianos/service"
"github.com/mandolyte/mdtopdf"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
//-------------------------------------------------------------------------------------------------
// Service Configuration
var logger service.Logger
type program struct{}
func (p *program) Start(s service.Service) error {
// Start should not block. Do the actual work async.
go p.run()
return nil
}
func (p *program) run() {
wikimain()
}
func (p *program) Stop(s service.Service) error {
// Stop should not block. Return with a few seconds.
return nil
}
func main() {
svcConfig := &service.Config{
Name: "GoWiki",
DisplayName: "GoWiki",
Description: "GoWiki Service",
}
prg := &program{}
s, err := service.New(prg, svcConfig)
if err != nil {
log.Fatal(err)
}
if len(os.Args) > 1 {
err = service.Control(s, os.Args[1])
if err != nil {
log.Fatal(err)
}
return
}
logger, err = s.Logger(nil)
if err != nil {
log.Fatal(err)
}
err = s.Run()
if err != nil {
logger.Error(err)
}
}
//-------------------------------------------------------------------------------------------------
var config *Configuration
type Configuration struct {
Host string
Port string
DataPath string
DataPathFTS string
WebUser string
WebPasswd string
}
// our main function
func wikimain() {
// Startverzeichnis auslesen und als Arbeitsverzeichnis setzen
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
os.Chdir(dir)
config = readConfig(path.Join(dir, "config.json"))
// Ausgeben der Config-Optionen
fmt.Println("Host: " + config.Host)
fmt.Println("Post: " + config.Port)
fmt.Println("DataPath: " + config.DataPath)
fmt.Println("DataPathFTS: " + config.DataPathFTS)
// DataPath-Verzeichnis anlegen, wenn es noch nicht existiert
if !directoryExists(config.DataPath) {
os.MkdirAll(config.DataPath, os.ModePerm)
// Default Seiten anlegen
bStartmd, _ := Asset("web/pages/start.md")
bHilfemd, _ := Asset("web/pages/hilfe.md")
bPlaygroundmd, _ := Asset("web/pages/playground.md")
bSidebarmd, _ := Asset("web/pages/sidebar.md")
err := ioutil.WriteFile(path.Join(config.DataPath, "start.md"), bStartmd, 0644)
check(err)
err = ioutil.WriteFile(path.Join(config.DataPath, "sidebar.md"), bSidebarmd, 0644)
check(err)
err = ioutil.WriteFile(path.Join(config.DataPath, "hilfe.md"), bHilfemd, 0644)
check(err)
err = ioutil.WriteFile(path.Join(config.DataPath, "playground.md"), bPlaygroundmd, 0644)
check(err)
}
// Git Repository Init
if !directoryExists(path.Join(config.DataPath, ".git")) {
_, err := git.PlainInit(config.DataPath, false)
check(err)
GitCommit("Initial Wiki Commit")
}
// Volltextsuche
if config.DataPathFTS != "" {
if strings.HasPrefix(config.DataPathFTS, config.DataPath) {
fmt.Println("FTS disabled (Please don´t put the DataPathFTS inside the DataPath)")
} else {
fmt.Println("FTS enabled")
if !directoryExists(config.DataPathFTS) {
fmt.Println("Create new FTS Data in " + config.DataPathFTS)
mapping := bleve.NewIndexMapping()
index, err := bleve.New(config.DataPathFTS, mapping)
check(err)
filepath.Walk(config.DataPath, func(filepath string, info os.FileInfo, err error) error {
tmpPath := strings.Replace(filepath, "\\", "/", -1)
tmpPath = strings.Replace(tmpPath, "data/", "", -1)
if !info.IsDir() && !strings.HasPrefix(tmpPath, ".git") {
b, err := ioutil.ReadFile(path.Join(config.DataPath, tmpPath))
check(err)
indexData := struct {
Text string
}{
string(b),
}
err = index.Index(tmpPath, indexData)
check(err)
fmt.Println("Indexed: " + info.Name())
}
return nil
})
index.Close()
}
}
}
/*
repo, err := git.PlainOpen(config.DataPath)
check(err)
ref, err := repo.Head()
check(err)
// ... retrieves the commit history
//since := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
//until := time.Date(2019, 7, 30, 0, 0, 0, 0, time.UTC)
var filename = "playground.md"
cIter, err := repo.Log(&git.LogOptions{From: ref.Hash(), FileName: &filename}) //, Since: &since, Until: &until})
check(err)
// ... just iterates over the commits, printing it
err = cIter.ForEach(func(c *object.Commit) error {
fmt.Println(c.Files())
fmt.Println(c.Committer.When)
return nil
})
*/
router := mux.NewRouter()
router.HandleFunc("/_api/md/{pagename:.*}", basicAuth(getRawPage)).Methods("GET")
router.HandleFunc("/_api/pdf/{pagename:.*}", basicAuth(getPDFPage)).Methods("GET")
router.HandleFunc("/_api/pinfo/{pagename:.*}", basicAuth(getPageInfo)).Methods("GET")
router.HandleFunc("/_api/fts/{searchterm:.*}", basicAuth(getFTS)).Methods("GET")
router.HandleFunc("/_api/config", basicAuth(getWikiConfig)).Methods("GET")
router.HandleFunc("/{pagename:.*}", basicAuth(getHTMLPage)).Methods("GET")
router.HandleFunc("/{pagename:.*}", basicAuth(postHTMLPage)).Methods("POST")
log.Fatal(http.ListenAndServe(":"+config.Port, router))
}
func getPDFPage(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
pPageName := params["pagename"] + ".md"
// Hack für anzeige der start.md wenn / aufgerufen wird
if pPageName == ".md" {
pPageName = "start.md"
}
content, err := ioutil.ReadFile(path.Join(config.DataPath, pPageName))
check(err)
pdfRender := mdtopdf.NewPdfRenderer("", "", "./tmp.pdf", "")
// get the unicode translator
tr := pdfRender.Pdf.UnicodeTranslatorFromDescriptor("cp1252")
pdfRender.Normal = mdtopdf.Styler{Font: "Arial", Style: "",
Size: 8, Spacing: 1,
TextColor: mdtopdf.Color{0, 0, 0},
FillColor: mdtopdf.Color{255, 255, 255}}
err = pdfRender.Process([]byte(tr(string(content))))
check(err)
pdfcontent, err := ioutil.ReadFile("./tmp.pdf")
check(err)
w.Header().Set("Content-Type", "application/pdf")
w.Write(pdfcontent)
}
func getHTMLPage(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
if strings.HasSuffix(params["pagename"], ".css") {
//fmt.Println("CSS: " + params["pagename"])
b, _ := Asset("web/" + params["pagename"])
w.Header().Set("Content-Type", "text/css")
w.Write(b)
} else if strings.HasSuffix(params["pagename"], ".js") {
//fmt.Println("JS: " + params["pagename"])
b, _ := Asset("web/" + params["pagename"])
w.Header().Set("Content-Type", "application/javascript")
w.Write(b)
} else {
// Directory Listing für die Sidebar
sidebarAusgabe := " \r\n \r\n**Inhaltsverzeichnis** \r\n\r\n"
filepath.Walk(config.DataPath, func(path string, info os.FileInfo, err error) error {
tmpPath := strings.Replace(path, "\\", "/", -1)
tmpPath = strings.Replace(tmpPath, "data", "", -1)
tmpPathSplit := strings.Split(tmpPath, "/")
sidebarTABs := ""
for i, _ := range tmpPathSplit {
//fmt.Println(i, " => ", string(c))
if i > 1 {
sidebarTABs = sidebarTABs + " "
}
}
if info.Name() != "data" && !strings.HasPrefix(tmpPath, "/.git") && info.Name() != "start.md" && info.Name() != "sidebar.md" && info.Name() != "hilfe.md" {
if !strings.HasSuffix(info.Name(), ".md") {
sidebarAusgabe = sidebarAusgabe + sidebarTABs + "* [" + strings.TrimSuffix(info.Name(), ".md") + "](" + strings.TrimSuffix(tmpPath, ".md") + "/start) \r\n"
} else {
sidebarAusgabe = sidebarAusgabe + sidebarTABs + "* [" + strings.TrimSuffix(info.Name(), ".md") + "](" + strings.TrimSuffix(tmpPath, ".md") + ") \r\n"
}
}
return nil
})
//tmpl := template.Must(template.ParseFiles("./web/index.html"))
bTmpl, _ := Asset("web/index.html")
tmpl := template.Must(template.New("tml").Parse(string(bTmpl)))
data := MarkDownData{
PageTitle: " ",
MDText: "",
SideBar: sidebarAusgabe,
}
tmpl.Execute(w, data)
}
}
func getRawPage(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
pPageName := params["pagename"] + ".md"
// Hack für anzeige der start.md wenn / aufgerufen wird
if pPageName == ".md" {
pPageName = "start.md"
}
pageContent := "Empty"
//fmt.Println(path.Join(config.DataPath, pPageName))
b, err := ioutil.ReadFile(path.Join(config.DataPath, pPageName))
check(err)
pageContent = string(b)
json.NewEncoder(w).Encode(pageContent)
}
func postHTMLPage(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
pPageName := params["pagename"] + ".md"
// Hack für anzeige der start.md wenn / aufgerufen wird
if pPageName == ".md" {
pPageName = "start.md"
}
// Verzeichnis erstellen
dir := path.Dir(pPageName)
if !directoryExists(path.Join(config.DataPath, dir)) {
os.MkdirAll(path.Join(config.DataPath, dir), os.ModePerm)
}
//{ "MDText":"**test3**" }
var oBody TPostBody
json.NewDecoder(r.Body).Decode(&oBody)
//fmt.Println(oBody)
f, err := os.Create(path.Join(config.DataPath, pPageName))
check(err)
defer f.Close()
fw := bufio.NewWriter(f)
_, err = fw.WriteString(oBody.MDText)
check(err)
fw.Flush()
// Git Commit
GitCommit("Auto-Commit Page: " + pPageName)
// FTS Index
BleveIndex(pPageName)
json.NewEncoder(w).Encode("OK")
}
func getFTS(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
pSearchterm := params["searchterm"]
if config.DataPathFTS != "" {
if directoryExists(config.DataPathFTS) {
index, err := bleve.Open(config.DataPathFTS)
check(err)
query := bleve.NewQueryStringQuery(pSearchterm)
search := bleve.NewSearchRequest(query)
search.Highlight = bleve.NewHighlightWithStyle(html.Name)
search.Size = 30
searchResults, err := index.Search(search)
index.Close()
//fmt.Println("SR: " + searchResults.String())
json.NewEncoder(w).Encode(searchResults.Hits)
}
}
}
func getPageInfo(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
pPageName := params["pagename"] + ".md"
// Hack für anzeige der start.md wenn / aufgerufen wird
if pPageName == ".md" {
pPageName = "start.md"
}
f, err := os.Open(path.Join(config.DataPath, pPageName))
check(err)
statinfo, err := f.Stat()
check(err)
data := struct {
PageName string
ModTime time.Time
}{
pPageName,
statinfo.ModTime(),
}
json.NewEncoder(w).Encode(data)
}
func getWikiConfig(w http.ResponseWriter, r *http.Request) {
FTS := false
Login := false
if config.DataPathFTS != "" {
FTS = true
}
if config.WebUser != "" && config.WebPasswd != "" {
Login = true
}
data := struct {
FTS bool
Login bool
}{
FTS,
Login,
}
json.NewEncoder(w).Encode(data)
}
func basicAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if config.WebUser == "" && config.WebPasswd == "" {
handler(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(config.WebUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(config.WebPasswd)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="Login GoWiki"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
return
}
handler(w, r)
}
}
//--------------------------------------------------------------------------
// Typen
//--------------------------------------------------------------------------
type MarkDownData struct {
PageTitle string
MDText string
SideBar string
}
type TPostBody struct {
MDText string
}
//--------------------------------------------------------------------------
// Hilfsfunktionen
//--------------------------------------------------------------------------
func check(e error) {
if e != nil {
fmt.Println(e)
}
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func directoryExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}
func readConfig(filename string) *Configuration {
// initialize conf with default values.
conf := &Configuration{Host: "http://127.0.0.1", Port: "8000", DataPath: "./data", DataPathFTS: "", WebUser: "", WebPasswd: ""}
b, err := ioutil.ReadFile(filename)
if err != nil {
return conf
}
if err = json.Unmarshal(b, conf); err != nil {
return conf
}
return conf
}
func GitCommit(CommitText string) {
// Git Repo öffnen
repo, err := git.PlainOpen(config.DataPath)
check(err)
// Working Tree setzen
wt, err := repo.Worktree()
check(err)
// Alle Dateien zum Working Tree hinzufügen
_, err = wt.Add(".")
check(err)
// Status ausgeben
//status, err := wt.Status()
//check(err)
//fmt.Println(status)
// Neuen Commit Erstellen
_, err = wt.Commit(CommitText, &git.CommitOptions{
Author: &object.Signature{
Name: "GoWiki",
Email: "gowiki@local.org",
When: time.Now(),
},
})
check(err)
// Commit infos anzeigen
//obj, err := repo.CommitObject(commit)
//check(err)
//fmt.Println(obj)
}
func BleveIndex(PageName string) {
if config.DataPathFTS != "" {
if directoryExists(config.DataPathFTS) {
index, err := bleve.Open(config.DataPathFTS)
check(err)
b, err := ioutil.ReadFile(path.Join(config.DataPath, PageName))
check(err)
indexData := struct {
Text string
}{
string(b),
}
err = index.Index(PageName, indexData)
check(err)
index.Close()
}
}
}