diff --git a/cmd/server/main.go b/cmd/server/main.go index e4d8976..e406271 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,6 +10,7 @@ import ( "forgejo.gwairfelin.com/max/gonotes/internal/conf" "forgejo.gwairfelin.com/max/gonotes/internal/middleware" + "forgejo.gwairfelin.com/max/gonotes/internal/notes" "forgejo.gwairfelin.com/max/gonotes/internal/notes/views" ) @@ -23,6 +24,11 @@ func main() { conf.LoadConfig(confFile) + err := notes.Init() + if err != nil { + log.Fatal(err) + } + log.SetOutput(os.Stdout) router := http.NewServeMux() diff --git a/go.mod b/go.mod index 8f27dd2..94853c5 100644 --- a/go.mod +++ b/go.mod @@ -9,3 +9,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 github.com/teekennedy/goldmark-markdown v0.5.1 ) + +require ( + github.com/yuin/goldmark-meta v1.1.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum index 42e8514..b7c1a06 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,12 @@ github.com/teekennedy/goldmark-markdown v0.5.1 h1:2lIlJ3AcIwaD1wFl4dflJSJFMhRTKE github.com/teekennedy/goldmark-markdown v0.5.1/go.mod h1:so260mNSPELuRyynZY18719dRYlD+OSnAovqsyrOMOM= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= go.abhg.dev/goldmark/toc v0.11.0 h1:IRixVy3/yVPKvFBc37EeBPi8XLTXrtH6BYaonSjkF8o= go.abhg.dev/goldmark/toc v0.11.0/go.mod h1:XMFIoI1Sm6dwF9vKzVDOYE/g1o5BmKXghLG8q/wJNww= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 114fc3a..0428e36 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -89,6 +89,8 @@ func LoadConfig(path string) { if err != nil { log.Fatal(err) } + + log.Printf("Config is %+v", Conf) } func FetchAssets() { diff --git a/internal/notes/notes.go b/internal/notes/notes.go index dbcc9f7..99d5d40 100644 --- a/internal/notes/notes.go +++ b/internal/notes/notes.go @@ -11,10 +11,12 @@ import ( "log" "os" "path/filepath" + "strings" "forgejo.gwairfelin.com/max/gonotes/internal/conf" markdown "github.com/teekennedy/goldmark-markdown" "github.com/yuin/goldmark" + meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" astext "github.com/yuin/goldmark/extension/ast" @@ -30,6 +32,66 @@ type Note struct { BodyRendered template.HTML Body []byte Owner string + Viewers []string +} + +type NoteStore struct { + notes map[string][]*Note +} + +var ( + Notes NoteStore + md goldmark.Markdown +) + +func Init() error { + Notes = NoteStore{ + notes: make(map[string][]*Note), + } + + md = goldmark.New( + goldmark.WithExtensions( + extension.TaskList, meta.Meta, + ), + ) + + notesDir := conf.Conf.NotesDir + + files, err := os.ReadDir(notesDir) + if err != nil { + if os.IsNotExist(err) { + os.MkdirAll(notesDir, os.FileMode(0750)) + } else { + log.Print(err.Error()) + return err + } + } + log.Printf("Looking in %s", notesDir) + for _, f := range files { + if !f.IsDir() { + encodedTitle := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) + note, err := LoadNote(encodedTitle) + if err != nil { + return err + } + + title := note.Title + + log.Printf("Found note %s (title '%s', owner %s)", encodedTitle, title, note.Owner) + + Notes.notes[note.Owner] = append(Notes.notes[note.Owner], note) + + for _, viewer := range note.Viewers { + Notes.notes[viewer] = append(Notes.notes[viewer], note) + } + } + } + + return nil +} + +func (ns *NoteStore) Get(owner string) []*Note { + return ns.notes[owner] } func fmtPath(path string) string { @@ -38,18 +100,14 @@ func fmtPath(path string) string { // Save a note to a path derived from the title func (n *Note) Save() error { - filename := filepath.Join(conf.Conf.NotesDir, n.Owner, fmtPath(n.EncodedTitle())) + filename := filepath.Join(conf.Conf.NotesDir, fmtPath(n.EncodedTitle())) return os.WriteFile(filename, n.Body, 0600) } // Render the markdown content of the note to HTML func (n *Note) Render() { var buf bytes.Buffer - md := goldmark.New( - goldmark.WithExtensions( - extension.TaskList, - ), - ) + err := md.Convert(n.Body, &buf) if err != nil { log.Fatal(err) @@ -59,18 +117,45 @@ func (n *Note) Render() { } // LoadNote from the disk. The path is derived from the title -func LoadNote(owner string, encodedTitle string) (*Note, error) { - filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(encodedTitle)) +func LoadNote(encodedTitle string) (*Note, error) { + filename := filepath.Join(conf.Conf.NotesDir, fmtPath(encodedTitle)) body, err := os.ReadFile(filename) if err != nil { return nil, err } title := DecodeTitle(encodedTitle) - return &Note{Title: title, Body: body, Owner: owner}, nil + + var buf bytes.Buffer + context := parser.NewContext() + if err := md.Convert([]byte(body), &buf, parser.WithContext(context)); err != nil { + return nil, err + } + metaData := meta.Get(context) + + log.Printf("note has frontmatter %+v", metaData) + owner, ok := metaData["owner"].(string) + if !ok { + return nil, errors.New("invalid note, missing 'owner' in frontmatter") + } + + note := &Note{Title: title, Body: body, Owner: owner} + + viewers := metaData["viewers"].([]interface{}) + + for _, viewer := range viewers { + v, ok := viewer.(string) + if !ok { + return nil, errors.New("invalid note, non string type in 'viewers' in frontmatter") + } + note.Viewers = append(note.Viewers, v) + } + log.Printf("Note %s shared with %v", note.Title, note.Viewers) + + return note, nil } -func DeleteNote(owner string, title string) error { - filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(title)) +func DeleteNote(title string) error { + filename := filepath.Join(conf.Conf.NotesDir, fmtPath(title)) return os.Remove(filename) } diff --git a/internal/notes/views/views.go b/internal/notes/views/views.go index 48db392..ba590c8 100644 --- a/internal/notes/views/views.go +++ b/internal/notes/views/views.go @@ -3,14 +3,9 @@ package views import ( "log" "net/http" - "os" - "path" - "path/filepath" "strconv" - "strings" urls "forgejo.gwairfelin.com/max/gispatcho" - "forgejo.gwairfelin.com/max/gonotes/internal/conf" "forgejo.gwairfelin.com/max/gonotes/internal/middleware" "forgejo.gwairfelin.com/max/gonotes/internal/notes" "forgejo.gwairfelin.com/max/gonotes/internal/templ" @@ -40,18 +35,19 @@ func GetRoutes(prefix string) *http.ServeMux { } func view(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(middleware.ContextKey("user")).(string) + // user := r.Context().Value(middleware.ContextKey("user")).(string) title := r.PathValue("note") - note, err := notes.LoadNote(user, title) + note, err := notes.LoadNote(title) urlEdit := myurls.Reverse("edit", urls.Repl{"note": title}) + urlNew := myurls.Reverse("new", urls.Repl{}) urlDelete := myurls.Reverse("delete", urls.Repl{"note": title}) if err != nil { http.Redirect(w, r, urlEdit, http.StatusFound) return } - context := templ.Ctx{"note": note, "urlEdit": urlEdit, "urlDelete": urlDelete} + context := templ.Ctx{"note": note, "urlEdit": urlEdit, "urlDelete": urlDelete, "urlNew": urlNew} note.Render() err = templ.RenderTemplate(w, r, "view.tmpl.html", context) if err != nil { @@ -62,10 +58,10 @@ func view(w http.ResponseWriter, r *http.Request) { } func edit(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(middleware.ContextKey("user")).(string) + // user := r.Context().Value(middleware.ContextKey("user")).(string) encodedTitle := r.PathValue("note") - note, err := notes.LoadNote(user, encodedTitle) + note, err := notes.LoadNote(encodedTitle) if err != nil { title := notes.DecodeTitle(encodedTitle) note = ¬es.Note{Title: title} @@ -94,10 +90,10 @@ func new(w http.ResponseWriter, r *http.Request) { } func delete(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(middleware.ContextKey("user")).(string) + // user := r.Context().Value(middleware.ContextKey("user")).(string) encodedTitle := r.PathValue("note") - err := notes.DeleteNote(user, encodedTitle) + err := notes.DeleteNote(encodedTitle) if err != nil { log.Print(err.Error()) http.Error(w, "Couldn't delete note", http.StatusInternalServerError) @@ -109,24 +105,24 @@ func delete(w http.ResponseWriter, r *http.Request) { } func save(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(middleware.ContextKey("user")).(string) + // user := r.Context().Value(middleware.ContextKey("user")).(string) oldTitle := r.PathValue("note") title := r.FormValue("title") body := r.FormValue("body") - note := ¬es.Note{Title: title, Body: []byte(body), Owner: user} + note := ¬es.Note{Title: title, Body: []byte(body)} note.Save() if oldTitle != note.EncodedTitle() { - notes.DeleteNote(user, oldTitle) + notes.DeleteNote(oldTitle) } http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()}), http.StatusFound) } func togglebox(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(middleware.ContextKey("user")).(string) + // user := r.Context().Value(middleware.ContextKey("user")).(string) title := r.PathValue("note") nthBox, err := strconv.Atoi(r.FormValue("box")) @@ -135,7 +131,7 @@ func togglebox(w http.ResponseWriter, r *http.Request) { return } - note, err := notes.LoadNote(user, title) + note, err := notes.LoadNote(title) if err != nil { http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": title}), http.StatusFound) return @@ -154,39 +150,22 @@ type titleAndURL struct { func list(w http.ResponseWriter, r *http.Request) { user := r.Context().Value(middleware.ContextKey("user")).(string) - notesDir := path.Join(conf.Conf.NotesDir, user) - - files, err := os.ReadDir(notesDir) - if err != nil { - if os.IsNotExist(err) { - os.MkdirAll(notesDir, os.FileMode(0750)) - } else { - log.Print(err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - } - titlesAndUrls := make([]titleAndURL, 0) - for _, f := range files { - if !f.IsDir() { - encodedTitle := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) - title := notes.DecodeTitle(encodedTitle) + ns := notes.Notes.Get(user) + log.Printf("Notes: %+v", notes.Notes) + log.Printf("Notes for %s: %+v", user, ns) - log.Printf("Found note %s (title '%s')", encodedTitle, title) - - titlesAndUrls = append( - titlesAndUrls, - titleAndURL{Title: title, URL: myurls.Reverse("view", urls.Repl{"note": encodedTitle})}, - ) - log.Print(titlesAndUrls) - } + for _, note := range ns { + titlesAndUrls = append( + titlesAndUrls, + titleAndURL{Title: note.Title, URL: myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()})}, + ) } urlNew := myurls.Reverse("new", urls.Repl{}) - err = templ.RenderTemplate(w, r, "list.tmpl.html", templ.Ctx{"notes": titlesAndUrls, "urlNew": urlNew}) + err := templ.RenderTemplate(w, r, "list.tmpl.html", templ.Ctx{"notes": titlesAndUrls, "urlNew": urlNew}) if err != nil { log.Print(err.Error()) http.Error(w, "Internal Server Error", http.StatusInternalServerError)