Skip to content

Commit

Permalink
feat(restore): Add api for restore. Test for checkout and restore apis.
Browse files Browse the repository at this point in the history
Use restore like:

```
curl -X POST -d "component=body" localhost:2503/restore/peername/ds_name
```
  • Loading branch information
dustmop committed Aug 28, 2019
1 parent 97aaed1 commit d1d9670
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 17 deletions.
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ func NewServerRoutes(s Server) *http.ServeMux {
m.Handle("/status/", s.middleware(fsih.StatusHandler("/status")))
m.Handle("/init/", s.middleware(fsih.InitHandler("/init")))
m.Handle("/checkout/", s.middleware(fsih.CheckoutHandler("/checkout")))
m.Handle("/restore/", s.middleware(fsih.RestoreHandler("/restore")))

renderh := NewRenderHandlers(node.Repo)
m.Handle("/render/", s.middleware(renderh.RenderHandler))
Expand Down
47 changes: 47 additions & 0 deletions api/fsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,50 @@ func (h *FSIHandlers) checkoutHandler(routePrefix string) http.HandlerFunc {
util.WriteResponse(w, res)
}
}

// RestoreHandler invokes restore via an API call
func (h *FSIHandlers) RestoreHandler(routePrefix string) http.HandlerFunc {
handleRestore := h.restoreHandler(routePrefix)
return func(w http.ResponseWriter, r *http.Request) {
if h.ReadOnly {
readOnlyResponse(w, routePrefix)
return
}

switch r.Method {
case "OPTIONS":
util.EmptyOkHandler(w, r)
case "POST":
handleRestore(w, r)
default:
util.NotFoundHandler(w, r)
}
}
}

func (h *FSIHandlers) restoreHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := DatasetRefFromPath(r.URL.Path[len(routePrefix):])
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
return
}

// Add the path for the version to restore
ref.Path = r.FormValue("path")

p := &lib.RestoreParams{
Dir: r.FormValue("dir"),
Ref: ref.String(),
Component: r.FormValue("component"),
}

var res string
if err := h.Restore(p, &res); err != nil {
util.WriteErrResponse(w, http.StatusInternalServerError, err)
return
}

util.WriteResponse(w, res)
}
}
184 changes: 174 additions & 10 deletions api/fsi_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package api

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/qri-io/dataset"
"github.com/qri-io/qri/fsi"
"github.com/qri-io/qri/lib"
"github.com/qri-io/qri/repo"
)

