merge current master

This commit is contained in:
Umputun
2019-04-07 13:32:12 -05:00
81 changed files with 3334 additions and 834 deletions
+3 -3
View File
@@ -115,14 +115,14 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
return
}
if err := a.dataService.DeleteUser(claims.Audience, claims.User.ID); err != nil {
if err = a.dataService.DeleteUser(claims.Audience, claims.User.ID); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user", rest.ErrNoAccess)
return
}
if claims.User.Picture != "" && a.authenticator.AvatarProxy() != nil {
avatartStore := a.authenticator.AvatarProxy().Store
if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil {
avatarStore := a.authenticator.AvatarProxy().Store
if err = avatarStore.Remove(path.Base(claims.User.Picture)); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar", rest.ErrInternal)
return
}
+4 -2
View File
@@ -113,11 +113,13 @@ func TestAdmin_Title(t *testing.T) {
srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second})
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/post1" {
w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
_, err := w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
assert.NoError(t, err)
return
}
if r.URL.String() == "/post2" {
w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
_, err := w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
assert.NoError(t, err)
return
}
w.WriteHeader(404)
+26 -2
View File
@@ -30,6 +30,7 @@ import (
"github.com/umputun/remark/backend/app/rest"
"github.com/umputun/remark/backend/app/rest/proxy"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/image"
"github.com/umputun/remark/backend/app/store/service"
)
@@ -44,6 +45,7 @@ type Rest struct {
CommentFormatter *store.CommentFormatter
Migrator *Migrator
NotifyService *notify.Service
ImageService *image.Service
WebRoot string
RemarkURL string
@@ -80,6 +82,7 @@ func (s *Rest) Run(port int) {
s.lock.Lock()
s.httpServer = s.makeHTTPServer(port, s.routes())
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
s.lock.Unlock()
err := s.httpServer.ListenAndServe()
@@ -89,7 +92,10 @@ func (s *Rest) Run(port int) {
s.lock.Lock()
s.httpsServer = s.makeHTTPSServer(s.SSLConfig.Port, s.routes())
s.httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
s.httpServer = s.makeHTTPServer(port, s.httpToHTTPSRouter())
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
s.lock.Unlock()
go func() {
@@ -106,7 +112,11 @@ func (s *Rest) Run(port int) {
m := s.makeAutocertManager()
s.lock.Lock()
s.httpsServer = s.makeHTTPSAutocertServer(s.SSLConfig.Port, s.routes(), m)
s.httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
s.httpServer = s.makeHTTPServer(port, s.httpChallengeRouter(m))
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
s.lock.Unlock()
go func() {
@@ -219,6 +229,7 @@ func (s *Rest) routes() chi.Router {
ropen.Get("/config", s.configCtrl)
ropen.Post("/preview", s.previewCommentCtrl)
ropen.Get("/info", s.infoCtrl)
ropen.Get("/picture/{user}/{id}", s.loadPictureCtrl)
ropen.Mount("/rss", s.rssRoutes())
ropen.Mount("/img", s.ImageProxy.Routes())
@@ -251,14 +262,27 @@ func (s *Rest) routes() chi.Router {
rauth.Put("/comment/{id}", s.updateCommentCtrl)
rauth.Post("/comment", s.createCommentCtrl)
rauth.With(rejectAnonUser).Put("/vote/{id}", s.voteCtrl)
rauth.Post("/deleteme", s.deleteMeCtrl)
rauth.With(rejectAnonUser).Post("/deleteme", s.deleteMeCtrl)
})
rapi.Group(func(rauth chi.Router) {
lmt := 10.0
if s.UpdateLimiter > 0 {
lmt = s.UpdateLimiter
}
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(lmt, nil)))
rauth.Use(authMiddleware.Auth)
rauth.Use(logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"), logger.IPfn(ipFn)).Handler)
rauth.With(rejectAnonUser).Post("/picture", s.savePictureCtrl)
})
})
// respond to /robots.txt with the list of allowed paths
router.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil))).
Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/img", "/avatar"}
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config",
"/img", "/avatar", "/picture"}
for i := range allowed {
allowed[i] = "Allow: /api/v1" + allowed[i]
}
+59 -24
View File
@@ -130,14 +130,9 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
return
}
if err != nil {
code := rest.ErrCommentRejected
switch {
case strings.HasPrefix(err.Error(), "too late to edit"):
code = rest.ErrCommentEditExpired
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
code = rest.ErrCommentEditChanged
}
code := s.parseError(err, rest.ErrCommentRejected)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment", code)
return
}
@@ -178,17 +173,7 @@ func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) {
comment, err := s.DataService.Vote(locator, id, user.ID, vote)
if err != nil {
code := rest.ErrVoteRejected
switch {
case strings.Contains(err.Error(), "can not vote for his own comment"):
code = rest.ErrVoteSelf
case strings.Contains(err.Error(), "already voted for"):
code = rest.ErrVoteDbl
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
code = rest.ErrVoteMax
case strings.Contains(err.Error(), "minimal score reached for comment"):
code = rest.ErrVoteMinScore
}
code := s.parseError(err, rest.ErrVoteRejected)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment", code)
return
}
@@ -228,14 +213,14 @@ func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
// get comments in 100 in each paginated request
for i := 0; i < 100; i++ {
comments, err := s.DataService.User(siteID, user.ID, 100, i*100)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get user comments", rest.ErrInternal)
comments, errUser := s.DataService.User(siteID, user.ID, 100, i*100)
if errUser != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errUser, "can't get user comments", rest.ErrInternal)
return
}
b, err := json.Marshal(comments)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user comments", rest.ErrInternal)
b, errUser := json.Marshal(comments)
if errUser != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errUser, "can't marshal user comments", rest.ErrInternal)
return
}
@@ -285,6 +270,31 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
}
// POST /image - save image with form request
func (s *Rest) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
if err := r.ParseMultipartForm(5 * 1024 * 1024); err != nil { // 5M max memory, if bigger will make a file
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't parse multipart form", rest.ErrDecode)
return
}
file, header, err := r.FormFile("file")
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get image file from the request", rest.ErrInternal)
return
}
defer func() { _ = file.Close() }()
id, err := s.ImageService.Save(header.Filename, user.ID, file)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't save image", rest.ErrInternal)
return
}
render.JSON(w, r, R.JSON{"id": id})
}
func (s *Rest) isReadOnly(locator store.Locator) bool {
if s.ReadOnlyAge > 0 {
// check RO by age
@@ -294,3 +304,28 @@ func (s *Rest) isReadOnly(locator store.Locator) bool {
}
return s.DataService.IsReadOnly(locator) // ro manually
}
func (s *Rest) parseError(err error, defaultCode int) (code int) {
code = defaultCode
switch {
// voting errors
case strings.Contains(err.Error(), "can not vote for his own comment"):
code = rest.ErrVoteSelf
case strings.Contains(err.Error(), "already voted for"):
code = rest.ErrVoteDbl
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
code = rest.ErrVoteMax
case strings.Contains(err.Error(), "minimal score reached for comment"):
code = rest.ErrVoteMinScore
// edit errors
case strings.HasPrefix(err.Error(), "too late to edit"):
code = rest.ErrCommentEditExpired
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
code = rest.ErrCommentEditChanged
}
return code
}
+170
View File
@@ -1,20 +1,29 @@
package api
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/rest"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/image"
)
func TestRest_Create(t *testing.T) {
@@ -499,3 +508,164 @@ func TestRest_DeleteMe(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestRest_SavePictureCtrl(t *testing.T) {
ts, _, teardown := startupT(t)
defer teardown()
// save picture
savePic := func(name string) (id string) {
r := strings.NewReader("file content 123")
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("file", name)
require.NoError(t, err)
_, err = io.Copy(fileWriter, r)
require.NoError(t, err)
contentType := bodyWriter.FormDataContentType()
require.NoError(t, bodyWriter.Close())
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/picture", ts.URL), bodyBuf)
require.NoError(t, err)
req.Header.Add("Content-Type", contentType)
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.Nil(t, err)
m := map[string]string{}
err = json.Unmarshal(body, &m)
assert.NoError(t, err)
assert.True(t, m["id"] != "")
return m["id"]
}
id := savePic("picture.png")
resp, err := http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.Nil(t, err)
assert.Equal(t, "file content 123", string(body))
assert.Equal(t, "image/png", resp.Header.Get("Content-Type"))
id = savePic("picture.gif")
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "image/gif", resp.Header.Get("Content-Type"))
id = savePic("picture.jpg")
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "image/jpeg", resp.Header.Get("Content-Type"))
id = savePic("picture.blah")
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "image/*", resp.Header.Get("Content-Type"))
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/blah/pic.blah", ts.URL))
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
func TestRest_CreateWithPictures(t *testing.T) {
ts, svc, teardown := startupT(t)
defer func() {
teardown()
os.RemoveAll("/tmp/remark42")
}()
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
svc.ImageService = &image.Service{
Store: &image.FileSystem{
Staging: "/tmp/remark42/images.staging",
Location: "/tmp/remark42/images",
MaxSize: 1000,
},
TTL: time.Millisecond * 100,
}
svc.DataService.EditDuration = time.Millisecond * 100
svc.DataService.ImageService = svc.ImageService
uploadPicture := func(file, content string) (id string) {
r := strings.NewReader(content)
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("file", file)
require.NoError(t, err)
_, err = io.Copy(fileWriter, r)
require.NoError(t, err)
contentType := bodyWriter.FormDataContentType()
require.NoError(t, bodyWriter.Close())
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/picture", ts.URL), bodyBuf)
require.NoError(t, err)
req.Header.Add("Content-Type", contentType)
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.Nil(t, err)
m := map[string]string{}
err = json.Unmarshal(body, &m)
assert.NoError(t, err)
assert.Contains(t, m["id"], ".png")
return m["id"]
}
id1 := uploadPicture("pic1.png", "file content 123")
id2 := uploadPicture("pic2.png", "file content 12345")
id3 := uploadPicture("pic3.png", "file content xyz12365789")
text := fmt.Sprintf(`text 123 ![](/api/v1/picture/%s) *xxx* ![](/api/v1/picture/%s) ![](/api/v1/picture/%s)`, id1, id2, id3)
body := fmt.Sprintf(`{"text": "%s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text)
resp, err := post(t, ts.URL+"/api/v1/comment", body)
assert.Nil(t, err)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
_, err = os.Stat("/tmp/remark42/images/" + id1)
assert.NotNil(t, err, "not moved from staging yet")
time.Sleep(300 * time.Millisecond)
_, err = os.Stat("/tmp/remark42/images/" + id1)
assert.NoError(t, err, "moved from staging")
_, err = os.Stat("/tmp/remark42/images/" + id2)
assert.NoError(t, err, "moved from staging")
_, err = os.Stat("/tmp/remark42/images/" + id3)
assert.NoError(t, err, "moved from staging")
}
func TestRest_parseError(t *testing.T) {
tbl := []struct {
err error
res int
}{
{errors.New("can not vote for his own comment"), rest.ErrVoteSelf},
{errors.New("already voted for"), rest.ErrVoteDbl},
{errors.New("maximum number of votes exceeded for comment"), rest.ErrVoteMax},
{errors.New("minimal score reached for comment"), rest.ErrVoteMinScore},
{errors.New("too late to edit"), rest.ErrCommentEditExpired},
{errors.New("parent comment with reply can't be edited"), rest.ErrCommentEditChanged},
{errors.New("blah blah"), rest.ErrInternal},
}
svc := Rest{}
for n, tt := range tbl {
t.Run(strconv.Itoa(n), func(t *testing.T) {
res := svc.parseError(tt.err, rest.ErrInternal)
assert.Equal(t, tt.res, res)
})
}
}
+48 -7
View File
@@ -1,8 +1,9 @@
package api
import (
"crypto/sha1" //nolint
"crypto/sha1" // nolint
"encoding/base64"
"io"
"net/http"
"strconv"
"strings"
@@ -274,12 +275,9 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
// key could be long for multiple posts, make it sha1
k := URLKey(r) + strings.Join(posts, ",")
hasher := sha1.New() //nolint
if _, err := hasher.Write([]byte(k)); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make sha1 for list of urls", rest.ErrInternal)
return
}
sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
h := sha1.Sum([]byte(k)) //nolint
sha := base64.URLEncoding.EncodeToString(h[:])
key := cache.NewKey(siteID).ID(sha).Scopes(siteID)
data, err := s.Cache.Get(key, func() ([]byte, error) {
counts, e := s.DataService.Counts(siteID, posts)
@@ -330,3 +328,46 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[WARN] can't render posts lits for site %s", siteID)
}
}
// GET /picture/{user}/{id} - get picture
func (s *Rest) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
imgContentType := func(img string) string {
img = strings.ToLower(img)
switch {
case strings.HasSuffix(img, ".png"):
return "image/png"
case strings.HasSuffix(img, ".jpg") || strings.HasSuffix(img, ".jpeg"):
return "image/jpeg"
case strings.HasSuffix(img, ".gif"):
return "image/gif"
}
return "image/*"
}
id := chi.URLParam(r, "user") + "/" + chi.URLParam(r, "id")
imgRdr, size, err := s.ImageService.Load(id)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get image "+id, rest.ErrAssetNotFound)
return
}
// enforce client-side caching
etag := `"` + id + `"`
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "max-age=604800") // 7 days
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
defer imgRdr.Close()
w.Header().Set("Content-Type", imgContentType(id))
w.Header().Set("Content-Length", strconv.Itoa(int(size)))
w.WriteHeader(http.StatusOK)
if _, err = io.Copy(w, imgRdr); err != nil {
log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err)
}
}
+32 -1
View File
@@ -377,6 +377,37 @@ func TestRest_List(t *testing.T) {
assert.Equal(t, 3, pi[1].Count)
}
func TestRest_ListWithSkipAndLimit(t *testing.T) {
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
c3 := store.Comment{Text: "test test #3", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah3"}}
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c2, ts)
addComment(t, c2, ts)
addComment(t, c3, ts)
addComment(t, c3, ts)
body, code := get(t, ts.URL+"/api/v1/list?site=radio-t&skip=1&limit=2")
assert.Equal(t, 200, code)
pi := []store.PostInfo{}
err := json.Unmarshal([]byte(body), &pi)
assert.Nil(t, err)
require.Equal(t, 2, len(pi))
assert.Equal(t, "https://radio-t.com/blah2", pi[0].URL)
assert.Equal(t, 2, pi[0].Count)
assert.Equal(t, "https://radio-t.com/blah1", pi[1].URL)
assert.Equal(t, 3, pi[1].Count)
}
func TestRest_Config(t *testing.T) {
ts, _, teardown := startupT(t)
defer teardown()
@@ -442,5 +473,5 @@ func TestRest_Robots(t *testing.T) {
assert.Equal(t, 200, code)
assert.Equal(t, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\nAllow: /api/v1/find\n"+
"Allow: /api/v1/last\nAllow: /api/v1/id\nAllow: /api/v1/count\nAllow: /api/v1/counts\n"+
"Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\n", string(body))
"Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\nAllow: /api/v1/picture\n", string(body))
}
+11 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/umputun/remark/backend/app/store"
adminstore "github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
"github.com/umputun/remark/backend/app/store/image"
"github.com/umputun/remark/backend/app/store/service"
)
@@ -203,6 +204,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
os.Remove(testDb)
os.Remove(testHTML)
os.RemoveAll("/tmp/ava-remark42")
os.RemoveAll("/tmp/pics-remark42")
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
@@ -232,7 +234,14 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
Cache: memCache,
WebRoot: "/tmp",
RemarkURL: "https://demo.remark42.com",
ImageService: &image.Service{
Store: &image.FileSystem{
Location: "/tmp/pics-remark42",
Partitions: 100,
MaxSize: 10000,
},
TTL: time.Millisecond * 100,
},
ImageProxy: &proxy.Image{},
ReadOnlyAge: 10,
CommentFormatter: store.NewCommentFormatter(&proxy.Image{}),
@@ -258,6 +267,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
os.Remove(testDb)
os.Remove(testHTML)
os.RemoveAll("/tmp/ava-remark42")
os.RemoveAll("/tmp/pics-remark42")
}
return ts, srv, teardown
+3 -3
View File
@@ -57,7 +57,7 @@ func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(data); err != nil {
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
@@ -89,7 +89,7 @@ func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(data); err != nil {
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
@@ -141,7 +141,7 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(data); err != nil {
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}