diff --git a/attachments.go b/attachments.go index 933772d..dac2323 100644 --- a/attachments.go +++ b/attachments.go @@ -27,7 +27,7 @@ func (db *DB) Attachment(docid, name, rev string) (*Attachment, error) { return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") } - resp, err := db.request("GET", revpath(rev, db.name, docid, name), nil) + resp, err := db.request("GET", revpath(rev, encid(db.name), encid(docid), name), nil) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (db *DB) AttachmentMeta(docid, name, rev string) (*Attachment, error) { return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") } - path := revpath(rev, db.name, docid, name) + path := revpath(rev, encid(db.name), encid(docid), name) resp, err := db.closedRequest("HEAD", path, nil) if err != nil { return nil, err @@ -72,7 +72,7 @@ func (db *DB) PutAttachment(docid string, att *Attachment, rev string) (newrev s return rev, fmt.Errorf("couchdb.PutAttachment: nil attachment Body") } - path := revpath(rev, db.name, docid, att.Name) + path := revpath(rev, encid(db.name), encid(docid), att.Name) req, err := db.newRequest("PUT", path, att.Body) if err != nil { return rev, err @@ -100,7 +100,7 @@ func (db *DB) DeleteAttachment(docid, name, rev string) (newrev string, err erro return rev, fmt.Errorf("couchdb.PutAttachment: empty name") } - path := revpath(rev, db.name, docid, name) + path := revpath(rev, encid(db.name), encid(docid), name) resp, err := db.closedRequest("DELETE", path, nil) return responseRev(resp, err) } diff --git a/couchdb.go b/couchdb.go index 724a767..7c7bed7 100644 --- a/couchdb.go +++ b/couchdb.go @@ -65,7 +65,7 @@ func (c *Client) SetAuth(a Auth) { // already exists. A valid DB object is returned in all cases, even if the // request fails. func (c *Client) CreateDB(name string) (*DB, error) { - if _, err := c.closedRequest("PUT", path(name), nil); err != nil { + if _, err := c.closedRequest("PUT", path(encid(name)), nil); err != nil { return c.DB(name), err } return c.DB(name), nil @@ -124,7 +124,7 @@ var getJsonKeys = []string{"open_revs", "atts_since"} // // http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid func (db *DB) Get(id string, doc interface{}, opts Options) error { - path, err := optpath(opts, getJsonKeys, db.name, id) + path, err := optpath(opts, getJsonKeys, encid(db.name), encid(id)) if err != nil { return err } @@ -139,12 +139,12 @@ func (db *DB) Get(id string, doc interface{}, opts Options) error { // It is faster than an equivalent Get request because no body // has to be parsed. func (db *DB) Rev(id string) (string, error) { - return responseRev(db.closedRequest("HEAD", path(db.name, id), nil)) + return responseRev(db.closedRequest("HEAD", path(encid(db.name), encid(id)), nil)) } // Put stores a document into the given database. func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err error) { - path := revpath(rev, db.name, id) + path := revpath(rev, encid(db.name), encid(id)) // TODO: make it possible to stream encoder output somehow json, err := json.Marshal(doc) if err != nil { @@ -156,7 +156,7 @@ func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err er // Delete marks a document revision as deleted. func (db *DB) Delete(id, rev string) (newrev string, err error) { - path := revpath(rev, db.name, id) + path := revpath(rev, encid(db.name), encid(id)) return responseRev(db.closedRequest("DELETE", path, nil)) } @@ -175,7 +175,7 @@ type Members struct { // Security retrieves the security object of a database. func (db *DB) Security() (*Security, error) { secobj := new(Security) - resp, err := db.request("GET", path(db.name, "_security"), nil) + resp, err := db.request("GET", path(encid(db.name), "_security"), nil) if err != nil { return nil, err } @@ -193,7 +193,7 @@ func (db *DB) Security() (*Security, error) { func (db *DB) PutSecurity(secobj *Security) error { json, _ := json.Marshal(secobj) body := bytes.NewReader(json) - _, err := db.request("PUT", path(db.name, "_security"), body) + _, err := db.request("PUT", path(encid(db.name), "_security"), body) return err } @@ -213,7 +213,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error { if !strings.HasPrefix(ddoc, "_design/") { return errors.New("couchdb.View: design doc name must start with _design/") } - path, err := optpath(opts, viewJsonKeys, db.name, ddoc, "_view", view) + path, err := optpath(opts, viewJsonKeys, encid(db.name), ddoc, "_view", encid(view)) if err != nil { return err } @@ -233,7 +233,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error { // // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-all-docs func (db *DB) AllDocs(result interface{}, opts Options) error { - path, err := optpath(opts, viewJsonKeys, db.name, "_all_docs") + path, err := optpath(opts, viewJsonKeys, encid(db.name), "_all_docs") if err != nil { return err } diff --git a/couchdb_test.go b/couchdb_test.go index 932847c..9c291b2 100644 --- a/couchdb_test.go +++ b/couchdb_test.go @@ -95,6 +95,18 @@ func TestCreateDB(t *testing.T) { check(t, "db.Name()", "db", db.Name()) } +func TestCreateDBWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /user%2F12345", func(resp ResponseWriter, req *Request) {}) + + db, err := c.CreateDB("user/12345") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "user/12345", db.Name()) +} + func TestDeleteDB(t *testing.T) { c := newTestClient(t) c.Handle("DELETE /db", func(resp ResponseWriter, req *Request) {}) @@ -103,6 +115,30 @@ func TestDeleteDB(t *testing.T) { } } +func TestEnsureDB(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /ensuredb", func(resp ResponseWriter, req *Request) {}) + + db, err := c.EnsureDB("ensuredb") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "ensuredb", db.Name()) +} + +func TestEnsureDBWithSlashInName(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /ensuredb%2Fslash", func(resp ResponseWriter, req *Request) {}) + + db, err := c.EnsureDB("ensuredb/slash") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "ensuredb/slash", db.Name()) +} + func TestAllDBs(t *testing.T) { c := newTestClient(t) c.Handle("GET /_all_dbs", func(resp ResponseWriter, req *Request) { @@ -202,6 +238,60 @@ func TestGetExistingDoc(t *testing.T) { check(t, "doc.Field", int64(999), doc.Field) } +func TestGetExistingDocWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/doc%2Fslash", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("doc/slash", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + +func TestGetDesignDoc(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/_design/myddoc", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("_design/myddoc", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + +func TestGetDesignDocWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/_design/myddoc%2Fslashed", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("_design/myddoc/slashed", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + func TestGetNonexistingDoc(t *testing.T) { c := newTestClient(t) c.Handle("GET /db/doc", func(resp ResponseWriter, req *Request) { @@ -261,6 +351,30 @@ func TestPut(t *testing.T) { check(t, "returned rev", "1-619db7ba8551c0de3f3a178775509611", rev) } +func TestPutWithSlashes(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /user%2F12345/todo%2F2711", func(resp ResponseWriter, req *Request) { + body, _ := ioutil.ReadAll(req.Body) + check(t, "request body", `{"field":999}`, string(body)) + + resp.Header().Set("ETag", `"1-619db7ba8551c0de3f3a178775509611"`) + resp.WriteHeader(StatusCreated) + io.WriteString(resp, `{ + "id": "doc", + "ok": true, + "rev": "1-619db7ba8551c0de3f3a178775509611" + }`) + }) + + doc := &testDocument{Field: 999} + rev, err := c.DB("user/12345").Put("todo/2711", doc, "") + if err != nil { + t.Fatal(err) + } + check(t, "returned rev", "1-619db7ba8551c0de3f3a178775509611", rev) +} + + func TestPutWithRev(t *testing.T) { c := newTestClient(t) c.Handle("PUT /db/doc", func(resp ResponseWriter, req *Request) { diff --git a/feeds.go b/feeds.go index abc51f9..4e988f6 100644 --- a/feeds.go +++ b/feeds.go @@ -140,7 +140,7 @@ type ChangesFeed struct { // // http://docs.couchdb.org/en/latest/api/database/changes.html#db-changes func (db *DB) Changes(options Options) (*ChangesFeed, error) { - path, err := optpath(options, nil, db.name, "_changes") + path, err := optpath(options, nil, encid(db.name), "_changes") if err != nil { return nil, err } diff --git a/http.go b/http.go index b3b2a68..12468de 100644 --- a/http.go +++ b/http.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net/http" "net/url" "reflect" @@ -48,15 +49,56 @@ func (t *transport) setAuth(a Auth) { } func (t *transport) newRequest(method, path string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, t.prefix+path, body) - if err != nil { - return nil, err + // workaround for https://github.com/golang/go/issues/5684 + // see also http://godoc.org/net/url#URL + // most of the request creation code was taken from net/http/request.go NewRequest() + parsed, _ := url.Parse(t.prefix + path) + pathcomp := strings.Split(path, "?") + + newurl := url.URL{ + Host: parsed.Host, + Scheme: parsed.Scheme, + Opaque: pathcomp[0], + } + if len(pathcomp) > 1 { + newurl.RawQuery = pathcomp[1] + } + + rc, ok := body.(io.ReadCloser) + if !ok && body != nil { + rc = ioutil.NopCloser(body) + } + + req := &http.Request{ + Method: method, + URL: &newurl, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Host: parsed.Host, + Header: http.Header{ + "User-Agent": {"go-couchdb/1.0"}, + }, + Body: rc, } + + if body != nil { + switch v := body.(type) { + case *bytes.Buffer: + req.ContentLength = int64(v.Len()) + case *bytes.Reader: + req.ContentLength = int64(v.Len()) + case *strings.Reader: + req.ContentLength = int64(v.Len()) + } + } + t.mu.RLock() defer t.mu.RUnlock() if t.auth != nil { t.auth.AddAuth(req) } + return req, nil } @@ -94,7 +136,7 @@ func path(segs ...string) string { r := "" for _, seg := range segs { r += "/" - r += url.QueryEscape(seg) + r += seg } return r } @@ -152,6 +194,23 @@ func encopts(opts Options, jskeys []string) (string, error) { return buf.String(), nil } +func encid(id string) string { + // issue #1: slashes in document IDs need to be escaped. + // ref: http://wiki.apache.org/couchdb/HTTP_Document_API#line-75 + const DDOC_PREFIX = "_design" + segments := strings.Split(id, "/") + if len(segments) > 1 { + if segments[0] == DDOC_PREFIX { + // preferred encoding for design docs is _design/seg1%2Fseg2 + id = segments[0] + "/" + strings.Join(segments[1:], "%2F") + } else { + id = strings.Join(segments, "%2F") + } + } + + return id +} + func encval(w io.Writer, k string, v interface{}) error { if v == nil { return errors.New("value is nil") diff --git a/x_test.go b/x_test.go index 9fc6188..a0ae198 100644 --- a/x_test.go +++ b/x_test.go @@ -35,9 +35,13 @@ func (s *testClient) ClearHandlers() { } func (s *testClient) RoundTrip(req *Request) (*Response, error) { - handler, ok := s.handlers[req.Method+" "+req.URL.Path] + path := req.URL.Path + if path == "" { + path = req.URL.Opaque + } + handler, ok := s.handlers[req.Method+" "+path] if !ok { - s.t.Fatalf("unhandled request: %s %s", req.Method, req.URL.Path) + s.t.Fatalf("unhandled request: %s %s", req.Method, path) return nil, nil } recorder := httptest.NewRecorder()