func TestFSIHandlers(t *testing.T) {
Expand Down Expand Up @@ -66,11 +72,13 @@ func TestNoHistory(t *testing.T) {

inst := newTestInstanceWithProfileFromNode(node)

initDir := "fsi_tests/init_dir"
tmpDir := os.TempDir()
initSubdir := "fsi_init_dir"
initDir := filepath.Join(tmpDir, initSubdir)
if err := os.MkdirAll(initDir, os.ModePerm); err != nil {
panic(err)
}
defer os.RemoveAll(filepath.Join("fsi_tests"))
defer os.RemoveAll(initDir)

// Create a linked dataset without saving, it has no versions in the repository
f := fsi.NewFSI(node.Repo)
Expand Down Expand Up @@ -104,9 +112,11 @@ func TestNoHistory(t *testing.T) {
if actualStatusCode != 200 {
t.Errorf("expected status code 200, got %d", actualStatusCode)
}
expectBody = `{"data":{"peername":"peer","name":"test_ds","fsiPath":"fsi_tests/init_dir","dataset":{"bodyPath":"fsi_tests/init_dir/body.csv","meta":{"keywords":[],"qri":"md:0"},"name":"test_ds","peername":"peer","qri":"ds:0","structure":{"format":"csv","qri":"st:0","schema":{"items":{"items":[{"title":"name","type":"string"},{"title":"describe","type":"string"},{"title":"quantity","type":"integer"}],"type":"array"},"type":"array"}}},"published":false},"meta":{"code":200}}`
if expectBody != actualBody {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
// Handle tempoary directory by replacing the temp part with a shorter string.
resultBody := strings.Replace(actualBody, initDir, initSubdir, -1)
expectBody = `{"data":{"peername":"peer","name":"test_ds","fsiPath":"fsi_init_dir","dataset":{"bodyPath":"fsi_init_dir/body.csv","meta":{"keywords":[],"qri":"md:0"},"name":"test_ds","peername":"peer","qri":"ds:0","structure":{"format":"csv","qri":"st:0","schema":{"items":{"items":[{"title":"name","type":"string"},{"title":"describe","type":"string"},{"title":"quantity","type":"integer"}],"type":"array"},"type":"array"}}},"published":false},"meta":{"code":200}}`
if expectBody != resultBody {
t.Errorf("expected body %s, got %s", expectBody, resultBody)
}

// Body with no history
Expand Down Expand Up @@ -146,9 +156,11 @@ func TestNoHistory(t *testing.T) {
if actualStatusCode != 200 {
t.Errorf("expected status code 200, got %d", actualStatusCode)
}
expectBody = `{"data":[{"sourceFile":"fsi_tests/init_dir/meta.json","component":"meta","type":"add","message":""},{"sourceFile":"fsi_tests/init_dir/schema.json","component":"schema","type":"add","message":""},{"sourceFile":"body.csv","component":"body","type":"add","message":""}],"meta":{"code":200}}`
if expectBody != actualBody {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
// Handle tempoary directory by replacing the temp part with a shorter string.
resultBody = strings.Replace(actualBody, initDir, initSubdir, -1)
expectBody = `{"data":[{"sourceFile":"fsi_init_dir/meta.json","component":"meta","type":"add","message":""},{"sourceFile":"fsi_init_dir/schema.json","component":"schema","type":"add","message":""},{"sourceFile":"body.csv","component":"body","type":"add","message":""}],"meta":{"code":200}}`
if expectBody != resultBody {
t.Errorf("expected body %s, got %s", expectBody, resultBody)
}

logHandler := NewLogHandlers(node)
Expand All @@ -174,9 +186,161 @@ func TestNoHistory(t *testing.T) {
}
}

func TestCheckoutAndRestore(t *testing.T) {
node, teardown := newTestNode(t)
defer teardown()

inst := newTestInstanceWithProfileFromNode(node)

tmpDir := os.TempDir()
workSubdir := "fsi_checkout_restore"
workDir := filepath.Join(tmpDir, workSubdir)
// Don't create the work directory, it must not exist for checkout to work. Remove if it
// already exists.
_ = os.RemoveAll(workDir)

dr := lib.NewDatasetRequests(node, nil)

// Save version 1
saveParams := lib.SaveParams{
Ref: "me/fsi_checkout_restore",
Dataset: &dataset.Dataset{
Meta: &dataset.Meta{
Title: "title one",
},
},
BodyPath: "testdata/cities/data.csv",
}
res := repo.DatasetRef{}
if err := dr.Save(&saveParams, &res); err != nil {
t.Fatal(err)
}

// Save the path from reference for later.
// TODO(dlong): Support full dataset refs, not just the path.
pos := strings.Index(res.String(), "/map/")
ref1 := res.String()[pos:]

// Save version 2 with a different title
saveParams = lib.SaveParams{
Ref: "me/fsi_checkout_restore",
Dataset: &dataset.Dataset{
Meta: &dataset.Meta{
Title: "title two",
},
},
}
if err := dr.Save(&saveParams, &res); err != nil {
t.Fatal(err)
}

fsiHandler := NewFSIHandlers(inst, false)

// Checkout the dataset
actualStatusCode, actualBody := APICallWithParams(
"POST",
"/checkout/peer/fsi_checkout_restore",
map[string]string{
"dir": workDir,
},
fsiHandler.CheckoutHandler("/checkout"))
if actualStatusCode != 200 {
t.Errorf("expected status code 200, got %d", actualStatusCode)
}
expectBody := `{"data":"","meta":{"code":200}}`
if expectBody != actualBody {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
}

// Read meta.json, should have "title two" as the meta title
metaContents, err := ioutil.ReadFile(filepath.Join(workDir, "meta.json"))
if err != nil {
t.Fatalf(err.Error())
}
expectContents := "{\n \"qri\": \"md:0\",\n \"title\": \"title two\"\n}"
if diff := cmp.Diff(expectContents, string(metaContents)); diff != "" {
t.Errorf("meta.json contents (-want +got):\n%s", diff)
}

// Overwrite meta so it has a different title
if err = ioutil.WriteFile("meta.json", []byte(`{"title": "hello"}`), os.ModePerm); err != nil {
t.Fatalf(err.Error())
}

// Restore the meta component
actualStatusCode, actualBody = APICallWithParams(
"POST",
"/restore/peer/fsi_checkout_restore",
map[string]string{
"component": "meta",
},
fsiHandler.RestoreHandler("/restore"))
if actualStatusCode != 200 {
t.Errorf("expected status code 200, got %d", actualStatusCode)
}
expectBody = `{"data":"","meta":{"code":200}}`
if expectBody != actualBody {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
}

// Read meta.json, should once again have "title two" as the meta title
metaContents, err = ioutil.ReadFile(filepath.Join(workDir, "meta.json"))
if err != nil {
t.Fatalf(err.Error())
}
expectContents = "{\n \"qri\": \"md:0\",\n \"title\": \"title two\"\n}"
if diff := cmp.Diff(expectContents, string(metaContents)); diff != "" {
t.Errorf("meta.json contents (-want +got):\n%s", diff)
}

// Restore the previous version of the dataset
actualStatusCode, actualBody = APICallWithParams(
"POST",
"/restore/peer/fsi_checkout_restore",
map[string]string{
// TODO(dlong): Have to pass "dir" to this method. In the test, the ref does
// not have an FSIPath. Might be because we're using /map/, not sure.
"dir": workDir,
"path": ref1,
},
fsiHandler.RestoreHandler("/restore"))
if actualStatusCode != 200 {
t.Errorf("expected status code 200, got %d", actualStatusCode)
}
expectBody = `{"data":"","meta":{"code":200}}`
if expectBody != actualBody {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
}

// Read meta.json, should now have "title one" as the meta title
metaContents, err = ioutil.ReadFile(filepath.Join(workDir, "meta.json"))
if err != nil {
t.Fatalf(err.Error())
}
expectContents = "{\n \"qri\": \"md:0\",\n \"title\": \"title one\"\n}"
if diff := cmp.Diff(expectContents, string(metaContents)); diff != "" {
t.Errorf("meta.json contents (-want +got):\n%s", diff)
}
}

// APICall calls the api and returns the status code and body
func APICall(url string, hf http.HandlerFunc) (int, string) {
req := httptest.NewRequest("GET", url, bytes.NewBuffer(nil))
return APICallWithParams("GET", url, nil, hf)
}

// APICallWithParams calls the api and returns the status code and body
func APICallWithParams(method, reqURL string, params map[string]string, hf http.HandlerFunc) (int, string) {
// Add parameters from map
reqParams := url.Values{}
if params != nil {
for key := range params {
reqParams.Set(key, params[key])
}
}
req := httptest.NewRequest(method, reqURL, strings.NewReader(reqParams.Encode()))
// Set form-encoded header so server will find the parameters
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(reqParams.Encode())))
w := httptest.NewRecorder()
hf(w, req)
res := w.Result()
Expand Down
Loading

0 comments on commit d1d9670

Please sign in to comment.