commit
c9508770a9
@ -0,0 +1,314 @@ |
||||
package comments |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/subtle" |
||||
"encoding/json" |
||||
"fmt" |
||||
"html/template" |
||||
"io" |
||||
"net/http" |
||||
"strconv" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/peterbourgon/diskv" |
||||
) |
||||
|
||||
type Comment struct { |
||||
Name, Text string |
||||
IP string |
||||
Date time.Time |
||||
Spam bool |
||||
} |
||||
|
||||
func MarshalJSON(c Comment) ([]byte, error) { |
||||
buf := new(bytes.Buffer) |
||||
err := json.NewEncoder(buf).Encode(c) |
||||
return buf.Bytes(), err |
||||
} |
||||
|
||||
func UnmarshalJSON(b []byte) (Comment, error) { |
||||
var c Comment |
||||
err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&c) |
||||
return c, err |
||||
} |
||||
|
||||
func Default() *Service { |
||||
return WithStorage(NewDiskv("comments")) |
||||
} |
||||
|
||||
func WithStorage(storage Storage) *Service { |
||||
return &Service{Storage: storage} |
||||
} |
||||
|
||||
type Service struct { |
||||
Storage Storage |
||||
Password string |
||||
} |
||||
|
||||
type Storage interface { |
||||
Store(post string, c Comment) error |
||||
Retreive(post string) ([]Comment, error) |
||||
ReadAll(post string) (io.Reader, error) |
||||
} |
||||
|
||||
func (cs *Service) User(r *http.Request) (string, bool) { |
||||
if cs.Password == "" { |
||||
return "", true |
||||
} |
||||
|
||||
user, providedPass, ok := r.BasicAuth() |
||||
if ok && subtle.ConstantTimeCompare([]byte(cs.Password), []byte(providedPass)) == 1 { |
||||
return user, true |
||||
} |
||||
|
||||
return "", false |
||||
} |
||||
|
||||
func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Request) error { |
||||
user, allowed := cs.User(r) |
||||
if !allowed { |
||||
fmt.Fprintf(w, "<a href='/login'>click here to enter your name and the comments password</a>") |
||||
w.WriteHeader(401) |
||||
return nil |
||||
} |
||||
|
||||
if r.Method == "POST" { |
||||
return cs.Comment(post, Comment{ |
||||
Name: user, |
||||
Text: readCommentText(r), |
||||
IP: r.RemoteAddr, |
||||
Date: time.Now(), |
||||
}) |
||||
} |
||||
|
||||
w.Header().Set("Content-Type", "text/html") |
||||
_, err := cs.WriteHTML(post, w) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func readCommentText(r *http.Request) string { |
||||
comment := struct{ Text string }{} |
||||
json.NewDecoder(r.Body).Decode(&comment) |
||||
return comment.Text |
||||
} |
||||
|
||||
func (cs *Service) Comment(post string, c Comment) error { |
||||
return cs.Storage.Store(post, c) |
||||
} |
||||
|
||||
func (cs *Service) Load(post string) ([]Comment, error) { |
||||
return cs.Storage.Retreive(post) |
||||
} |
||||
|
||||
func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) { |
||||
buf := &bytes.Buffer{} |
||||
tmpl := template.Must(template.New("").Parse( |
||||
`<style> |
||||
comment { |
||||
margin: 10px 0; |
||||
} |
||||
comment, comment name, comment text { |
||||
display: block; |
||||
} |
||||
comment name::after { |
||||
content: " says:"; |
||||
} |
||||
comment text { |
||||
margin-left: 5px; |
||||
padding-left: 5px; |
||||
border-left: 2px solid gray; |
||||
} |
||||
</style> |
||||
{{range .}}{{ if .Text }}<comment> |
||||
<name>{{.Name}}</name> |
||||
<text>{{.Text}}</text> |
||||
</comment> |
||||
{{end}}{{end}} |
||||
<form onsubmit="submitComment(event)"> |
||||
<p> |
||||
<div> |
||||
<label for="comment">post a comment:</label> |
||||
</div> |
||||
<div> |
||||
<textarea placeholder="comment..." name="comment"></textarea> |
||||
</div> |
||||
<div> |
||||
<button type="submit">post</button> |
||||
</div> |
||||
</p> |
||||
</form>`)) |
||||
|
||||
comments, err := cs.Load(post) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
err = tmpl.Execute(buf, comments) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
return io.Copy(w, buf) |
||||
} |
||||
|
||||
func (cs *Service) WriteJSON(post string, w io.Writer) (int64, error) { |
||||
r, err := cs.Storage.ReadAll(post) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
return io.Copy(w, r) |
||||
} |
||||
|
||||
type diskvStorage struct { |
||||
*diskv.Diskv |
||||
|
||||
keyMtx sync.Mutex |
||||
keyLastKnown int64 |
||||
} |
||||
|
||||
func (d *diskvStorage) nextKey(post string) (string, error) { |
||||
d.keyMtx.Lock() |
||||
defer d.keyMtx.Unlock() |
||||
|
||||
prefix := post + "-" |
||||
|
||||
lastKey := prefix |
||||
|
||||
keys := d.KeysPrefix(prefix, nil) |
||||
for key := range keys { |
||||
lastKey = key |
||||
} |
||||
|
||||
if lastKey == prefix { |
||||
return prefix + "1", nil |
||||
} |
||||
|
||||
minusPrefix := lastKey[len(prefix):] |
||||
lastIdx, err := strconv.Atoi(minusPrefix) |
||||
if err != nil { |
||||
return "", fmt.Errorf("comment key %q seems malformed", lastKey) |
||||
} |
||||
|
||||
key := prefix + fmt.Sprint(lastIdx+1) |
||||
err = d.WriteStream(key, &bytes.Buffer{}, true) |
||||
|
||||
return key, err |
||||
} |
||||
|
||||
func (d *diskvStorage) Store(post string, c Comment) error { |
||||
b, err := MarshalJSON(c) |
||||
if err != nil { |
||||
return fmt.Errorf("error marshaling comment for storage: %v", err) |
||||
} |
||||
|
||||
key, err := d.nextKey(post) |
||||
if err != nil { |
||||
return fmt.Errorf("error recording comment: %v", err) |
||||
} |
||||
|
||||
err = d.Write(key, b) |
||||
if err != nil { |
||||
return fmt.Errorf("error writing comment to storage: %v", err) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
type multiReadCloser struct { |
||||
rcs []io.ReadCloser |
||||
} |
||||
|
||||
func (mrc *multiReadCloser) Read(b []byte) (int, error) { |
||||
if len(mrc.rcs) == 0 { |
||||
return 0, io.EOF |
||||
} |
||||
|
||||
n, err := mrc.rcs[0].Read(b) |
||||
if n == len(b) { // we read enough
|
||||
return n, err |
||||
} |
||||
|
||||
for (err == nil || err == io.EOF) && len(mrc.rcs) > 1 { |
||||
var n2 int |
||||
closeErr := mrc.rcs[0].Close() |
||||
if closeErr != nil { |
||||
return n, closeErr |
||||
} |
||||
|
||||
mrc.rcs = mrc.rcs[1:] |
||||
n2, err = mrc.rcs[0].Read(b[n:]) |
||||
n += n2 |
||||
} |
||||
|
||||
return n, err |
||||
} |
||||
|
||||
func (mrc *multiReadCloser) Close() error { |
||||
var err error |
||||
|
||||
for _, rc := range mrc.rcs { |
||||
err2 := rc.Close() |
||||
if err2 != nil { |
||||
err = err2 |
||||
} |
||||
} |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (d *diskvStorage) ReadAll(post string) (io.Reader, error) { |
||||
prefix := post + "-" |
||||
|
||||
var rcs []io.ReadCloser |
||||
|
||||
keys := d.KeysPrefix(prefix, nil) |
||||
for key := range keys { |
||||
rc, err := d.ReadStream(key, false) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rcs = append(rcs, rc) |
||||
} |
||||
|
||||
return &multiReadCloser{rcs}, nil |
||||
} |
||||
|
||||
func (d *diskvStorage) Retreive(post string) ([]Comment, error) { |
||||
var comments []Comment |
||||
|
||||
r, err := d.ReadAll(post) |
||||
if err != nil { |
||||
return comments, fmt.Errorf("error reading comment from storage: %v", err) |
||||
} |
||||
|
||||
dec := json.NewDecoder(r) |
||||
for dec.More() { |
||||
comment := Comment{} |
||||
err := dec.Decode(&comment) |
||||
if err != nil { |
||||
return comments, fmt.Errorf("error unmarshaling comment: %v", err) |
||||
} |
||||
comments = append(comments, comment) |
||||
} |
||||
|
||||
return comments, nil |
||||
} |
||||
|
||||
func NewDiskv(path string) Storage { |
||||
return &diskvStorage{ |
||||
Diskv: diskv.New(diskv.Options{ |
||||
BasePath: path, |
||||
CacheSizeMax: 1024 * 1024, |
||||
}), |
||||
} |
||||
} |
Loading…
Reference in new issue