From 2d0910b72da9511a40c33e20f9d293262210c414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sun, 17 Sep 2017 13:39:35 +0200 Subject: [PATCH] note creation added --- assets/templates/note.html | 2 + server.go | 96 ++++++++++------------------ storage.go | 128 +++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 62 deletions(-) create mode 100644 storage.go diff --git a/assets/templates/note.html b/assets/templates/note.html index fdfca4d..fe7af7c 100644 --- a/assets/templates/note.html +++ b/assets/templates/note.html @@ -13,9 +13,11 @@ diff --git a/server.go b/server.go index f12b305..7a16f2f 100644 --- a/server.go +++ b/server.go @@ -2,21 +2,17 @@ package main import ( "bytes" - "fmt" "html/template" "io" "io/ioutil" "net/http" - "regexp" - "strings" - "time" + "net/url" "database/sql" _ "github.com/mattn/go-sqlite3" "github.com/labstack/echo" - "github.com/russross/blackfriday" ) type Template struct{ templates *template.Template } @@ -47,15 +43,15 @@ func main() { return c.Render(code, "Page", n) }) e.GET("/:id", func(c echo.Context) error { - n, code := note(c, db) + n, code := load(c, db) return c.Render(code, "Note", n) }) e.GET("/:id/export", func(c echo.Context) error { - n, code := note(c, db) + n, code := load(c, db) return c.String(code, n.Text) }) e.GET("/:id/stats", func(c echo.Context) error { - n, code := note(c, db) + n, code := load(c, db) buf := bytes.NewBuffer([]byte{}) e.Renderer.Render(buf, "Stats", n, c) n.Content = template.HTML(buf.String()) @@ -67,59 +63,40 @@ func main() { if err != nil { return err } - fmt.Printf("DEBUG %+v", vals) - return nil + if get(vals, "tos") != "on" { + code := http.StatusPreconditionFailed + return c.Render(code, "Note", errPage(code)) + } + text := get(vals, "text") + if 10 > len(text) || len(text) > 50000 { + code := http.StatusBadRequest + return c.Render(code, "Note", + errPage(code, "note length not accepted")) + } + n := Note{ + Text: text, + Password: get(vals, "password"), + } + id, err := save(c, db, &n) + if err != nil { + c.Logger().Error(err) + code := http.StatusServiceUnavailable + return c.Render(code, "Note", errPage(code)) + } + c.Logger().Infof("new note %q created", n.ID) + return c.Redirect(http.StatusMovedPermanently, "/"+id) }) e.Logger.Fatal(e.Start(":3000")) } -type Note struct { - ID, Title, Text string - Published, Edited time.Time - Views int - Content template.HTML -} - -func (n Note) withTitle() Note { - fstLine := rexpNewLine.Split(n.Text, -1)[0] - maxLength := 25 - if len(fstLine) < 25 { - maxLength = len(fstLine) - } - n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], "")) - return n -} - -var ( - rexpNewLine = regexp.MustCompile("[\n\r]") - rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]") -) - -func note(c echo.Context, db *sql.DB) (Note, int) { - stmt, err := db.Prepare("select id, text, strftime('%s', published) as published," + - " strftime('%s',edited) as edited, password, views from notes where id = ?") - if err != nil { - c.Logger().Error(err) - return note503, http.StatusServiceUnavailable - } - defer stmt.Close() - row := stmt.QueryRow(c.Param("id")) - var id, text, password string - var published, edited int64 - var views int - if err := row.Scan(&id, &text, &published, &edited, &password, &views); err != nil { - c.Logger().Error(err) - return note404, http.StatusNotFound +func get(vals url.Values, key string) string { + if list, found := vals[key]; found { + if len(list) == 1 { + return list[0] + } } - return Note{ - ID: id, - Text: text, - Views: views, - Published: time.Unix(published, 0), - Edited: time.Unix(edited, 0), - Content: mdTmplHTML([]byte(text)), - }.withTitle(), http.StatusOK + return "" } func md2html(c echo.Context, name string) (Note, int) { @@ -127,13 +104,8 @@ func md2html(c echo.Context, name string) (Note, int) { mdContent, err := ioutil.ReadFile(path) if err != nil { c.Logger().Errorf("couldn't open markdown page %q: %v", path, err) - return note503, http.StatusServiceUnavailable + code := http.StatusServiceUnavailable + return errPage(code), code } return Note{Title: name, Content: mdTmplHTML(mdContent)}, http.StatusOK } - -func mdTmplHTML(content []byte) template.HTML { return template.HTML(string(blackfriday.Run(content))) } - -// error notes -var note404 = Note{Title: "Not found", Content: mdTmplHTML([]byte("# 404 NOT FOUND"))} -var note503 = Note{Title: "Service unavailable", Content: mdTmplHTML([]byte("# 503 SERVICE UNAVAILABLE"))} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..a4bd624 --- /dev/null +++ b/storage.go @@ -0,0 +1,128 @@ +package main + +import ( + "bytes" + "database/sql" + "fmt" + "html/template" + "math/rand" + "net/http" + "regexp" + "strings" + "time" + + "github.com/labstack/echo" + "github.com/russross/blackfriday" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +const idLength = 5 + +var ( + errorCodes = map[int]string{ + 400: "Bad request", + 404: "Not found", + 412: "Precondition failed", + 503: "Service unavailable", + } + rexpNewLine = regexp.MustCompile("[\n\r]") + rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]") +) + +type Note struct { + ID, Title, Text, Password string + Published, Edited time.Time + Views int + Content template.HTML +} + +func errPage(code int, details ...string) Note { + text := errorCodes[code] + if len(details) > 0 { + text += ": " + strings.Join(details, ";") + } + return Note{ + Title: text, + Content: mdTmplHTML([]byte(fmt.Sprintf("# %d %s", code, text))), + } +} + +func (n *Note) render() { + fstLine := rexpNewLine.Split(n.Text, -1)[0] + maxLength := 25 + if len(fstLine) < 25 { + maxLength = len(fstLine) + } + n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], "")) + n.Password = "" + n.Content = mdTmplHTML([]byte(n.Text)) +} + +func save(c echo.Context, db *sql.DB, n *Note) (string, error) { + tx, err := db.Begin() + if err != nil { + return "", err + } + stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)") + defer stmt.Close() + id := randId() + _, err = stmt.Exec(id, n.Text, n.Password) + if err != nil { + if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + tx.Rollback() + c.Logger().Infof("collision on id %q", id) + return save(c, db, n) + } + return "", err + } + return id, tx.Commit() +} + +func randId() string { + buf := bytes.NewBuffer([]byte{}) + for i := 0; i < idLength; i++ { + b := '0' + z := rand.Intn(36) + if z > 9 { + b = 'a' + z -= 10 + } + buf.WriteRune(rune(z) + b) + } + return buf.String() +} + +func load(c echo.Context, db *sql.DB) (Note, int) { + stmt, _ := db.Prepare("select * from notes where id = ?") + defer stmt.Close() + row := stmt.QueryRow(c.Param("id")) + var id, text, password string + var published time.Time + var editedVal interface{} + var views int + if err := row.Scan(&id, &text, &published, &editedVal, &password, &views); err != nil { + c.Logger().Error(err) + code := http.StatusNotFound + return errPage(code), code + } + var edited time.Time + if editedVal != nil { + edited = editedVal.(time.Time) + } + n := &Note{ + ID: id, + Text: text, + Views: views, + Published: published, + Edited: edited, + } + n.render() + return *n, http.StatusOK +} + +func mdTmplHTML(content []byte) template.HTML { + return template.HTML(string(blackfriday.Run(content))) +}