diff --git a/go/constants/http.go b/go/constants/http.go index 9e37db51d9..427c4484b2 100644 --- a/go/constants/http.go +++ b/go/constants/http.go @@ -7,6 +7,7 @@ package constants const ( RootPath = "/root/" GetRefsPath = "/getRefs/" + GetBlobPath = "/getBlob/" HasRefsPath = "/hasRefs/" WriteValuePath = "/writeValue/" BasePath = "/" diff --git a/go/datas/database_server.go b/go/datas/database_server.go index deaa1cc892..7892cf84ea 100644 --- a/go/datas/database_server.go +++ b/go/datas/database_server.go @@ -61,6 +61,7 @@ func (s *RemoteDatabaseServer) Run() { router := httprouter.New() router.POST(constants.GetRefsPath, s.corsHandle(s.makeHandle(HandleGetRefs))) + router.GET(constants.GetBlobPath, s.corsHandle(s.makeHandle(HandleGetBlob))) router.OPTIONS(constants.GetRefsPath, s.corsHandle(noopHandle)) router.POST(constants.HasRefsPath, s.corsHandle(s.makeHandle(HandleHasRefs))) router.OPTIONS(constants.HasRefsPath, s.corsHandle(noopHandle)) diff --git a/go/datas/remote_database_handlers.go b/go/datas/remote_database_handlers.go index 16eefa4cfd..4586e479d8 100644 --- a/go/datas/remote_database_handlers.go +++ b/go/datas/remote_database_handlers.go @@ -44,34 +44,40 @@ var ( // ordered sequence of Chunks to be validated and stored on the server. // TODO: Nice comment about what headers it expects/honors, payload // format, and error responses. - HandleWriteValue = versionCheck(handleWriteValue) + HandleWriteValue = createHandler(handleWriteValue, true) // HandleGetRefs is meant to handle HTTP POST requests to the getRefs/ // server endpoint. Given a sequence of Chunk hashes, the server will // fetch and return them. // TODO: Nice comment about what headers it // expects/honors, payload format, and responses. - HandleGetRefs = versionCheck(handleGetRefs) + HandleGetRefs = createHandler(handleGetRefs, true) + + // HandleGetBlob is a custom endpoint whose sole purpose is to directly + // fetch the *bytes* contained in a Blob value. It expects a single query + // param of `h` to be the ref of the Blob. + // TODO: Support retrieving blob contents via a path. + HandleGetBlob = createHandler(handleGetBlob, false) // HandleWriteValue is meant to handle HTTP POST requests to the hasRefs/ // server endpoint. Given a sequence of Chunk hashes, the server check for // their presence and return a list of true/false responses. // TODO: Nice comment about what headers it expects/honors, payload // format, and responses. - HandleHasRefs = versionCheck(handleHasRefs) + HandleHasRefs = createHandler(handleHasRefs, true) // HandleRootGet is meant to handle HTTP GET requests to the root/ server // endpoint. The server returns the hash of the Root as a string. // TODO: Nice comment about what headers it expects/honors, payload // format, and responses. - HandleRootGet = versionCheck(handleRootGet) + HandleRootGet = createHandler(handleRootGet, true) // HandleWriteValue is meant to handle HTTP POST requests to the root/ // server endpoint. This is used to update the Root to point to a new // Chunk. // TODO: Nice comment about what headers it expects/honors, payload // format, and error responses. - HandleRootPost = versionCheck(handleRootPost) + HandleRootPost = createHandler(handleRootPost, true) // HandleBaseGet is meant to handle HTTP GET requests to the / server // endpoint. This is used to give a friendly message to users. @@ -82,10 +88,11 @@ var ( writeValueConcurrency = runtime.NumCPU() ) -func versionCheck(hndlr Handler) Handler { +func createHandler(hndlr Handler, versionCheck bool) Handler { return func(w http.ResponseWriter, req *http.Request, ps URLParams, cs chunks.ChunkStore) { w.Header().Set(NomsVersionHeader, constants.NomsVersion) - if req.Header.Get(NomsVersionHeader) != constants.NomsVersion { + + if versionCheck && req.Header.Get(NomsVersionHeader) != constants.NomsVersion { verbose.Log("Returning version mismatch error") http.Error( w, @@ -278,6 +285,29 @@ func handleGetRefs(w http.ResponseWriter, req *http.Request, ps URLParams, cs ch } } +func handleGetBlob(w http.ResponseWriter, req *http.Request, ps URLParams, cs chunks.ChunkStore) { + refStr := req.URL.Query().Get("h") + if refStr == "" { + d.Panic("Expected h param") + } + + h := hash.Parse(refStr) + if (h == hash.Hash{}) { + d.Panic("h failed to parse") + } + + vs := types.NewValueStore(types.NewBatchStoreAdaptor(cs)) + v := vs.ReadValue(h) + b, ok := v.(types.Blob) + if !ok { + d.Panic("h is not a Blob") + } + + w.Header().Add("Content-Type", "application/octet-stream") + w.Header().Add("Content-Length", fmt.Sprintf("%d", b.Len())) + b.Reader().Copy(w) +} + func extractHashes(req *http.Request) hash.HashSlice { err := req.ParseForm() d.PanicIfError(err) diff --git a/go/datas/remote_database_handlers_test.go b/go/datas/remote_database_handlers_test.go index b1787f9da8..05d5ec7e3e 100644 --- a/go/datas/remote_database_handlers_test.go +++ b/go/datas/remote_database_handlers_test.go @@ -245,6 +245,69 @@ func TestHandleGetRefs(t *testing.T) { } } +func TestHandleGetBlob(t *testing.T) { + assert := assert.New(t) + + blobContents := "I am a blob" + cs := chunks.NewTestStore() + db := NewDatabase(cs) + ds := db.GetDataset("foo") + + // Test missing h + w := httptest.NewRecorder() + HandleGetBlob( + w, + newRequest("GET", "", "/getBlob/", strings.NewReader(""), http.Header{}), + params{}, + cs, + ) + assert.Equal(http.StatusBadRequest, w.Code, "Handler error:\n%s", string(w.Body.Bytes())) + + b := types.NewStreamingBlob(db, bytes.NewBuffer([]byte(blobContents))) + + // Test non-present hash + w = httptest.NewRecorder() + HandleGetBlob( + w, + newRequest("GET", "", fmt.Sprintf("/getBlob/?h=%s", b.Hash().String()), strings.NewReader(""), http.Header{}), + params{}, + cs, + ) + assert.Equal(http.StatusBadRequest, w.Code, "Handler error:\n%s", string(w.Body.Bytes())) + + r := db.WriteValue(b) + ds, err := db.CommitValue(ds, r) + assert.NoError(err) + + // Valid + w = httptest.NewRecorder() + HandleGetBlob( + w, + newRequest("GET", "", fmt.Sprintf("/getBlob/?h=%s", r.TargetHash().String()), strings.NewReader(""), http.Header{}), + params{}, + cs, + ) + + if assert.Equal(http.StatusOK, w.Code, "Handler error:\n%s", string(w.Body.Bytes())) { + out, _ := ioutil.ReadAll(w.Body) + assert.Equal(string(out), blobContents) + } + + // Test non-blob + r2 := db.WriteValue(types.Number(1)) + ds, err = db.CommitValue(ds, r2) + assert.NoError(err) + + w = httptest.NewRecorder() + HandleGetBlob( + w, + newRequest("GET", "", fmt.Sprintf("/getBlob/?h=%s", r2.TargetHash().String()), strings.NewReader(""), http.Header{}), + params{}, + cs, + ) + assert.Equal(http.StatusBadRequest, w.Code, "Handler error:\n%s", string(w.Body.Bytes())) +} + func TestHandleHasRefs(t *testing.T) { assert := assert.New(t) cs := chunks.NewTestStore()