Commit 53885e61 authored by Janoš Guljaš's avatar Janoš Guljaš Committed by GitHub

support binary upload to api files endpoint (#306)

parent 77c1f634
...@@ -27,45 +27,40 @@ type bytesPostResponse struct { ...@@ -27,45 +27,40 @@ type bytesPostResponse struct {
// bytesUploadHandler handles upload of raw binary data of arbitrary length. // bytesUploadHandler handles upload of raw binary data of arbitrary length.
func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
responseObject, err := s.splitUpload(ctx, r.Body, r.ContentLength) address, err := s.splitUpload(ctx, r.Body, r.ContentLength)
if err != nil { if err != nil {
s.Logger.Debugf("bytes upload: %v", err) s.Logger.Debugf("bytes upload: %v", err)
var response jsonhttp.StatusResponse jsonhttp.InternalServerError(w, nil)
response.Message = "upload error" return
response.Code = http.StatusInternalServerError
jsonhttp.Respond(w, response.Code, response)
} else {
jsonhttp.OK(w, responseObject)
} }
jsonhttp.OK(w, bytesPostResponse{
Reference: address,
})
} }
func (s *server) splitUpload(ctx context.Context, r io.ReadCloser, l int64) (interface{}, error) { func (s *server) splitUpload(ctx context.Context, r io.Reader, l int64) (swarm.Address, error) {
chunkPipe := file.NewChunkPipe() chunkPipe := file.NewChunkPipe()
go func() { go func() {
buf := make([]byte, swarm.ChunkSize) buf := make([]byte, swarm.ChunkSize)
c, err := io.CopyBuffer(chunkPipe, r, buf) c, err := io.CopyBuffer(chunkPipe, r, buf)
if err != nil { if err != nil {
s.Logger.Debugf("split upload: io error %d: %v", c, err) s.Logger.Debugf("split upload: io error %d: %v", c, err)
s.Logger.Error("io error") s.Logger.Error("split upload: io error")
return return
} }
if c != l { if c != l {
s.Logger.Debugf("split upload: read count mismatch %d: %v", c, err) s.Logger.Debugf("split upload: read count mismatch %d: %v", c, err)
s.Logger.Error("read count mismatch") s.Logger.Error("split upload: read count mismatch")
return return
} }
err = chunkPipe.Close() err = chunkPipe.Close()
if err != nil { if err != nil {
s.Logger.Errorf("split upload: incomplete file write close %v", err) s.Logger.Debugf("split upload: incomplete file write close %v", err)
s.Logger.Error("incomplete file write close") s.Logger.Error("split upload: incomplete file write close")
} }
}() }()
sp := splitter.NewSimpleSplitter(s.Storer) sp := splitter.NewSimpleSplitter(s.Storer)
address, err := sp.Split(ctx, chunkPipe, l) return sp.Split(ctx, chunkPipe, l)
if err != nil {
return swarm.ZeroAddress, err
}
return bytesPostResponse{Reference: address}, nil
} }
// bytesGetHandler handles retrieval of raw binary data of arbitrary length. // bytesGetHandler handles retrieval of raw binary data of arbitrary length.
......
...@@ -6,4 +6,5 @@ package api ...@@ -6,4 +6,5 @@ package api
type ( type (
BytesPostResponse = bytesPostResponse BytesPostResponse = bytesPostResponse
FileUploadResponse = fileUploadResponse
) )
This diff is collapsed.
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"mime" "mime"
"net/http" "net/http"
"strings"
"testing" "testing"
"github.com/ethersphere/bee/pkg/api" "github.com/ethersphere/bee/pkg/api"
...@@ -21,29 +22,29 @@ import ( ...@@ -21,29 +22,29 @@ import (
"github.com/ethersphere/bee/pkg/tags" "github.com/ethersphere/bee/pkg/tags"
) )
func TestBzz(t *testing.T) { func TestFiles(t *testing.T) {
var ( var (
simpleResource = func() string { return "/files" } fileUploadResource = "/files"
addressResource = func(addr string) string { return "/files/" + addr } fileDownloadResource = func(addr string) string { return "/files/" + addr }
simpleData = []byte("this is a simple text") simpleData = []byte("this is a simple text")
mockStorer = mock.NewStorer()
client = newTestServer(t, testServerOptions{ client = newTestServer(t, testServerOptions{
Storer: mockStorer, Storer: mock.NewStorer(),
Tags: tags.NewTags(), Tags: tags.NewTags(),
Logger: logging.New(ioutil.Discard, 5), Logger: logging.New(ioutil.Discard, 5),
}) })
) )
t.Run("simple-upload", func(t *testing.T) {
jsonhttptest.ResponseDirectSendHeadersAndReceiveHeaders(t, client, http.MethodPost, simpleResource(), bytes.NewReader(simpleData), http.StatusBadRequest, jsonhttp.StatusResponse{ t.Run("invalid-content-type", func(t *testing.T) {
Message: "not a mutlipart/form-data", jsonhttptest.ResponseDirectSendHeadersAndReceiveHeaders(t, client, http.MethodPost, fileUploadResource, bytes.NewReader(simpleData), http.StatusBadRequest, jsonhttp.StatusResponse{
Message: "invalid content-type header",
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
}, nil) }, nil)
}) })
t.Run("simple-upload", func(t *testing.T) { t.Run("multipart-upload", func(t *testing.T) {
fileName := "simple_file.txt" fileName := "simple_file.txt"
rootHash := "295673cf7aa55d119dd6f82528c91d45b53dd63dc2e4ca4abf4ed8b3a0788085" rootHash := "295673cf7aa55d119dd6f82528c91d45b53dd63dc2e4ca4abf4ed8b3a0788085"
_ = jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, simpleResource(), fileName, simpleData, http.StatusOK, "", api.FileUploadResponse{ _ = jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, fileUploadResource, fileName, simpleData, http.StatusOK, "", api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(rootHash), Reference: swarm.MustParseHexAddress(rootHash),
}) })
}) })
...@@ -51,11 +52,35 @@ func TestBzz(t *testing.T) { ...@@ -51,11 +52,35 @@ func TestBzz(t *testing.T) {
t.Run("check-content-type-detection", func(t *testing.T) { t.Run("check-content-type-detection", func(t *testing.T) {
fileName := "my-pictures.jpeg" fileName := "my-pictures.jpeg"
rootHash := "f2e761160deda91c1fbfab065a5abf530b0766b3e102b51fbd626ba37c3bc581" rootHash := "f2e761160deda91c1fbfab065a5abf530b0766b3e102b51fbd626ba37c3bc581"
_ = jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, simpleResource(), fileName, simpleData, http.StatusOK, "image/jpeg; charset=utf-8", api.FileUploadResponse{
t.Run("binary", func(t *testing.T) {
headers := make(http.Header)
headers.Add("Content-Type", "image/jpeg; charset=utf-8")
_ = jsonhttptest.ResponseDirectSendHeadersAndReceiveHeaders(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, bytes.NewReader(simpleData), http.StatusOK, api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(rootHash),
}, headers)
rcvdHeader := jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, fileDownloadResource(rootHash), nil, http.StatusOK, simpleData, nil)
cd := rcvdHeader.Get("Content-Disposition")
_, params, err := mime.ParseMediaType(cd)
if err != nil {
t.Fatal(err)
}
if params["filename"] != fileName {
t.Fatal("Invalid file name detected")
}
if rcvdHeader.Get("Content-Type") != "image/jpeg; charset=utf-8" {
t.Fatal("Invalid content type detected")
}
})
t.Run("multipart", func(t *testing.T) {
_ = jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, fileUploadResource, fileName, simpleData, http.StatusOK, "image/jpeg; charset=utf-8", api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(rootHash), Reference: swarm.MustParseHexAddress(rootHash),
}) })
rcvdHeader := jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, addressResource(rootHash), nil, http.StatusOK, simpleData, nil) rcvdHeader := jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, fileDownloadResource(rootHash), nil, http.StatusOK, simpleData, nil)
cd := rcvdHeader.Get("Content-Disposition") cd := rcvdHeader.Get("Content-Disposition")
_, params, err := mime.ParseMediaType(cd) _, params, err := mime.ParseMediaType(cd)
if err != nil { if err != nil {
...@@ -68,6 +93,7 @@ func TestBzz(t *testing.T) { ...@@ -68,6 +93,7 @@ func TestBzz(t *testing.T) {
t.Fatal("Invalid content type detected") t.Fatal("Invalid content type detected")
} }
}) })
})
t.Run("upload-then-download-and-check-data", func(t *testing.T) { t.Run("upload-then-download-and-check-data", func(t *testing.T) {
fileName := "sample.html" fileName := "sample.html"
...@@ -83,7 +109,37 @@ func TestBzz(t *testing.T) { ...@@ -83,7 +109,37 @@ func TestBzz(t *testing.T) {
</body> </body>
</html>` </html>`
rcvdHeader := jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, simpleResource(), fileName, []byte(sampleHtml), http.StatusOK, "", api.FileUploadResponse{ t.Run("binary", func(t *testing.T) {
headers := make(http.Header)
headers.Add("Content-Type", "text/html; charset=utf-8")
rcvdHeader := jsonhttptest.ResponseDirectSendHeadersAndReceiveHeaders(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, strings.NewReader(sampleHtml), http.StatusOK, api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(rootHash),
}, headers)
if rcvdHeader.Get("ETag") != fmt.Sprintf("%q", rootHash) {
t.Fatal("Invalid ETags header received")
}
// try to fetch the same file and check the data
rcvdHeader = jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, fileDownloadResource(rootHash), nil, http.StatusOK, []byte(sampleHtml), nil)
// check the headers
cd := rcvdHeader.Get("Content-Disposition")
_, params, err := mime.ParseMediaType(cd)
if err != nil {
t.Fatal(err)
}
if params["filename"] != fileName {
t.Fatal("Invalid filename detected")
}
if rcvdHeader.Get("Content-Type") != "text/html; charset=utf-8" {
t.Fatal("Invalid content type detected")
}
})
t.Run("multipart", func(t *testing.T) {
rcvdHeader := jsonhttptest.ResponseDirectWithMultiPart(t, client, http.MethodPost, fileUploadResource, fileName, []byte(sampleHtml), http.StatusOK, "", api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(rootHash), Reference: swarm.MustParseHexAddress(rootHash),
}) })
...@@ -92,7 +148,7 @@ func TestBzz(t *testing.T) { ...@@ -92,7 +148,7 @@ func TestBzz(t *testing.T) {
} }
// try to fetch the same file and check the data // try to fetch the same file and check the data
rcvdHeader = jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, addressResource(rootHash), nil, http.StatusOK, []byte(sampleHtml), nil) rcvdHeader = jsonhttptest.ResponseDirectCheckBinaryResponse(t, client, http.MethodGet, fileDownloadResource(rootHash), nil, http.StatusOK, []byte(sampleHtml), nil)
// check the headers // check the headers
cd := rcvdHeader.Get("Content-Disposition") cd := rcvdHeader.Get("Content-Disposition")
...@@ -107,4 +163,6 @@ func TestBzz(t *testing.T) { ...@@ -107,4 +163,6 @@ func TestBzz(t *testing.T) {
t.Fatal("Invalid content type detected") t.Fatal("Invalid content type detected")
} }
}) })
})
} }
...@@ -35,18 +35,16 @@ func (s *server) setupRouting() { ...@@ -35,18 +35,16 @@ func (s *server) setupRouting() {
fmt.Fprintln(w, "User-agent: *\nDisallow: /") fmt.Fprintln(w, "User-agent: *\nDisallow: /")
}) })
handle(router, "/bytes", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.bytesUploadHandler),
})
handle(router, "/files", jsonhttp.MethodHandler{ handle(router, "/files", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.bzzFileUploadHandler), "POST": http.HandlerFunc(s.fileUploadHandler),
}) })
handle(router, "/files/{addr}", jsonhttp.MethodHandler{ handle(router, "/files/{addr}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.bzzFileDownloadHandler), "GET": http.HandlerFunc(s.fileDownloadHandler),
}) })
handle(router, "/bytes", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.bytesUploadHandler),
})
handle(router, "/bytes/{address}", jsonhttp.MethodHandler{ handle(router, "/bytes/{address}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.bytesGetHandler), "GET": http.HandlerFunc(s.bytesGetHandler),
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment