add rest
This commit is contained in:
+9
-5
@@ -7,16 +7,14 @@ import (
|
||||
|
||||
"github.com/hashicorp/logutils"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/umputun/remark/app/store"
|
||||
|
||||
"github.com/umputun/remark/app/rest"
|
||||
)
|
||||
|
||||
var opts struct {
|
||||
Mongo []string `short:"m" long:"mongo" env:"MONGO" default:"mongo" description:"mongo host:port" env-delim:","`
|
||||
MongoPasswd string `short:"p" long:"mongo-password" env:"MONGO_PASSWD" default:"" description:"mongo password"`
|
||||
MongoDelay int `long:"mongo-delay" env:"MONGO_DELAY" default:"0" description:"mongo initial delay"`
|
||||
|
||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
DBFile string `long:"db" env:"BOLTDB_FILE" default:"/tmp/remark.db" description:"bolt file name"`
|
||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
}
|
||||
|
||||
var revision = "unknown"
|
||||
@@ -29,8 +27,14 @@ func main() {
|
||||
setupLog(opts.Dbg)
|
||||
log.Print("[INFO] started remark")
|
||||
|
||||
dataStore, err := store.NewBoltDB(opts.DBFile)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] can't initialize data store, %+v", err)
|
||||
}
|
||||
|
||||
srv := rest.Server{
|
||||
Version: revision,
|
||||
Store: dataStore,
|
||||
}
|
||||
srv.Run()
|
||||
}
|
||||
|
||||
+117
-10
@@ -3,16 +3,20 @@ package rest
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/umputun/remark/app/store"
|
||||
)
|
||||
|
||||
// Server is a rest access server
|
||||
type Server struct {
|
||||
Version string
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
// Run the lister and request's router, activate rest server
|
||||
@@ -23,19 +27,122 @@ func (s *Server) Run() {
|
||||
router.Use(middleware.Throttle(100), middleware.Timeout(60*time.Second))
|
||||
router.Use(Limiter(10), AppInfo("remark", s.Version), Ping)
|
||||
|
||||
router.Route("/blah", func(r chi.Router) {
|
||||
r.Get("/{id}", s.getBlahCtrl)
|
||||
})
|
||||
|
||||
router.Post("/comment", s.createCommentCtrl)
|
||||
router.Delete("/comment/{id}", s.deleteCommentCtrl)
|
||||
router.Get("/find", s.getUrlComments)
|
||||
router.Get("/last/{max}", s.getLastComments)
|
||||
router.Get("/id/{id}", s.getByID)
|
||||
log.Fatal(http.ListenAndServe(":8080", router))
|
||||
}
|
||||
|
||||
// GET /blah/:id?foo=bar
|
||||
func (s *Server) getBlahCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
foo := r.URL.Query().Get("foo")
|
||||
log.Printf("[INFO] request for id=%s, foo=%s", id, foo)
|
||||
// POST /comment
|
||||
func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
comment := store.Comment{}
|
||||
if err := render.DecodeJSON(r.Body, &comment); err != nil {
|
||||
log.Printf("[WARN] can't bind request %s", comment)
|
||||
httpError(w, r, http.StatusBadRequest, err, "can't bind comment")
|
||||
return
|
||||
}
|
||||
comment.User.IP = strings.Split(r.RemoteAddr, ":")[0]
|
||||
|
||||
log.Printf("[INFO] create comment %+v", comment)
|
||||
|
||||
id, err := s.Store.Create(comment)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't save comment, %s", err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't save comment")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusAccepted)
|
||||
render.JSON(w, r, JSON{"data": "something"})
|
||||
render.JSON(w, r, JSON{"id": id, "url": comment.Locator.URL})
|
||||
}
|
||||
|
||||
// DELETE /comment/{id}?url=post-url
|
||||
func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] bad id %s", chi.URLParam(r, "id"))
|
||||
httpError(w, r, http.StatusBadRequest, err, "can't parse id")
|
||||
}
|
||||
|
||||
log.Printf("[INFO] delete comment %d", id)
|
||||
|
||||
url := r.URL.Query().Get("url")
|
||||
err = s.Store.Delete(url, id)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't delete comment, %s", err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, JSON{"id": id, "url": url})
|
||||
}
|
||||
|
||||
// GET /find?url=post-url
|
||||
func (s *Server) getUrlComments(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
log.Printf("[INFO] get comments for %s", url)
|
||||
|
||||
comments, err := s.Store.Find(store.Request{Locator: store.Locator{URL: url}})
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get comments for %s, %s", url, err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't load comments comment")
|
||||
return
|
||||
}
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, comments)
|
||||
}
|
||||
|
||||
// GET /last/{max}
|
||||
func (s *Server) getLastComments(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
max, err := strconv.Atoi(chi.URLParam(r, "max"))
|
||||
if err != nil {
|
||||
max = 0
|
||||
}
|
||||
|
||||
url := r.URL.Query().Get("url")
|
||||
log.Printf("[INFO] get comments for %s", url)
|
||||
|
||||
comments, err := s.Store.Last(store.Locator{}, max)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get last comments, %s", err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't get last comments")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, comments)
|
||||
}
|
||||
|
||||
// GET /id/{id}?url=post-url
|
||||
func (s *Server) getByID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] bad id %s", chi.URLParam(r, "id"))
|
||||
httpError(w, r, http.StatusBadRequest, err, "can't parse id")
|
||||
}
|
||||
url := r.URL.Query().Get("url")
|
||||
|
||||
log.Printf("[INFO] get comments by id %d, %s", id, url)
|
||||
|
||||
comment, err := s.Store.Get(store.Locator{URL: url}, id)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get comment, %s", err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't get comment by id")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, comment)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, r *http.Request, code int, err error, details string) {
|
||||
render.Status(r, code)
|
||||
render.JSON(w, r, JSON{"error": err.Error(), "details": details})
|
||||
}
|
||||
|
||||
+55
-11
@@ -35,11 +35,12 @@ func NewBoltDB(dbFile string) (*BoltDB, error) {
|
||||
}
|
||||
|
||||
// Create saves new comment to store
|
||||
func (b *BoltDB) Create(comment Comment) error {
|
||||
func (b *BoltDB) Create(comment Comment) (int64, error) {
|
||||
|
||||
comment.ID = time.Now().UnixNano()
|
||||
comment.Timestamp = time.Now()
|
||||
|
||||
return b.Update(func(tx *bolt.Tx) error {
|
||||
err := b.Update(func(tx *bolt.Tx) error {
|
||||
bucket, e := tx.CreateBucketIfNotExists([]byte(comment.Locator.URL))
|
||||
if e != nil {
|
||||
return errors.Wrapf(e, "can't make bucket", comment.Locator.URL)
|
||||
@@ -75,6 +76,8 @@ func (b *BoltDB) Create(comment Comment) error {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return comment.ID, err
|
||||
}
|
||||
|
||||
// Delete removed comment by url and id from the store
|
||||
@@ -85,7 +88,7 @@ func (b *BoltDB) Delete(url string, id int64) error {
|
||||
if bucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", url)
|
||||
}
|
||||
key := []byte(fmt.Sprintf("%12d", id))
|
||||
key := b.keyFromValue(id)
|
||||
if err := bucket.Delete(key); err != nil {
|
||||
return errors.Wrapf(err, "can't delete key %s from bucket %s", key, url)
|
||||
}
|
||||
@@ -116,26 +119,64 @@ func (b *BoltDB) Find(request Request) ([]Comment, error) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Get comment by id
|
||||
func (b *BoltDB) Get(locator Locator, id int64) (comment Comment, err error) {
|
||||
err = b.View(func(tx *bolt.Tx) error {
|
||||
|
||||
lastBucket := tx.Bucket([]byte(lastBucketName))
|
||||
if lastBucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", lastBucketName)
|
||||
}
|
||||
|
||||
c := lastBucket.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
url, foundID, e := refFromValue(v).parse()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if foundID == id && url == locator.URL {
|
||||
urlBucket := tx.Bucket([]byte(url))
|
||||
if urlBucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", url)
|
||||
}
|
||||
commentVal := urlBucket.Get(b.keyFromValue(id))
|
||||
if commentVal == nil {
|
||||
return errors.Errorf("no comment for %d in store %s", id, url)
|
||||
}
|
||||
|
||||
if e := json.Unmarshal(commentVal, &comment); e != nil {
|
||||
return errors.Wrap(e, "failed to unmarshal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.Errorf("no id %d in store %s", id, locator.URL)
|
||||
})
|
||||
|
||||
return comment, err
|
||||
}
|
||||
|
||||
// Last returns up to max last comments for given locator
|
||||
func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) {
|
||||
|
||||
err = b.View(func(tx *bolt.Tx) error {
|
||||
lastBk := tx.Bucket([]byte(lastBucketName))
|
||||
if lastBk == nil {
|
||||
lastBucket := tx.Bucket([]byte(lastBucketName))
|
||||
if lastBucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", lastBucketName)
|
||||
}
|
||||
|
||||
c := lastBk.Cursor()
|
||||
c := lastBucket.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
url, id, e := refFromValue(v).parse()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
urlBk := tx.Bucket([]byte(url))
|
||||
if urlBk == nil {
|
||||
urlBucket := tx.Bucket([]byte(url))
|
||||
if urlBucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", url)
|
||||
}
|
||||
commentVal := urlBk.Get(b.keyFromValue(id))
|
||||
commentVal := urlBucket.Get(b.keyFromValue(id))
|
||||
if commentVal == nil {
|
||||
return errors.Errorf("no comment for %d in store %s", id, url)
|
||||
}
|
||||
@@ -145,6 +186,9 @@ func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) {
|
||||
return errors.Wrap(e, "failed to unmarshal")
|
||||
}
|
||||
result = append(result, comment)
|
||||
if max > 0 && len(result) >= max {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -153,11 +197,11 @@ func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) {
|
||||
}
|
||||
|
||||
func (b *BoltDB) keyFromComment(comment Comment) []byte {
|
||||
return []byte(fmt.Sprintf("%12d", comment.ID))
|
||||
return []byte(fmt.Sprintf("%22d", comment.ID))
|
||||
}
|
||||
|
||||
func (b *BoltDB) keyFromValue(id int64) []byte {
|
||||
return []byte(fmt.Sprintf("%12d", id))
|
||||
return []byte(fmt.Sprintf("%22d", id))
|
||||
}
|
||||
|
||||
// buckets returns list of buckets, which is list of all commented posts
|
||||
|
||||
+53
-26
@@ -11,43 +11,21 @@ import (
|
||||
var testDb = "/tmp/test-remark.db"
|
||||
|
||||
func TestBoltDB_CreateAndFind(t *testing.T) {
|
||||
var b Interface
|
||||
defer os.Remove(testDb)
|
||||
|
||||
b, err := NewBoltDB(testDb)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment := Comment{Text: "some text", Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment = Comment{Text: "some text2", Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
b = prep(t)
|
||||
|
||||
res, err := b.Find(Request{Locator: Locator{URL: "https://radio-t.com"}})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "some text", res[0].Text)
|
||||
assert.Equal(t, "user1", res[0].User.ID)
|
||||
t.Log(res[0].ID)
|
||||
}
|
||||
|
||||
func TestBoltDB_Delete(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
|
||||
b, err := NewBoltDB(testDb)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment := Comment{Text: "some text", Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment = Comment{Text: "some text2", Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
b := prep(t)
|
||||
|
||||
res, err := b.Find(Request{Locator: Locator{URL: "https://radio-t.com"}})
|
||||
assert.Nil(t, err)
|
||||
@@ -61,3 +39,52 @@ func TestBoltDB_Delete(t *testing.T) {
|
||||
assert.Equal(t, 1, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
}
|
||||
|
||||
func TestBoltDB_Get(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
b := prep(t)
|
||||
|
||||
res, err := b.Find(Request{Locator: Locator{URL: "https://radio-t.com"}})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
|
||||
comment, err := b.Get(Locator{URL: "https://radio-t.com"}, res[1].ID)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "some text2", comment.Text)
|
||||
|
||||
comment, err = b.Get(Locator{URL: "https://radio-t.com"}, 1234567)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBoltDB_Last(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
b := prep(t)
|
||||
|
||||
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
|
||||
res, err = b.Last(Locator{URL: "https://radio-t.com"}, 1)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prep(t *testing.T) *BoltDB {
|
||||
b, err := NewBoltDB(testDb)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment := Comment{Text: "some text", Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
_, err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment = Comment{Text: "some text2", Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
|
||||
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}}
|
||||
_, err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
+4
-3
@@ -39,8 +39,9 @@ type Request struct {
|
||||
|
||||
// Interface defines basic CRUD for comments
|
||||
type Interface interface {
|
||||
Create(comment Comment) error
|
||||
Delete(id string) error
|
||||
Create(comment Comment) (int64, error)
|
||||
Delete(url string, id int64) error
|
||||
Find(request Request) ([]Comment, error)
|
||||
Last(locator Locator, max int) []Comment
|
||||
Last(locator Locator, max int) ([]Comment, error)
|
||||
Get(locator Locator, id int64) (Comment, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user