Browse Source

note creation added

master
Christian Müller 8 years ago
parent
commit
2d0910b72d
  1. 2
      assets/templates/note.html
  2. 92
      server.go
  3. 128
      storage.go

2
assets/templates/note.html

@ -13,9 +13,11 @@
</article> </article>
<footer> <footer>
<a href="/">&#8962; notehub</a> &middot; <a href="/">&#8962; notehub</a> &middot;
{{if .ID}}
<a href="/{{.ID}}/stats">statistics</a> &middot; <a href="/{{.ID}}/stats">statistics</a> &middot;
<a href="/{{.ID}}/edit">edit</a> &middot; <a href="/{{.ID}}/edit">edit</a> &middot;
<a href="/{{.ID}}/export">export</a> &middot; <a href="/{{.ID}}/export">export</a> &middot;
{{end}}
<a href="/TOS.md">terms of service</a> <a href="/TOS.md">terms of service</a>
</footer> </footer>
</body> </body>

92
server.go

@ -2,21 +2,17 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"html/template" "html/template"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"regexp" "net/url"
"strings"
"time"
"database/sql" "database/sql"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/russross/blackfriday"
) )
type Template struct{ templates *template.Template } type Template struct{ templates *template.Template }
@ -47,15 +43,15 @@ func main() {
return c.Render(code, "Page", n) return c.Render(code, "Page", n)
}) })
e.GET("/:id", func(c echo.Context) error { e.GET("/:id", func(c echo.Context) error {
n, code := note(c, db) n, code := load(c, db)
return c.Render(code, "Note", n) return c.Render(code, "Note", n)
}) })
e.GET("/:id/export", func(c echo.Context) error { 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) return c.String(code, n.Text)
}) })
e.GET("/:id/stats", func(c echo.Context) error { e.GET("/:id/stats", func(c echo.Context) error {
n, code := note(c, db) n, code := load(c, db)
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
e.Renderer.Render(buf, "Stats", n, c) e.Renderer.Render(buf, "Stats", n, c)
n.Content = template.HTML(buf.String()) n.Content = template.HTML(buf.String())
@ -67,59 +63,40 @@ func main() {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("DEBUG %+v", vals) if get(vals, "tos") != "on" {
return nil code := http.StatusPreconditionFailed
}) return c.Render(code, "Note", errPage(code))
e.Logger.Fatal(e.Start(":3000"))
} }
text := get(vals, "text")
type Note struct { if 10 > len(text) || len(text) > 50000 {
ID, Title, Text string code := http.StatusBadRequest
Published, Edited time.Time return c.Render(code, "Note",
Views int errPage(code, "note length not accepted"))
Content template.HTML
} }
n := Note{
func (n Note) withTitle() Note { Text: text,
fstLine := rexpNewLine.Split(n.Text, -1)[0] Password: get(vals, "password"),
maxLength := 25
if len(fstLine) < 25 {
maxLength = len(fstLine)
} }
n.Title = strings.TrimSpace(rexpNonAlphaNum.ReplaceAllString(fstLine[:maxLength], "")) id, err := save(c, db, &n)
return 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)
})
var ( e.Logger.Fatal(e.Start(":3000"))
rexpNewLine = regexp.MustCompile("[\n\r]") }
rexpNonAlphaNum = regexp.MustCompile("[`~!@#$%^&*_|+=?;:'\",.<>{}\\/]")
)
func note(c echo.Context, db *sql.DB) (Note, int) { func get(vals url.Values, key string) string {
stmt, err := db.Prepare("select id, text, strftime('%s', published) as published," + if list, found := vals[key]; found {
" strftime('%s',edited) as edited, password, views from notes where id = ?") if len(list) == 1 {
if err != nil { return list[0]
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
} }
return Note{ return ""
ID: id,
Text: text,
Views: views,
Published: time.Unix(published, 0),
Edited: time.Unix(edited, 0),
Content: mdTmplHTML([]byte(text)),
}.withTitle(), http.StatusOK
} }
func md2html(c echo.Context, name string) (Note, int) { 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) mdContent, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
c.Logger().Errorf("couldn't open markdown page %q: %v", path, err) 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 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"))}

128
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)))
}
Loading…
Cancel
Save