/* * Wiki - A wiki with editor * Copyright (c) 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 . */ package main import ( "bytes" "context" "encoding/json" "flag" "fmt" "html" "html/template" "io" "log" "math/rand" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "time" "p83.nl/go/ekster/pkg/util" "p83.nl/go/indieauth" "p83.nl/go/wiki/link" "github.com/blevesearch/bleve/v2" ) func init() { log.SetFlags(log.Lshortfile) rand.Seed(time.Now().UnixNano()) } type authorizedKey string var ( authKey = authorizedKey("authorizedKey") ) var ( mp PagesRepository port = flag.Int("port", 8080, "listen port") baseurl = flag.String("baseurl", "", "baseurl") redirectURI = "" authToken = os.Getenv("API_TOKEN") ) type Day struct { Class string Text string URL string Count string } type Backref struct { Name string Title string LineHTML template.HTML LineEditHTML template.HTML Line string } // Page type Page struct { // Name is the filename of the page Name string // Title is the human-readable title of the page Title string Content string Refs map[string][]Backref Blocks BlockResponse Parent string } type DiffPage struct { Content string Diff template.HTML } type Revision struct { Version string Page DiffPage Summary string } type Change struct { Page string Date time.Time EndDate time.Time Body string Count int DateSummary string TimeSummary string } type PagesRepository interface { Get(p string) Page Save(p string, page Page, summary, author string) error Exist(p string) bool PageHistory(p string) ([]Revision, error) RecentChanges() ([]Change, error) AllPages() ([]Page, error) } type pageBaseInfo struct { BaseURL string RedirectURI string Days []Day Month string Year int PrevMonth string NextMonth string } type indexPage struct { pageBaseInfo Session *Session Title string Name string Content template.HTML Backrefs map[string][]Backref ShowGraph bool TodayPage string } type Node struct { Id int `json:"id"` Label string `json:"label"` Title string `json:"title"` Color *string `json:"color"` Opacity float64 `json:"opacity"` } type Edge struct { From int `json:"from"` To int `json:"to"` } type graphPage struct { pageBaseInfo Session *Session Title string Name string Nodes template.JS Edges template.JS } type Parent struct { Text string ID string } type editPage struct { pageBaseInfo Session *Session Title string TitleHTML template.HTML Content string Name string Editor template.HTML Backrefs map[string][]Backref ShowGraph bool TodayPage string Parent Parent Parents []Parent } type historyPage struct { pageBaseInfo Session *Session Title string Name string History []Revision } type recentPage struct { pageBaseInfo Session *Session Title string Name string Recent []Change } var baseTemplate = []string{"templates/layout.html", "templates/sidebar.html", "templates/sidebar-right.html", "templates/calendar.html"} type indexHandler struct{} type graphHandler struct{} type saveHandler struct { SearchIndex bleve.Index } type editHandler struct{} type historyHandler struct{} type recentHandler struct{} type authHandler struct{} func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if r.Method == http.MethodGet { if r.URL.Path == "/auth/login" { templates := []string{"templates/layout_no_sidebar.html"} templates = append(templates, "templates/login.html") t, err := template.ParseFiles(templates...) if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, nil) if err != nil { http.Error(w, err.Error(), 500) return } } else if r.URL.Path == "/auth/callback" { code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") if state != sess.State { log.Printf("mismatched state: %s != %s", state, sess.State) http.Error(w, "mismatched state", 500) return } authURL := sess.AuthorizationEndpoint verified, response, err := indieauth.VerifyAuthCode(*baseurl, code, redirectURI, authURL) if err != nil { http.Error(w, err.Error(), 500) return } if !verified { http.Redirect(w, r, "/auth/login", 302) return } sess.Me = response.Me sess.LoggedIn = true sess.State = "" http.Redirect(w, r, sess.NextURI, 302) return } else if r.URL.Path == "/auth/logout" { templates := baseTemplate templates = append(templates, "templates/logout.html") t, err := template.ParseFiles(templates...) if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, nil) if err != nil { http.Error(w, err.Error(), 500) return } } } else if r.Method == http.MethodPost { if r.URL.Path == "/auth/login" { r.ParseForm() urlString := r.PostFormValue("url") u, err := url.Parse(urlString) if err != nil { urlString = fmt.Sprintf("https://%s/", urlString) u, err = url.Parse(urlString) if err != nil { http.Error(w, err.Error(), 400) return } } endpoints, err := indieauth.GetEndpoints(u) if err != nil { http.Error(w, err.Error(), 500) return } authURL, err := url.Parse(endpoints.AuthorizationEndpoint) if err != nil { http.Error(w, err.Error(), 500) return } state := util.RandStringBytes(32) sess.AuthorizationEndpoint = authURL.String() sess.Me = urlString sess.LoggedIn = false sess.RedirectURI = redirectURI sess.NextURI = "/" sess.State = state newURL := indieauth.CreateAuthenticationURL(*authURL, urlString, *baseurl, redirectURI, state) http.Redirect(w, r, newURL, 302) return } else if r.URL.Path == "/auth/logout" { sess.LoggedIn = false sess.Me = "" sess.AuthorizationEndpoint = "" http.Redirect(w, r, "/", 302) return } } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *historyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/", http.StatusFound) return } r.ParseForm() page := r.URL.Path[9:] page = resolvePageName(page) history, err := mp.PageHistory(page) if err != nil { http.Error(w, err.Error(), 500) return } funcs := template.FuncMap{ "historyIndex": func(x int, history []Revision) int { return len(history) - x }, } t, err := template.New("layout.html").Funcs(funcs).ParseFiles("templates/layout.html", "templates/history.html") if err != nil { http.Error(w, err.Error(), 500) return } pageBase := getPageBase(mp, time.Now()) pageData := historyPage{ pageBaseInfo: pageBase, Session: sess, Title: "History of " + cleanTitle(page), Name: page, History: history, } err = t.Execute(w, pageData) if err != nil { http.Error(w, err.Error(), 500) return } } func daysInMonth(year int, month time.Month) int { return time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, -1).Day() } func prepareDays(repo PagesRepository, t time.Time) []Day { today := time.Now() var days []Day curDate := t.AddDate(0, 0, -(t.Day() - 1)) preDays := int(curDate.Weekday()) if preDays == 0 { preDays = 7 } preDays -= 1 maxDays := daysInMonth(curDate.Year(), curDate.Month()) endOfMonth := false for i := 0; i < 6; i++ { year, week := curDate.ISOWeek() days = append(days, Day{ Class: "week day", Text: fmt.Sprintf("%02d", week), URL: fmt.Sprintf("/edit/%s", formatWeekPageName(year, week)), Count: "", }) for d := 0; d < 7; d++ { day := -preDays + (i * 7) + 1 + d fday := fmt.Sprintf("%d", day) if day <= 0 { fday = "" } if day > maxDays { fday = "" endOfMonth = true } class := "day" if timeEqual(curDate, today) { class += " today" } if timeEqual(curDate, t) { class += " current" } pageName := formatDatePageName(curDate) if fday != "" && pageHasContent(mp, pageName) { class += " has-content" } days = append(days, Day{ Class: class, Text: fday, URL: fmt.Sprintf("/edit/%s", pageName), Count: "", }) if fday != "" { curDate = curDate.AddDate(0, 0, 1) } if day == maxDays { endOfMonth = true } } if endOfMonth { break } } return days } func pageHasContent(repo PagesRepository, day string) bool { return repo.Exist(day) // page := repo.Get(day) // return page.Blocks.Children != nil } func formatWeekPageName(year int, week int) string { return fmt.Sprintf("%04dW%02d", year, week) } func timeEqual(date time.Time, today time.Time) bool { if date.Year() != today.Year() { return false } if date.Month() != today.Month() { return false } if date.Day() != today.Day() { return false } return true } func getPageBase(repo PagesRepository, t time.Time) pageBaseInfo { clientID := *baseurl pageBase := pageBaseInfo{ BaseURL: clientID, RedirectURI: redirectURI, Days: prepareDays(repo, t), Month: t.Month().String(), Year: t.Year(), } pageBase.PrevMonth = t.AddDate(0, -1, -t.Day()+1).Format("2006-01-02") pageBase.NextMonth = t.AddDate(0, 1, -t.Day()+1).Format("2006-01-02") return pageBase } func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) log.Println(err) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/", http.StatusFound) return } err = r.ParseForm() if err != nil { http.Error(w, err.Error(), 500) log.Println(err) return } isJson := r.PostForm.Get("json") == "1" page := r.PostForm.Get("p") summary := r.PostForm.Get("summary") pageData := Page{Content: r.PostForm.Get("content")} err = mp.Save(page, pageData, summary, sess.Me) if err != nil { log.Println(err) } if isJson { fmt.Print(w, "{}") } else { http.Redirect(w, r, "/"+page, http.StatusFound) } } func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/", http.StatusFound) return } page := r.URL.Path[6:] page = resolvePageName(page) mpPage := mp.Get(page) pageText := mpPage.Content if pageText == "" { pageText = "[]" } var rawMsg json.RawMessage err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg) jsonEditor := pageText != "" && err == nil var editor template.HTML if jsonEditor { editor, err = renderEditor(page, pageText, "json", *baseurl) if err != nil { http.Error(w, err.Error(), 500) return } } else { editor = template.HTML(fmt.Sprintf(`
`, html.EscapeString(pageText))) } curDate, err := ParseDatePageName(page) if err != nil { curDate = time.Now() } pageBase := getPageBase(mp, curDate) title := cleanTitle(mpPage.Title) if newTitle, err := PageTitle(pageText); err == nil { title = newTitle } var parent Parent parent.ID = mpPage.Parent parent.Text = mpPage.Blocks.Texts[parent.ID] var parents []Parent for _, p := range mpPage.Blocks.Parents { var parent Parent parent.ID = p parent.Text = mpPage.Blocks.Texts[p] parents = append(parents, parent) } for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 { parents[i], parents[j] = parents[j], parents[i] } htmlTitle := link.FormatHtmlTitle(title) data := editPage{ pageBaseInfo: pageBase, Session: sess, Title: title, TitleHTML: htmlTitle, Content: pageText, Editor: editor, Name: page, Backrefs: mpPage.Refs, TodayPage: "Today", ShowGraph: page != "Daily_Notes", Parent: parent, Parents: parents, } templates := baseTemplate templates = append(templates, "templates/edit.html") t, err := template.ParseFiles(templates...) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=600") err = t.Execute(w, data) if err != nil { log.Println(err) http.Error(w, err.Error(), 500) return } } func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/", http.StatusFound) return } gb, err := NewGraphBuilder(mp) if err != nil { http.Error(w, err.Error(), 500) return } gb.prepareNodeMap() gb.RemoveNode("DONE") gb.RemoveNode("TODO") gb.RemoveNode("Home") gb.RemoveNode("Projects") gb.RemoveNode("Notes") gb.RemoveNode("Books") gb.RemoveNode("Daily_Notes") gb.RemoveNode("NewJsonTest") gb.RemoveNodeWithSuffix("_2020") err = gb.prepareGraph() if err != nil { http.Error(w, err.Error(), 500) return } var nodesBuf bytes.Buffer var edgesBuf bytes.Buffer err = json.NewEncoder(&nodesBuf).Encode(&gb.nodes) if err != nil { http.Error(w, err.Error(), 500) return } err = json.NewEncoder(&edgesBuf).Encode(&gb.edges) if err != nil { http.Error(w, err.Error(), 500) return } pageBase := getPageBase(mp, time.Now()) data := graphPage{ pageBaseInfo: pageBase, Session: sess, Title: "Graph", Name: "Graph", Nodes: template.JS(nodesBuf.String()), Edges: template.JS(edgesBuf.String()), } templates := baseTemplate templates = append(templates, "templates/graph.html") t, err := template.ParseFiles(templates...) if err != nil { http.Error(w, err.Error(), 500) return } err = t.Execute(w, data) if err != nil { http.Error(w, err.Error(), 500) return } } func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() metaKV, err := regexp.Compile(`(\w[ \w]*)::(?: +(.*))?`) if err != nil { log.Fatal(err) } sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/auth/login", http.StatusFound) return } format := r.URL.Query().Get("format") if format == "" { format = "html" } if !(format == "html" || format == "markdown" || format == "hmarkdown" || format == "json" || format == "metakv") { http.Error(w, "unknown format", http.StatusBadRequest) } page := r.URL.Path[1:] if page == "favicon.ico" { http.Error(w, "Not Found", 404) return } page = resolvePageName(page) mpPage := mp.Get(page) pageText := mpPage.Content if (format == "" || format == "html") && pageText == "" { http.Redirect(w, r, "/edit/"+page, 302) return } if pageText == "" { pageText = "[]" } var rawMsg json.RawMessage err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg) title := cleanTitle(mpPage.Title) jsonPage := pageText != "" && err == nil if jsonPage { if format == "json" { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") // Shortcut for json output _, err := io.WriteString(w, pageText) if err != nil { http.Error(w, err.Error(), 500) } return } else if format == "metakv" { so, err := createStructuredFormat(mpPage) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") enc := json.NewEncoder(w) enc.SetIndent("", " ") err = enc.Encode(so) if err != nil { http.Error(w, err.Error(), 500) } return } var listItems []struct { Indented int Text string } err = json.NewDecoder(strings.NewReader(pageText)).Decode(&listItems) if err != nil { http.Error(w, err.Error(), 500) return } builder := strings.Builder{} if format == "markdown" || format == "html" { for _, item := range listItems { lines := strings.Split(item.Text, "\n") if len(lines) > 1 { first := true for _, line := range lines { if first { builder.WriteString(strings.Repeat(" ", item.Indented)) builder.WriteString("- ") builder.WriteString(line) builder.WriteByte('\n') first = false } else { builder.WriteString(strings.Repeat(" ", item.Indented+1)) builder.WriteString(line) builder.WriteByte('\n') } } } else { if matches := metaKV.FindStringSubmatch(item.Text); matches != nil { if matches[1] == "Title" { title = matches[2] } } builder.WriteString(strings.Repeat(" ", item.Indented)) builder.WriteString("- ") builder.WriteString(item.Text) builder.WriteByte('\n') } } } else if format == "hmarkdown" { for _, item := range listItems { builder.WriteString(item.Text) builder.WriteByte('\n') builder.WriteByte('\n') } } pageText = builder.String() } if format == "html" { pageText = metaKV.ReplaceAllString(pageText, "**$1**: $2") pageText = renderLinks(pageText, false) pageText = renderMarkdown2(pageText) curDate, err := ParseDatePageName(page) if err != nil { curDate = time.Now() } pageBase := getPageBase(mp, curDate) data := indexPage{ pageBaseInfo: pageBase, Session: sess, Title: title, Content: template.HTML(pageText), Name: page, Backrefs: mpPage.Refs, ShowGraph: page != "Daily_Notes", TodayPage: "Today", } templates := []string{"templates/layout_no_sidebar.html"} templates = append(templates, "templates/view.html") t, err := template.ParseFiles(templates...) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=600") err = t.Execute(w, data) if err != nil { log.Println(err) // http.Error(w, err.Error(), 500) return } } else if format == "markdown" || format == "hmarkdown" { w.Header().Set("Content-Type", "text/markdown") w.Header().Set("Cache-Control", "public, max-age=600") _, err = io.WriteString(w, "# ") _, err = io.WriteString(w, title) _, err = io.WriteString(w, "\n\n") _, err = io.WriteString(w, pageText) } } func renderLinks(pageText string, edit bool) string { hrefRE := regexp.MustCompile(`#?\[\[\s*([^\]]+)\s*\]\]`) pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string { tag := false if s[0] == '#' { s = strings.TrimPrefix(s, "#[[") tag = true } else { s = strings.TrimPrefix(s, "[[") } s = strings.TrimSuffix(s, "]]") s = strings.TrimSpace(s) if tag { switch s { case "TODO": return fmt.Sprint(`[ ] `) case "DONE": return fmt.Sprint(`[X] `) default: return fmt.Sprintf(`%s`, cleanNameURL(s), s) } } editPart := "" if edit { editPart = "edit/" } return fmt.Sprintf("[%s](/%s%s)", s, editPart, cleanNameURL(s)) }) tagRE := regexp.MustCompile(`#(\S+)`) pageText = tagRE.ReplaceAllStringFunc(pageText, func(s string) string { s = strings.TrimPrefix(s, "#") s = strings.TrimSpace(s) return fmt.Sprintf(`%s`, url.PathEscape(cleanNameURL(s)), s) }) return pageText } func (h *recentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil { http.Error(w, err.Error(), 500) return } defer func() { if err := sess.Flush(); err != nil { log.Println(err) } }() if !sess.LoggedIn { http.Redirect(w, r, "/", http.StatusFound) return } r.ParseForm() changes, err := mp.RecentChanges() if err != nil { http.Error(w, err.Error(), 500) return } // group recentchanges on Page if len(changes) > 0 { f := groupRecentChanges(changes) changes = changes[f:] } for i, c := range changes { if c.Count <= 1 { changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006") changes[i].TimeSummary = c.Date.Format("15:04") } else { changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006") changes[i].TimeSummary = fmt.Sprintf("%s - %s", c.Date.Format("15:04"), c.EndDate.Format("15:04")) } } t, err := template.New("layout.html").ParseFiles("templates/layout.html", "templates/recent.html") if err != nil { http.Error(w, err.Error(), 500) return } pageBase := getPageBase(mp, time.Now()) err = t.Execute(w, recentPage{ pageBaseInfo: pageBase, Session: sess, Title: "Recent changes", Name: "Recent changes", Recent: changes, }) if err != nil { http.Error(w, err.Error(), 500) return } } type LinkResponseMetaImage struct { Url string `json:"url"` } type LinkResponseMeta struct { Title string `json:"title"` Description string `json:"description"` Image LinkResponseMetaImage `json:"image"` } type LinkResponse struct { Success int `json:"success"` Link string `json:"link"` Meta LinkResponseMeta `json:"meta"` } func main() { flag.Parse() *baseurl = strings.TrimRight(*baseurl, "/") + "/" redirectURI = fmt.Sprintf("%sauth/callback", *baseurl) dataDir := "data" searchIndex, err := createSearchIndex(dataDir, "_page-index") if err != nil { log.Fatal(err) } defer searchIndex.Close() sh, err := NewSearchHandler(searchIndex) if err != nil { log.Fatal(err) } mp = NewFilePages(dataDir, searchIndex) repo := NewBlockRepo("data") http.Handle("/auth/", &authHandler{}) http.HandleFunc("/api/block/view", wrapAuth(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if !r.Context().Value(authKey).(bool) { http.Error(w, "Unauthorized", 401) return } if r.Method != "GET" { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } id := r.URL.Query().Get("id") block, err := repo.Load(id) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") enc := json.NewEncoder(w) enc.SetIndent("", " ") err = enc.Encode(block) if err != nil { http.Error(w, err.Error(), 500) } })) http.HandleFunc("/api/block/", wrapAuth(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if !r.Context().Value(authKey).(bool) { http.Error(w, "Unauthorized", 401) return } if r.Method != "GET" { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } format := r.URL.Query().Get("format") if format == "" { format = "json" } if !(format == "json" || format == "metakv") { http.Error(w, "unknown format", http.StatusBadRequest) } page := r.URL.Path page = strings.TrimPrefix(page, "/api/block/") page = resolvePageName(page) mpPage := mp.Get(page) pageText := mpPage.Content if pageText == "" { http.NotFound(w, r) return } if format == "json" { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") // Shortcut for json output _, err := io.WriteString(w, pageText) if err != nil { http.Error(w, err.Error(), 500) } } else if format == "metakv" { so, err := createStructuredFormat(mpPage) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") enc := json.NewEncoder(w) enc.SetIndent("", " ") err = enc.Encode(so) if err != nil { http.Error(w, err.Error(), 500) } } })) http.HandleFunc("/api/block/replace", wrapAuth(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, err.Error(), 400) return } id := r.Form.Get("id") if id == "" { http.Error(w, "missing id", 400) return } block, err := repo.Load(id) block.Text = r.Form.Get("text") err = repo.Save(id, block) // update search index searchObjects, err := createSearchObjects(id) batch := searchIndex.NewBatch() for _, so := range searchObjects { err = batch.Index(so.ID, so) if err != nil { log.Println(err) } } err = searchIndex.Batch(batch) if err != nil { log.Println(err) } // TODO: update backlinks return })) http.HandleFunc("/api/block/append", wrapAuth(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, err.Error(), 400) return } id := r.Form.Get("id") if id == "" { http.Error(w, "missing id", 400) return } newBlock := Block{ Text: r.Form.Get("text"), Children: []string{}, Parent: id, } newId := &ID{"1", true} generatedID := newId.NewID() err = repo.Save(generatedID, newBlock) block, err := repo.Load(id) block.Children = append(block.Children, generatedID) err = repo.Save(id, block) // update search index sw := stopwatch{} sw.Start("createSearchObjects") searchObjects, err := createSearchObjects(id) batch := searchIndex.NewBatch() for _, so := range searchObjects { err = batch.Index(so.ID, so) if err != nil { log.Println(err) } } err = searchIndex.Batch(batch) if err != nil { log.Println(err) } sw.Stop() return })) http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") http.ServeFile(w, r, filepath.Join(dataDir, LinksFile)) }) http.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) { gb, err := NewGraphBuilder(mp) if err != nil { http.Error(w, err.Error(), 500) return } for _, name := range r.URL.Query()["name"] { err = gb.buildFromCenter(name) if err != nil { http.Error(w, err.Error(), 500) return } } // Keep a copy of the nodes, buildFromCenter appends to the nodeMap var nodes []string for k := range gb.nodeMap { nodes = append(nodes, k) } for _, node := range nodes { err = gb.buildFromCenter(node) } err = gb.prepareGraph() if err != nil { http.Error(w, err.Error(), 500) return } type data struct { Nodes []Node `json:"nodes"` Edges []Edge `json:"edges"` } err = json.NewEncoder(w).Encode(&data{gb.nodes, gb.edges}) if err != nil { http.Error(w, err.Error(), 500) return } }) http.HandleFunc("/api/calendar/", func(w http.ResponseWriter, r *http.Request) { dateString := r.URL.Path[len("/api/calendar/"):] curDate, err := time.Parse("2006-01-02", dateString) if err != nil { curDate = time.Now() } pageBase := getPageBase(mp, curDate) t, err := template.ParseFiles("templates/calendar.html") if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=600") err = t.ExecuteTemplate(w, "calendar", pageBase) if err != nil { log.Println(err) http.Error(w, err.Error(), 500) return } }) http.Handle("/search/", sh) http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist")))) http.Handle("/save/", &saveHandler{}) http.Handle("/edit/", &editHandler{}) http.Handle("/history/", &historyHandler{}) http.Handle("/recent/", &recentHandler{}) http.Handle("/graph/", &graphHandler{}) http.Handle("/", &indexHandler{}) fmt.Printf("Running on port %d\n", *port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) } func wrapAuth(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth == "" || (authToken != "" && (auth == "Token "+authToken || auth == "Bearer "+authToken)) { r = r.WithContext(context.WithValue(r.Context(), authKey, auth != "")) // auth == "", require cookie in handler handler.ServeHTTP(w, r) return } http.Error(w, "Authorization Required", 401) } } func createSearchIndex(dataDir, indexName string) (bleve.Index, error) { indexDir := filepath.Join(dataDir, indexName) if _, err := os.Stat(indexDir); os.IsNotExist(err) { indexMapping := bleve.NewIndexMapping() documentMapping := bleve.NewDocumentMapping() pageFieldMapping := bleve.NewTextFieldMapping() pageFieldMapping.Store = true documentMapping.AddFieldMappingsAt("page", pageFieldMapping) titleFieldMapping := bleve.NewTextFieldMapping() titleFieldMapping.Store = true documentMapping.AddFieldMappingsAt("title", titleFieldMapping) linkFieldMapping := bleve.NewTextFieldMapping() linkFieldMapping.Store = true documentMapping.AddFieldMappingsAt("link", linkFieldMapping) textFieldMapping := bleve.NewTextFieldMapping() textFieldMapping.Store = true documentMapping.AddFieldMappingsAt("text", textFieldMapping) dateFieldMapping := bleve.NewDateTimeFieldMapping() dateFieldMapping.Store = false dateFieldMapping.Index = true documentMapping.AddFieldMappingsAt("date", dateFieldMapping) indexMapping.AddDocumentMapping("block", documentMapping) searchIndex, err := bleve.New(indexDir, indexMapping) if err != nil { return nil, err } fp := NewFilePages(dataDir, nil) pages, err := fp.AllPages() if err != nil { return nil, err } for _, page := range pages { searchObjects, err := createSearchObjects(page.Name) if err != nil { log.Println(err) continue } for _, so := range searchObjects { err = searchIndex.Index(so.ID, so) if err != nil { return nil, err } } } return searchIndex, nil } else { searchIndex, err := bleve.Open(indexDir) if err != nil { return nil, err } return searchIndex, nil } } func resolvePageName(name string) string { if name == "" { return "Home" } if name == "Today" { return todayPage() } return name }