├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── attachments.go ├── attachments_test.go ├── auth.go ├── auth_test.go ├── cmd ├── couchapp │ └── main.go └── couchfeed │ └── main.go ├── couchapp ├── couchapp.go ├── couchapp_test.go └── testdata │ ├── dir │ ├── _id │ ├── language │ ├── options.json │ └── views │ │ └── abc.xyz │ │ └── map.js │ └── doc.json ├── couchdaemon ├── couchdaemon.go └── couchdaemon_test.go ├── couchdb.go ├── couchdb_test.go ├── feeds.go ├── feeds_test.go ├── go.mod ├── http.go ├── http_test.go └── x_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | c.out 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - 1.x 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Felix Lange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's this? 2 | 3 | go-couchdb is yet another CouchDB client written in Go. 4 | It was written because all the other ones didn't provide 5 | functionality that I need. 6 | 7 | The API is not fully baked at this time and may change. 8 | 9 | This project contains three Go packages: 10 | 11 | ## package couchdb [![GoDoc](https://godoc.org/github.com/fjl/go-couchdb?status.png)](http://godoc.org/github.com/fjl/go-couchdb) 12 | 13 | import "github.com/fjl/go-couchdb" 14 | 15 | This wraps the CouchDB HTTP API. 16 | 17 | ## package couchapp [![GoDoc](https://godoc.org/github.com/fjl/go-couchdb?status.png)](http://godoc.org/github.com/fjl/go-couchdb/couchapp) 18 | 19 | import "github.com/fjl/go-couchdb/couchapp" 20 | 21 | This provides functionality similar to the original 22 | [couchapp](https://github.com/couchapp/couchapp) tool, 23 | namely compiling a filesystem directory into a JSON object 24 | and storing the object as a CouchDB design document. 25 | 26 | ## package couchdaemon [![GoDoc](https://godoc.org/github.com/fjl/go-couchdb?status.png)](http://godoc.org/github.com/fjl/go-couchdb/couchdaemon) 27 | 28 | import "github.com/fjl/go-couchdb/couchdaemon" 29 | 30 | This package contains some functions that help 31 | you write Go programs that run as a daemon started by CouchDB, 32 | e.g. fetching values from the CouchDB config. 33 | 34 | # Tests 35 | 36 | You can run the unit tests with `go test`. 37 | 38 | [![Build Status](https://travis-ci.org/fjl/go-couchdb.png?branch=master)](https://travis-ci.org/fjl/go-couchdb) 39 | -------------------------------------------------------------------------------- /attachments.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | // Attachment represents document attachments. 11 | type Attachment struct { 12 | Name string // Filename 13 | Type string // MIME type of the Body 14 | MD5 []byte // MD5 checksum of the Body 15 | Body io.Reader // The body itself 16 | } 17 | 18 | // Attachment retrieves an attachment. 19 | // The rev argument can be left empty to retrieve the latest revision. 20 | // The caller is responsible for closing the attachment's Body if 21 | // the returned error is nil. 22 | func (db *DB) Attachment(docid, name, rev string) (*Attachment, error) { 23 | if docid == "" { 24 | return nil, fmt.Errorf("couchdb.GetAttachment: empty docid") 25 | } 26 | if name == "" { 27 | return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") 28 | } 29 | 30 | path := db.path().docID(docid).addRaw(name).rev(rev) 31 | resp, err := db.request("GET", path, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | att, err := attFromHeaders(name, resp) 36 | if err != nil { 37 | resp.Body.Close() 38 | return nil, err 39 | } 40 | att.Body = resp.Body 41 | return att, nil 42 | } 43 | 44 | // AttachmentMeta requests attachment metadata. 45 | // The rev argument can be left empty to retrieve the latest revision. 46 | // The returned attachment's Body is always nil. 47 | func (db *DB) AttachmentMeta(docid, name, rev string) (*Attachment, error) { 48 | if docid == "" { 49 | return nil, fmt.Errorf("couchdb.GetAttachment: empty docid") 50 | } 51 | if name == "" { 52 | return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") 53 | } 54 | 55 | path := db.path().docID(docid).addRaw(name).rev(rev) 56 | resp, err := db.closedRequest("HEAD", path, nil) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return attFromHeaders(name, resp) 61 | } 62 | 63 | // PutAttachment creates or updates an attachment. 64 | // To create an attachment on a non-existing document, pass an empty rev. 65 | func (db *DB) PutAttachment(docid string, att *Attachment, rev string) (newrev string, err error) { 66 | if docid == "" { 67 | return rev, fmt.Errorf("couchdb.PutAttachment: empty docid") 68 | } 69 | if att.Name == "" { 70 | return rev, fmt.Errorf("couchdb.PutAttachment: empty attachment Name") 71 | } 72 | if att.Body == nil { 73 | return rev, fmt.Errorf("couchdb.PutAttachment: nil attachment Body") 74 | } 75 | 76 | path := db.path().docID(docid).addRaw(att.Name).rev(rev) 77 | req, err := db.newRequest("PUT", path, att.Body) 78 | if err != nil { 79 | return rev, err 80 | } 81 | req.Header.Set("content-type", att.Type) 82 | 83 | resp, err := db.http.Do(req) 84 | if err != nil { 85 | return rev, err 86 | } 87 | var result struct{ Rev string } 88 | if err := readBody(resp, &result); err != nil { 89 | // TODO: close body if it implements io.ReadCloser 90 | return rev, fmt.Errorf("couchdb.PutAttachment: couldn't decode rev: %v", err) 91 | } 92 | return result.Rev, nil 93 | } 94 | 95 | // DeleteAttachment removes an attachment. 96 | func (db *DB) DeleteAttachment(docid, name, rev string) (newrev string, err error) { 97 | if docid == "" { 98 | return rev, fmt.Errorf("couchdb.PutAttachment: empty docid") 99 | } 100 | if name == "" { 101 | return rev, fmt.Errorf("couchdb.PutAttachment: empty name") 102 | } 103 | 104 | path := db.path().docID(docid).addRaw(name).rev(rev) 105 | resp, err := db.closedRequest("DELETE", path, nil) 106 | return responseRev(resp, err) 107 | } 108 | 109 | func attFromHeaders(name string, resp *http.Response) (*Attachment, error) { 110 | att := &Attachment{Name: name, Type: resp.Header.Get("content-type")} 111 | md5 := resp.Header.Get("content-md5") 112 | if md5 != "" { 113 | if len(md5) < 22 || len(md5) > 24 { 114 | return nil, fmt.Errorf("couchdb: Content-MD5 header has invalid size %d", len(md5)) 115 | } 116 | sum, err := base64.StdEncoding.DecodeString(md5) 117 | if err != nil { 118 | return nil, fmt.Errorf("couchdb: invalid base64 in Content-MD5 header: %v", err) 119 | } 120 | att.MD5 = sum 121 | } 122 | return att, nil 123 | } 124 | -------------------------------------------------------------------------------- /attachments_test.go: -------------------------------------------------------------------------------- 1 | package couchdb_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "github.com/fjl/go-couchdb" 8 | "io" 9 | "io/ioutil" 10 | . "net/http" 11 | "testing" 12 | ) 13 | 14 | var ( 15 | md5string = "2mGd+/VXL8dJsUlrD//Xag==" 16 | md5bytes, _ = base64.StdEncoding.DecodeString(md5string) 17 | ) 18 | 19 | func TestAttachment(t *testing.T) { 20 | c := newTestClient(t) 21 | c.Handle("GET /db/doc/attachment/1", 22 | func(resp ResponseWriter, req *Request) { 23 | resp.Header().Set("content-md5", "2mGd+/VXL8dJsUlrD//Xag==") 24 | resp.Header().Set("content-type", "text/plain") 25 | io.WriteString(resp, "the content") 26 | }) 27 | 28 | att, err := c.DB("db").Attachment("doc", "attachment/1", "") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | body, err := ioutil.ReadAll(att.Body) 33 | if err != nil { 34 | t.Fatalf("error reading body: %v", err) 35 | } 36 | 37 | check(t, "att.Name", "attachment/1", att.Name) 38 | check(t, "att.Type", "text/plain", att.Type) 39 | check(t, "att.MD5", md5bytes, att.MD5) 40 | check(t, "att.Body content", "the content", string(body)) 41 | } 42 | 43 | func TestAttachmentMeta(t *testing.T) { 44 | c := newTestClient(t) 45 | c.Handle("HEAD /db/doc/attachment/1", 46 | func(resp ResponseWriter, req *Request) { 47 | resp.Header().Set("content-md5", "2mGd+/VXL8dJsUlrD//Xag==") 48 | resp.Header().Set("content-type", "text/plain") 49 | resp.WriteHeader(StatusOK) 50 | }) 51 | 52 | att, err := c.DB("db").AttachmentMeta("doc", "attachment/1", "") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | check(t, "att.Name", "attachment/1", att.Name) 58 | check(t, "att.Type", "text/plain", att.Type) 59 | check(t, "att.MD5", md5bytes, att.MD5) 60 | check(t, "att.Body", nil, att.Body) 61 | } 62 | 63 | func TestPutAttachment(t *testing.T) { 64 | c := newTestClient(t) 65 | c.Handle("PUT /db/doc/attachment/1", 66 | func(resp ResponseWriter, req *Request) { 67 | reqBodyContent, err := ioutil.ReadAll(req.Body) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | ctype := req.Header.Get("Content-Type") 72 | check(t, "request content type", "text/plain", ctype) 73 | check(t, "request body", "the content", string(reqBodyContent)) 74 | check(t, "request query string", 75 | "rev=1-619db7ba8551c0de3f3a178775509611", 76 | req.URL.RawQuery) 77 | 78 | resp.Header().Set("content-md5", md5string) 79 | resp.Header().Set("content-type", "application/json") 80 | json.NewEncoder(resp).Encode(map[string]interface{}{ 81 | "ok": true, 82 | "id": "doc", 83 | "rev": "2-619db7ba8551c0de3f3a178775509611", 84 | }) 85 | }) 86 | 87 | att := &couchdb.Attachment{ 88 | Name: "attachment/1", 89 | Type: "text/plain", 90 | Body: bytes.NewBufferString("the content"), 91 | } 92 | newrev, err := c.DB("db").PutAttachment("doc", att, "1-619db7ba8551c0de3f3a178775509611") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | check(t, "newrev", "2-619db7ba8551c0de3f3a178775509611", newrev) 98 | check(t, "att.Name", "attachment/1", att.Name) 99 | check(t, "att.Type", "text/plain", att.Type) 100 | check(t, "att.MD5", []byte(nil), att.MD5) 101 | } 102 | 103 | func TestDeleteAttachment(t *testing.T) { 104 | c := newTestClient(t) 105 | c.Handle("DELETE /db/doc/attachment/1", 106 | func(resp ResponseWriter, req *Request) { 107 | check(t, "request query string", 108 | "rev=1-619db7ba8551c0de3f3a178775509611", 109 | req.URL.RawQuery) 110 | 111 | resp.Header().Set("etag", `"2-619db7ba8551c0de3f3a178775509611"`) 112 | json.NewEncoder(resp).Encode(map[string]interface{}{ 113 | "ok": true, 114 | "id": "doc", 115 | "rev": "2-619db7ba8551c0de3f3a178775509611", 116 | }) 117 | }) 118 | 119 | newrev, err := c.DB("db").DeleteAttachment("doc", "attachment/1", "1-619db7ba8551c0de3f3a178775509611") 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | check(t, "newrev", "2-619db7ba8551c0de3f3a178775509611", newrev) 125 | } 126 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // Auth is implemented by HTTP authentication mechanisms. 14 | type Auth interface { 15 | // AddAuth should add authentication information (e.g. headers) 16 | // to the given HTTP request. 17 | AddAuth(*http.Request) 18 | } 19 | 20 | type basicauth string 21 | 22 | // BasicAuth returns an Auth that performs HTTP Basic Authentication. 23 | func BasicAuth(username, password string) Auth { 24 | auth := []byte(username + ":" + password) 25 | hdr := "Basic " + base64.StdEncoding.EncodeToString(auth) 26 | return basicauth(hdr) 27 | } 28 | 29 | func (a basicauth) AddAuth(req *http.Request) { 30 | req.Header.Set("Authorization", string(a)) 31 | } 32 | 33 | type proxyauth struct { 34 | username, roles, tok string 35 | } 36 | 37 | // ProxyAuth returns an Auth that performs CouchDB proxy authentication. 38 | // Please consult the CouchDB documentation for more information on proxy 39 | // authentication: 40 | // 41 | // http://docs.couchdb.org/en/latest/api/server/authn.html?highlight=proxy#proxy-authentication 42 | func ProxyAuth(username string, roles []string, secret string) Auth { 43 | pa := &proxyauth{username, strings.Join(roles, ","), ""} 44 | if secret != "" { 45 | mac := hmac.New(sha1.New, []byte(secret)) 46 | io.WriteString(mac, username) 47 | pa.tok = fmt.Sprintf("%x", mac.Sum(nil)) 48 | } 49 | return pa 50 | } 51 | 52 | func (a proxyauth) AddAuth(req *http.Request) { 53 | req.Header.Set("X-Auth-CouchDB-UserName", a.username) 54 | req.Header.Set("X-Auth-CouchDB-Roles", a.roles) 55 | if a.tok != "" { 56 | req.Header.Set("X-Auth-CouchDB-Token", a.tok) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package couchdb_test 2 | 3 | import ( 4 | "github.com/fjl/go-couchdb" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestBasicAuth(t *testing.T) { 10 | tests := []struct{ username, password, header string }{ 11 | {"", "", "Basic Og=="}, 12 | {"user", "", "Basic dXNlcjo="}, 13 | {"", "password", "Basic OnBhc3N3b3Jk"}, 14 | {"user", "password", "Basic dXNlcjpwYXNzd29yZA=="}, 15 | } 16 | 17 | for _, test := range tests { 18 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 19 | auth := couchdb.BasicAuth(test.username, test.password) 20 | auth.AddAuth(req) 21 | 22 | expected := http.Header{"Authorization": {test.header}} 23 | check(t, "req headers", expected, req.Header) 24 | } 25 | } 26 | 27 | func TestProxyAuthWithoutToken(t *testing.T) { 28 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 29 | auth := couchdb.ProxyAuth("user", []string{"role1", "role2"}, "") 30 | auth.AddAuth(req) 31 | 32 | expected := http.Header{ 33 | "X-Auth-Couchdb-Username": {"user"}, 34 | "X-Auth-Couchdb-Roles": {"role1,role2"}, 35 | } 36 | check(t, "req headers", expected, req.Header) 37 | } 38 | 39 | func TestProxyAuthWithToken(t *testing.T) { 40 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 41 | auth := couchdb.ProxyAuth("user", []string{"role1", "role2"}, "secret") 42 | auth.AddAuth(req) 43 | 44 | expected := http.Header{ 45 | "X-Auth-Couchdb-Username": {"user"}, 46 | "X-Auth-Couchdb-Roles": {"role1,role2"}, 47 | "X-Auth-Couchdb-Token": {"027da48c8c642ca4c58eb982eec81915179e77a3"}, 48 | } 49 | check(t, "req headers", expected, req.Header) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/couchapp/main.go: -------------------------------------------------------------------------------- 1 | // The couchapp tool deploys a directory as a CouchDB design document. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/fjl/go-couchdb" 11 | "github.com/fjl/go-couchdb/couchapp" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | server = flag.String("server", "http://127.0.0.1:5984/", "CouchDB server URL") 17 | dbname = flag.String("db", "", "Database name (required)") 18 | docid = flag.String("docid", "", "Design document name (required)") 19 | ignore = flag.String("ignore", "", "Ignore patterns.") 20 | ) 21 | flag.Parse() 22 | if flag.NArg() != 1 { 23 | fatalf("Need directory as argument.") 24 | } 25 | if *docid == "" { 26 | fatalf("-docid is required.") 27 | } 28 | if *dbname == "" { 29 | fatalf("-db is required.") 30 | } 31 | 32 | dir := flag.Arg(0) 33 | ignores := strings.Split(*ignore, ",") 34 | doc, err := couchapp.LoadDirectory(dir, ignores) 35 | if err != nil { 36 | fatalf("%v", err) 37 | } 38 | client, err := couchdb.NewClient(*server, nil) 39 | if err != nil { 40 | fatalf("can't create database client: %v", err) 41 | } 42 | rev, err := couchapp.Store(client.DB(*dbname), *docid, doc) 43 | if err != nil { 44 | fatalf("%v", err) 45 | } 46 | fmt.Println(rev) 47 | } 48 | 49 | func fatalf(format string, args ...interface{}) { 50 | fmt.Fprintf(os.Stderr, format+"\n", args...) 51 | os.Exit(1) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/couchfeed/main.go: -------------------------------------------------------------------------------- 1 | // The couchfeed tool logs CouchDB feeds. 2 | // This tool is not very useful, it's mostly an API demo. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/fjl/go-couchdb" 11 | ) 12 | 13 | func main() { 14 | var ( 15 | server = flag.String("server", "http://127.0.0.1:5984/", "CouchDB server URL") 16 | dbname = flag.String("db", "", "Database name") 17 | dbupdates = flag.Bool("dbupdates", false, "Show DB updates feed") 18 | follow = flag.Bool("f", false, "Use 'continuous' feed mode") 19 | ) 20 | flag.Parse() 21 | if !*dbupdates && *dbname == "" { 22 | fatalf("-db or -dbupdates is required.") 23 | } 24 | opt := couchdb.Options{"feed": "normal"} 25 | if *follow { 26 | opt["feed"] = "continuous" 27 | } 28 | 29 | client, err := couchdb.NewClient(*server, nil) 30 | if err != nil { 31 | fatalf("can't create database client: %v") 32 | } 33 | 34 | var f feed 35 | var show func() 36 | if *dbupdates { 37 | f, err = client.DBUpdates(opt) 38 | show = func() { 39 | chf := f.(*couchdb.DBUpdatesFeed) 40 | fmt.Println(chf.Event, "db", chf.DB, "seq", chf.Seq) 41 | } 42 | } else { 43 | f, err = client.DB(*dbname).Changes(opt) 44 | show = func() { 45 | chf := f.(*couchdb.ChangesFeed) 46 | if chf.Deleted { 47 | fmt.Println("deleted:", chf.ID, chf.Seq) 48 | } else { 49 | fmt.Println("changed:", chf.ID, chf.Seq) 50 | } 51 | } 52 | } 53 | if err != nil { 54 | fatalf("can't open feed: %v", err) 55 | } 56 | defer f.Close() 57 | for f.Next() { 58 | show() 59 | } 60 | if f.Err() != nil { 61 | fatalf("feed error: %#v", f.Err()) 62 | } 63 | } 64 | 65 | type feed interface { 66 | Next() bool 67 | Err() error 68 | Close() error 69 | } 70 | 71 | func fatalf(format string, args ...interface{}) { 72 | fmt.Fprintf(os.Stderr, format+"\n", args...) 73 | os.Exit(1) 74 | } 75 | -------------------------------------------------------------------------------- /couchapp/couchapp.go: -------------------------------------------------------------------------------- 1 | // Package couchapp implements a mapping from files to CouchDB documents. 2 | // 3 | // CouchDB design documents, which contain view definitions etc., are stored 4 | // as JSON objects in the database. A 'couchapp' is a directory structure 5 | // that is compiled into a design document and then installed into the 6 | // database. The functions in this package are probably most useful for 7 | // uploading design documents, but can be used for any kind of document. 8 | package couchapp 9 | 10 | import ( 11 | "bytes" 12 | "encoding/json" 13 | "fmt" 14 | "io/ioutil" 15 | "mime" 16 | "os" 17 | "path" 18 | "strings" 19 | 20 | "github.com/fjl/go-couchdb" 21 | ) 22 | 23 | // DefaultIgnorePatterns contains the default list of glob patterns 24 | // that are ignored when building a document from a directory. 25 | var DefaultIgnorePatterns = []string{ 26 | "*~", // editor swap files 27 | ".*", // hidden files 28 | "_*", // CouchDB system fields 29 | } 30 | 31 | // Doc represents CouchDB documents. 32 | type Doc map[string]interface{} 33 | 34 | // LoadFile creates a document from a single JSON file. 35 | func LoadFile(file string) (Doc, error) { 36 | val, err := loadJSON(file) 37 | if err != nil { 38 | return nil, err 39 | } 40 | mapval, ok := val.(map[string]interface{}) 41 | if !ok { 42 | return nil, fmt.Errorf("%s does not contain a JSON object", file) 43 | } 44 | return Doc(mapval), nil 45 | } 46 | 47 | // LoadDirectory transforms a directory structure on disk 48 | // into a JSON object. All directories become JSON objects 49 | // whose keys are file and directory names. For regular files, 50 | // the file extension is stripped from the key. Files with the 51 | // .json extension are included as JSON. 52 | // 53 | // Example tree: 54 | // 55 | // / 56 | // a.txt // contains `text-a` 57 | // b.json // contains `{"key": 1}` 58 | // c/ 59 | // d.xyz/ 60 | // e/ 61 | // f // contains `text-f` 62 | // 63 | // This would be compiled into the following JSON object: 64 | // 65 | // { 66 | // "a": "text-a", 67 | // "b": {"key": 1}, 68 | // "c": { 69 | // "d.xyz": {}, 70 | // "e": { 71 | // "f": "text-f" 72 | // } 73 | // } 74 | // } 75 | // 76 | // The second argument is a slice of glob patterns for ignored files. 77 | // If nil is given, the default patterns are used. The patterns are 78 | // matched against the basename, not the full path. 79 | func LoadDirectory(dirname string, ignores []string) (Doc, error) { 80 | stack := &objstack{obj: make(Doc)} 81 | err := walk(dirname, ignores, func(p string, isDir, dirEnd bool) error { 82 | if dirEnd { 83 | stack = stack.parent // pop 84 | return nil 85 | } 86 | 87 | name := path.Base(p) 88 | if isDir { 89 | val := make(map[string]interface{}) 90 | stack.obj[name] = val 91 | stack = &objstack{obj: val, parent: stack} // push 92 | } else { 93 | content, err := load(p) 94 | if err != nil { 95 | return err 96 | } 97 | stack.obj[stripExtension(name)] = content 98 | } 99 | return nil 100 | }) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return stack.obj, err 105 | } 106 | 107 | type objstack struct { 108 | obj map[string]interface{} 109 | parent *objstack 110 | } 111 | 112 | func load(filename string) (interface{}, error) { 113 | if path.Ext(filename) == ".json" { 114 | return loadJSON(filename) 115 | } 116 | return loadString(filename) 117 | } 118 | 119 | // loadString returns the given file's contents as a string 120 | // and strips off any surrounding whitespace. 121 | func loadString(file string) (string, error) { 122 | data, err := ioutil.ReadFile(file) 123 | if err != nil { 124 | return "", err 125 | } 126 | return string(bytes.Trim(data, " \n\r")), nil 127 | } 128 | 129 | // loadJSON decodes the content of the given file as JSON. 130 | func loadJSON(file string) (interface{}, error) { 131 | content, err := ioutil.ReadFile(file) 132 | if err != nil { 133 | return nil, err 134 | } 135 | // TODO: use json.Number 136 | var val interface{} 137 | if err := json.Unmarshal(content, &val); err != nil { 138 | if syntaxerr, ok := err.(*json.SyntaxError); ok { 139 | line := findLine(content, syntaxerr.Offset) 140 | err = fmt.Errorf("JSON syntax error at %v:%v: %v", file, line, err) 141 | return nil, err 142 | } 143 | return nil, fmt.Errorf("JSON unmarshal error in %v: %v", file, err) 144 | } 145 | return val, nil 146 | } 147 | 148 | // findLine returns the line number for the given offset into data. 149 | func findLine(data []byte, offset int64) (line int) { 150 | line = 1 151 | for i, r := range string(data) { 152 | if int64(i) >= offset { 153 | return 154 | } 155 | if r == '\n' { 156 | line++ 157 | } 158 | } 159 | return 160 | } 161 | 162 | // stripExtension returns the given filename without its extension. 163 | func stripExtension(filename string) string { 164 | if i := strings.LastIndex(filename, "."); i != -1 { 165 | return filename[:i] 166 | } 167 | return filename 168 | } 169 | 170 | // Store updates the given document in a database. 171 | // If the document exists, it will be overwritten. 172 | // The new revision of the document is returned. 173 | func Store(db *couchdb.DB, docid string, doc Doc) (string, error) { 174 | if rev, err := db.Rev(docid); err == nil { 175 | return db.Put(docid, doc, rev) 176 | } else if couchdb.NotFound(err) { 177 | return db.Put(docid, doc, "") 178 | } else { 179 | return "", err 180 | } 181 | } 182 | 183 | // StoreAttachments uploads the files in a directory as attachments 184 | // to a document extension. The document does not need to exist in the 185 | // database. The MIME type of each file is guessed by x the filename. 186 | // 187 | // As with LoadDirectory, ignores is a slice of glob patterns 188 | // that are matched against the file/directory basename. If any one of them 189 | // matches, the file is not uploaded. If a nil slice is given, the default 190 | // patterns are used. 191 | // 192 | // A correct revision id is returned in all cases, even if there was an error. 193 | func StoreAttachments( 194 | db *couchdb.DB, 195 | docid, rev, dir string, 196 | ignores []string, 197 | ) (newrev string, err error) { 198 | newrev = rev 199 | err = walk(dir, ignores, func(p string, isDir, dirEnd bool) error { 200 | if isDir { 201 | return nil 202 | } 203 | 204 | att := &couchdb.Attachment{ 205 | Name: strings.TrimPrefix(p, dir+"/"), 206 | Type: mime.TypeByExtension(path.Ext(p)), 207 | } 208 | if att.Body, err = os.Open(p); err != nil { 209 | return err 210 | } 211 | newrev, err = db.PutAttachment(docid, att, newrev) 212 | return err 213 | }) 214 | return 215 | } 216 | 217 | type walkFunc func(path string, isDir, dirEnd bool) error 218 | 219 | func walk(dir string, ignores []string, callback walkFunc) error { 220 | if ignores == nil { 221 | ignores = DefaultIgnorePatterns 222 | } 223 | files, err := ioutil.ReadDir(dir) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | for _, info := range files { 229 | isDir := info.IsDir() 230 | subpath := path.Join(dir, info.Name()) 231 | // skip ignored files 232 | for _, pat := range ignores { 233 | if ign, err := path.Match(pat, info.Name()); err != nil { 234 | return err 235 | } else if ign { 236 | goto next 237 | } 238 | } 239 | 240 | if err := callback(subpath, isDir, false); err != nil { 241 | return err 242 | } 243 | if isDir { 244 | if err := walk(subpath, ignores, callback); err != nil { 245 | return err 246 | } 247 | if err := callback(subpath, true, true); err != nil { 248 | return err 249 | } 250 | } 251 | next: 252 | } 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /couchapp/couchapp_test.go: -------------------------------------------------------------------------------- 1 | package couchapp 2 | 3 | import ( 4 | "path" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestLoadFile(t *testing.T) { 10 | doc, err := LoadFile("testdata/doc.json") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | expdoc := Doc{ 16 | "_id": "doc", 17 | "float": 1.0, 18 | "array": []interface{}{1.0, 2.0, 3.0}, 19 | } 20 | check(t, "doc", expdoc, doc) 21 | } 22 | 23 | func TestLoadDirectory(t *testing.T) { 24 | doc, err := LoadDirectory("testdata/dir", nil) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | expdoc := Doc{ 30 | "language": "javascript", 31 | "views": map[string]interface{}{ 32 | "abc.xyz": map[string]interface{}{ 33 | "map": "function (x) { return x; }", 34 | }, 35 | }, 36 | "options": map[string]interface{}{ 37 | "local_seq": true, 38 | }, 39 | } 40 | check(t, "doc", expdoc, doc) 41 | } 42 | 43 | func TestBrokenIgnorePattern(t *testing.T) { 44 | doc, err := LoadDirectory("testdata/dir", []string{"[]"}) 45 | check(t, "doc", Doc(nil), doc) 46 | check(t, "error", path.ErrBadPattern, err) 47 | } 48 | 49 | func check(t *testing.T, field string, expected, actual interface{}) { 50 | if !reflect.DeepEqual(expected, actual) { 51 | t.Errorf("%s mismatch: want %#v, got %#v", field, expected, actual) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /couchapp/testdata/dir/_id: -------------------------------------------------------------------------------- 1 | _design/foobar 2 | -------------------------------------------------------------------------------- /couchapp/testdata/dir/language: -------------------------------------------------------------------------------- 1 | javascript 2 | -------------------------------------------------------------------------------- /couchapp/testdata/dir/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "local_seq": true 3 | } 4 | -------------------------------------------------------------------------------- /couchapp/testdata/dir/views/abc.xyz/map.js: -------------------------------------------------------------------------------- 1 | function (x) { return x; } 2 | 3 | -------------------------------------------------------------------------------- /couchapp/testdata/doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "doc", 3 | "float": 1.0, 4 | "array": [1,2,3] 5 | } 6 | -------------------------------------------------------------------------------- /couchdaemon/couchdaemon.go: -------------------------------------------------------------------------------- 1 | // Package couchdaemon provides utilities for processes running 2 | // as a CouchDB os_daemon. 3 | package couchdaemon 4 | 5 | import ( 6 | "bufio" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "os" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | var ( 17 | initOnce sync.Once 18 | ) 19 | 20 | var ( 21 | // ErrNotFound is returned by the config API when a key is not available. 22 | ErrNotFound = errors.New("couchdaemon: config key not found") 23 | 24 | // ErrNotInitialized is returned by all API functions 25 | // before Init has been called. 26 | ErrNotInitialized = errors.New("couchdaemon: not initialized") 27 | ) 28 | 29 | // Init configures stdin and stdout for communication with couchdb. 30 | // 31 | // The argument can be a writable channel or nil. If it is nil, the process 32 | // will exit with status 0 when CouchDB signals that is exiting. If the value 33 | // is a channel, the channel will be closed instead. 34 | // 35 | // Stdin or stdout directly will confuse CouchDB should therefore be avoided. 36 | // 37 | // You should call this function early in your initialization. 38 | // The other API functions will return ErrNotInitialized until Init 39 | // has been called. 40 | func Init(exit chan<- struct{}) { 41 | initOnce.Do(func() { 42 | if exit == nil { 43 | start(os.Stdin, os.Stdout, func() { os.Exit(0) }) 44 | } else { 45 | start(os.Stdin, os.Stdout, func() { os.Exit(0) }) 46 | } 47 | }) 48 | } 49 | 50 | // ConfigSection reads a whole section from the CouchDB configuration. 51 | // If the section is not present, the error will be ErrNotFound and 52 | // the returned map will be nil. 53 | func ConfigSection(section string) (map[string]string, error) { 54 | var val *map[string]string 55 | err := request(&val, "get", section) 56 | switch { 57 | case err != nil: 58 | return nil, err 59 | case val == nil: 60 | return nil, ErrNotFound 61 | default: 62 | return *val, nil 63 | } 64 | } 65 | 66 | // ConfigVal reads a parameter value from the CouchDB configuration. 67 | // If the parameter is unset, the error will be ErrNotFound and the 68 | // returned string will be empty. 69 | func ConfigVal(section, item string) (string, error) { 70 | var val *string 71 | err := request(&val, "get", section, item) 72 | switch { 73 | case err != nil: 74 | return "", err 75 | case val == nil: 76 | return "", ErrNotFound 77 | default: 78 | return *val, nil 79 | } 80 | } 81 | 82 | // ServerURL returns the URL of the CouchDB server that started the daemon. 83 | func ServerURL() (string, error) { 84 | port, err := ConfigVal("httpd", "port") 85 | if err != nil { 86 | return "", err 87 | } 88 | addr, err := ConfigVal("httpd", "bind_address") 89 | if err != nil { 90 | return "", err 91 | } 92 | if addr == "0.0.0.0" { 93 | addr = "127.0.0.1" 94 | } 95 | return "http://" + addr + ":" + port + "/", nil 96 | } 97 | 98 | // A LogWriter writes messages to the CouchDB log. 99 | // Its method set is a subset of the methods provided by log/syslog.Writer. 100 | type LogWriter interface { 101 | io.Writer 102 | // Err writes a message with level "error" 103 | Err(msg string) error 104 | // Info writes a message with level "info" 105 | Info(msg string) error 106 | // Info writes a message with level "debug" 107 | Debug(msg string) error 108 | } 109 | 110 | type logger struct{} 111 | 112 | // NewLogWriter creates a log writer that outputs to the CouchDB log. 113 | func NewLogWriter() LogWriter { return logger{} } 114 | 115 | func (logger) Err(msg string) error { return logwrite(msg, &optsError) } 116 | func (logger) Info(msg string) error { return logwrite(msg, &optsInfo) } 117 | func (logger) Debug(msg string) error { return logwrite(msg, &optsDebug) } 118 | 119 | func (logger) Write(msg []byte) (int, error) { 120 | if err := logwrite(string(msg), nil); err != nil { 121 | return 0, err 122 | } 123 | return len(msg), nil 124 | } 125 | 126 | var ( 127 | optsError = json.RawMessage(`{"level":"error"}`) 128 | optsInfo = json.RawMessage(`{"level":"info"}`) 129 | optsDebug = json.RawMessage(`{"level":"debug"}`) 130 | ) 131 | 132 | func logwrite(msg string, opts *json.RawMessage) error { 133 | msg = strings.TrimRight(msg, "\n") 134 | if opts == nil { 135 | return request(nil, "log", msg) 136 | } 137 | return request(nil, "log", msg, opts) 138 | } 139 | 140 | var ( 141 | // mutex protects the globals during initialization and request I/O 142 | mutex sync.Mutex 143 | 144 | exit func() 145 | stdin io.ReadCloser 146 | stdout io.Writer 147 | inputc chan []byte 148 | ) 149 | 150 | func start(in io.ReadCloser, out io.Writer, ef func()) { 151 | mutex.Lock() 152 | defer mutex.Unlock() 153 | 154 | exit = ef 155 | stdin = in 156 | stdout = out 157 | inputc = make(chan []byte) 158 | go inputloop(in, inputc, exit) 159 | } 160 | 161 | // inputloop reads lines from stdin until it is closed. 162 | func inputloop(in io.Reader, inputc chan<- []byte, exit func()) { 163 | bufin := bufio.NewReader(in) 164 | for { 165 | line, err := bufin.ReadBytes('\n') 166 | if err != nil { 167 | break 168 | } 169 | inputc <- line 170 | } 171 | exit() 172 | close(inputc) 173 | } 174 | 175 | func request(result interface{}, query ...interface{}) error { 176 | mutex.Lock() 177 | defer mutex.Unlock() 178 | 179 | if exit == nil { 180 | return ErrNotInitialized 181 | } 182 | line, err := json.Marshal(query) 183 | if err != nil { 184 | return err 185 | } 186 | if _, err := fmt.Fprintf(stdout, "%s\n", line); err != nil { 187 | return err 188 | } 189 | if result != nil { 190 | if err := json.Unmarshal(<-inputc, result); err != nil { 191 | return err 192 | } 193 | } 194 | return nil 195 | } 196 | -------------------------------------------------------------------------------- /couchdaemon/couchdaemon_test.go: -------------------------------------------------------------------------------- 1 | package couchdaemon 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "io" 8 | "reflect" 9 | "sync" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | type testHost struct { 15 | output bytes.Buffer 16 | exitchan chan struct{} 17 | config testConfig 18 | outW io.Closer 19 | wg sync.WaitGroup 20 | stopOnce sync.Once 21 | } 22 | 23 | type testConfig map[string]map[string]string 24 | 25 | func startTestHost(t *testing.T, config testConfig) *testHost { 26 | inR, inW := io.Pipe() // input stream (testHost writes, daemon reads) 27 | outR, outW := io.Pipe() // output stream (testHost reads, daemon writes) 28 | th := &testHost{ 29 | exitchan: make(chan struct{}), 30 | config: config, 31 | outW: outW, 32 | } 33 | 34 | th.wg.Add(1) 35 | go func() { 36 | defer th.wg.Done() 37 | enc := json.NewEncoder(inW) 38 | bufoutR := bufio.NewReader(outR) 39 | var req []interface{} 40 | for { 41 | line, err := bufoutR.ReadBytes('\n') 42 | if err != nil { 43 | return 44 | } 45 | 46 | th.output.Write(line) 47 | if err := json.Unmarshal(line, &req); err != nil { 48 | t.Errorf("testHost: could not decode request: %v", err) 49 | return 50 | } 51 | 52 | t.Logf("testHost: got request %v", req) 53 | switch { 54 | case len(req) <= 1: 55 | t.Errorf("request array to short") 56 | case req[0] == "log": 57 | // no response 58 | case req[0] == "get" && req[1] == "garbage": 59 | io.WriteString(inW, "garbage line\n") 60 | case req[0] == "get" && len(req) == 2: 61 | enc.Encode(config[req[1].(string)]) 62 | case req[0] == "get" && len(req) == 3: 63 | if v, found := config[req[1].(string)][req[2].(string)]; found { 64 | enc.Encode(v) 65 | } else { 66 | enc.Encode(nil) 67 | } 68 | default: 69 | t.Errorf("testHost: unmatched request") 70 | } 71 | } 72 | }() 73 | 74 | start(inR, outW, func() { close(th.exitchan) }) 75 | return th 76 | } 77 | 78 | // stop stops listening and returns the accumulated output. 79 | func (th *testHost) stop() string { 80 | th.stopOnce.Do(func() { 81 | th.outW.Close() 82 | stdin.Close() 83 | th.wg.Wait() 84 | }) 85 | return th.output.String() 86 | } 87 | 88 | func TestNotInitialized(t *testing.T) { 89 | if _, err := ConfigSection("s"); err != ErrNotInitialized { 90 | t.Errorf("ConfigSection err mismatch, got %v, want ErrNotInitialized", err) 91 | } 92 | if _, err := ConfigVal("s", "k"); err != ErrNotInitialized { 93 | t.Errorf("ConfigVal err mismatch, got %v, want ErrNotInitialized", err) 94 | } 95 | if _, err := ServerURL(); err != ErrNotInitialized { 96 | t.Errorf("ServerURL err mismatch, got %v, want ErrNotInitialized", err) 97 | } 98 | log := NewLogWriter() 99 | if _, err := log.Write([]byte("foo")); err != ErrNotInitialized { 100 | t.Errorf("log.Write err mismatch, got %v, want ErrNotInitialized", err) 101 | } 102 | if err := log.Err("foo"); err != ErrNotInitialized { 103 | t.Errorf("log.Err err mismatch, got %v, want ErrNotInitialized", err) 104 | } 105 | if err := log.Info("foo"); err != ErrNotInitialized { 106 | t.Errorf("log.Info err mismatch, got %v, want ErrNotInitialized", err) 107 | } 108 | if err := log.Debug("foo"); err != ErrNotInitialized { 109 | t.Errorf("log.Debug err mismatch, got %v, want ErrNotInitialized", err) 110 | } 111 | } 112 | 113 | func TestLogWrite(t *testing.T) { 114 | th := startTestHost(t, nil) 115 | defer th.stop() 116 | 117 | log := NewLogWriter() 118 | msg := "a\"bc\n" 119 | 120 | n, err := io.WriteString(log, msg) 121 | if err != nil { 122 | t.Errorf("write error: %v", err) 123 | } 124 | if n != len(msg) { 125 | t.Errorf("short write: %v != %v", n, len(msg)) 126 | } 127 | if output := th.stop(); output != `["log","a\"bc"]`+"\n" { 128 | t.Errorf("wrong JSON output: %s", output) 129 | } 130 | } 131 | 132 | func TestLogWriteError(t *testing.T) { 133 | th := startTestHost(t, nil) 134 | defer th.stop() 135 | 136 | th.outW.Close() 137 | 138 | log := NewLogWriter() 139 | if _, err := log.Write([]byte("msg")); err != io.ErrClosedPipe { 140 | t.Errorf(`log.Write("msg") err mismatch, got %v, want io.ErrClosedPipe`, err) 141 | } 142 | } 143 | 144 | func TestLogLevels(t *testing.T) { 145 | log := NewLogWriter() 146 | cases := []struct { 147 | method func(string) error 148 | output string 149 | }{ 150 | {log.Err, `["log","msg",{"level":"error"}]`}, 151 | {log.Info, `["log","msg",{"level":"info"}]`}, 152 | {log.Debug, `["log","msg",{"level":"debug"}]`}, 153 | } 154 | 155 | for _, testcase := range cases { 156 | th := startTestHost(t, nil) 157 | if err := testcase.method("msg"); err != nil { 158 | t.Errorf("unexpected error: %v", err) 159 | } 160 | output := th.stop() 161 | if output != testcase.output+"\n" { 162 | t.Errorf("wrong JSON output: %s", output) 163 | } 164 | } 165 | } 166 | 167 | func TestConfigVal(t *testing.T) { 168 | th := startTestHost(t, testConfig{ 169 | "a": {"b": "12345678"}, 170 | }) 171 | defer th.stop() 172 | 173 | expVal := th.config["a"]["b"] 174 | val, err := ConfigVal("a", "b") 175 | if err != nil { 176 | t.Fatalf("unexpected error") 177 | } 178 | if val != expVal { 179 | t.Errorf(`ConfigVal("a", "b") got: %q, want: %q`, val, expVal) 180 | } 181 | if th.output.String() != `["get","a","b"]`+"\n" { 182 | t.Errorf("wrong JSON output: %q", th.output.String()) 183 | } 184 | } 185 | 186 | func TestConfigValNotFound(t *testing.T) { 187 | th := startTestHost(t, nil) 188 | defer th.stop() 189 | 190 | val, err := ConfigVal("missing-s", "missing-k") 191 | if val != "" { 192 | t.Errorf(`ConfigVal("missing-s", "missing-k") got: %q, want: ""`, val) 193 | } 194 | if err != ErrNotFound { 195 | t.Errorf(`ConfigVal("missing-s", "missing-k") got err: %v, want: ErrNotFound`, err) 196 | } 197 | } 198 | 199 | func TestConfigValDecodeError(t *testing.T) { 200 | th := startTestHost(t, nil) 201 | defer th.stop() 202 | 203 | _, err := ConfigVal("garbage", "key") 204 | t.Logf("err: %+v", err) 205 | if err == nil { 206 | t.Fatalf("expected error but no error was returned") 207 | } 208 | } 209 | 210 | func TestConfigSection(t *testing.T) { 211 | th := startTestHost(t, testConfig{ 212 | "section1": { 213 | "a": "value-of-a", 214 | "b": "value-of-b", 215 | "c": "value-of-c", 216 | }, 217 | "section2": { 218 | "a": "value-of-a-in-section-2", 219 | }, 220 | }) 221 | defer th.stop() 222 | 223 | expVal := th.config["section1"] 224 | val, err := ConfigSection("section1") 225 | if err != nil { 226 | t.Fatalf(`ConfigSection("section1") returned an error: %v`, err) 227 | } 228 | if !reflect.DeepEqual(val, expVal) { 229 | t.Errorf(`ConfigSection("section1") got: %v, want %v`, val, expVal) 230 | } 231 | if th.output.String() != `["get","section1"]`+"\n" { 232 | t.Errorf("wrong JSON output: %q", th.output.String()) 233 | } 234 | } 235 | 236 | func TestConfigSectionNotFound(t *testing.T) { 237 | th := startTestHost(t, nil) 238 | defer th.stop() 239 | 240 | val, err := ConfigSection("missing") 241 | if val != nil { 242 | t.Errorf(`ConfigSection("missing") got: %q, want: ""`, val) 243 | } 244 | if err != ErrNotFound { 245 | t.Errorf(`ConfigSection("missing") got err: %v, want: ErrNotFound`, err) 246 | } 247 | } 248 | 249 | func TestConfigSectionDecodeError(t *testing.T) { 250 | th := startTestHost(t, nil) 251 | defer th.stop() 252 | 253 | val, err := ConfigSection("garbage") 254 | if err == nil { 255 | t.Errorf(`ConfigSection("garbage") should've returned an error`) 256 | } 257 | if val != nil { 258 | t.Errorf(`ConfigSection("garbage") got: %v, want nil`, val) 259 | } 260 | } 261 | 262 | func TestServerURL(t *testing.T) { 263 | th := startTestHost(t, testConfig{ 264 | "httpd": { 265 | "bind_address": "127.0.0.1", 266 | "port": "5984", 267 | }, 268 | }) 269 | defer th.stop() 270 | 271 | expVal := "http://127.0.0.1:5984/" 272 | respurl, err := ServerURL() 273 | if err != nil { 274 | t.Fatalf("ServerURL() returned error: %v", err) 275 | } 276 | if respurl != expVal { 277 | t.Errorf("ServerURL() mismatch: got %q, want %q", respurl, expVal) 278 | } 279 | } 280 | 281 | func TestExit(t *testing.T) { 282 | th := startTestHost(t, nil) 283 | th.stop() 284 | 285 | select { 286 | case <-th.exitchan: 287 | return 288 | case <-time.After(200 * time.Millisecond): 289 | t.Error("exit func has not been called") 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /couchdb.go: -------------------------------------------------------------------------------- 1 | // Package couchdb implements wrappers for the CouchDB HTTP API. 2 | // 3 | // Unless otherwise noted, all functions in this package 4 | // can be called from more than one goroutine at the same time. 5 | package couchdb 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "errors" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | // Client represents a remote CouchDB server. 18 | type Client struct{ *transport } 19 | 20 | // NewClient creates a new client object. 21 | // 22 | // If rawurl contains credentials, the client will authenticate 23 | // using HTTP Basic Authentication. If rawurl has a query string, 24 | // it is ignored. 25 | // 26 | // The second argument can be nil to use http.Transport, 27 | // which should be good enough in most cases. 28 | func NewClient(rawurl string, rt http.RoundTripper) (*Client, error) { 29 | url, err := url.Parse(rawurl) 30 | if err != nil { 31 | return nil, err 32 | } 33 | url.RawQuery, url.Fragment = "", "" 34 | var auth Auth 35 | if url.User != nil { 36 | passwd, _ := url.User.Password() 37 | auth = BasicAuth(url.User.Username(), passwd) 38 | url.User = nil 39 | } 40 | return &Client{newTransport(url.String(), rt, auth)}, nil 41 | } 42 | 43 | // URL returns the URL prefix of the server. 44 | // The url will not contain a trailing '/'. 45 | func (c *Client) URL() string { 46 | return c.prefix 47 | } 48 | 49 | // Ping can be used to check whether a server is alive. 50 | // It sends an HTTP HEAD request to the server's URL. 51 | func (c *Client) Ping() error { 52 | _, err := c.closedRequest("HEAD", "/", nil) 53 | return err 54 | } 55 | 56 | // SetAuth sets the authentication mechanism used by the client. 57 | // Use SetAuth(nil) to unset any mechanism that might be in use. 58 | // In order to verify the credentials against the server, issue any request 59 | // after the call the SetAuth. 60 | func (c *Client) SetAuth(a Auth) { 61 | c.transport.setAuth(a) 62 | } 63 | 64 | // CreateDB creates a new database. 65 | // The request will fail with status "412 Precondition Failed" if the database 66 | // already exists. A valid DB object is returned in all cases, even if the 67 | // request fails. 68 | func (c *Client) CreateDB(name string) (*DB, error) { 69 | if _, err := c.closedRequest("PUT", dbpath(name), nil); err != nil { 70 | return c.DB(name), err 71 | } 72 | return c.DB(name), nil 73 | } 74 | 75 | // EnsureDB ensures that a database with the given name exists. 76 | func (c *Client) EnsureDB(name string) (*DB, error) { 77 | db, err := c.CreateDB(name) 78 | if err != nil && !ErrorStatus(err, http.StatusPreconditionFailed) { 79 | return nil, err 80 | } 81 | return db, nil 82 | } 83 | 84 | // DeleteDB deletes an existing database. 85 | func (c *Client) DeleteDB(name string) error { 86 | _, err := c.closedRequest("DELETE", dbpath(name), nil) 87 | return err 88 | } 89 | 90 | // AllDBs returns the names of all existing databases. 91 | func (c *Client) AllDBs() (names []string, err error) { 92 | resp, err := c.request("GET", "/_all_dbs", nil) 93 | if err != nil { 94 | return names, err 95 | } 96 | err = readBody(resp, &names) 97 | return names, err 98 | } 99 | 100 | // DB represents a remote CouchDB database. 101 | type DB struct { 102 | *transport 103 | name string 104 | } 105 | 106 | // DB creates a database object. 107 | // The database inherits the authentication and http.RoundTripper 108 | // of the client. The database's actual existence is not verified. 109 | func (c *Client) DB(name string) *DB { 110 | return &DB{c.transport, name} 111 | } 112 | 113 | func (db *DB) path() *pathBuilder { 114 | return new(pathBuilder).add(db.name) 115 | } 116 | 117 | // Name returns the name of a database. 118 | func (db *DB) Name() string { 119 | return db.name 120 | } 121 | 122 | var getJsonKeys = []string{"open_revs", "atts_since"} 123 | 124 | // Get retrieves a document from the given database. 125 | // The document is unmarshalled into the given object. 126 | // Some fields (like _conflicts) will only be returned if the 127 | // options require it. Please refer to the CouchDB HTTP API documentation 128 | // for more information. 129 | // 130 | // http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid 131 | func (db *DB) Get(id string, doc interface{}, opts Options) error { 132 | path, err := db.path().docID(id).options(opts, getJsonKeys) 133 | if err != nil { 134 | return err 135 | } 136 | resp, err := db.request("GET", path, nil) 137 | if err != nil { 138 | return err 139 | } 140 | return readBody(resp, &doc) 141 | } 142 | 143 | // Rev fetches the current revision of a document. 144 | // It is faster than an equivalent Get request because no body 145 | // has to be parsed. 146 | func (db *DB) Rev(id string) (string, error) { 147 | path := db.path().docID(id).path() 148 | return responseRev(db.closedRequest("HEAD", path, nil)) 149 | } 150 | 151 | // Put stores a document into the given database. 152 | func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err error) { 153 | path := db.path().docID(id).rev(rev) 154 | // TODO: make it possible to stream encoder output somehow 155 | json, err := json.Marshal(doc) 156 | if err != nil { 157 | return "", err 158 | } 159 | b := bytes.NewReader(json) 160 | return responseRev(db.closedRequest("PUT", path, b)) 161 | } 162 | 163 | // Delete marks a document revision as deleted. 164 | func (db *DB) Delete(id, rev string) (newrev string, err error) { 165 | path := db.path().docID(id).rev(rev) 166 | return responseRev(db.closedRequest("DELETE", path, nil)) 167 | } 168 | 169 | // Security represents database security objects. 170 | type Security struct { 171 | Admins Members `json:"admins"` 172 | Members Members `json:"members"` 173 | } 174 | 175 | // Members represents member lists in database security objects. 176 | type Members struct { 177 | Names []string `json:"names,omitempty"` 178 | Roles []string `json:"roles,omitempty"` 179 | } 180 | 181 | // Security retrieves the security object of a database. 182 | func (db *DB) Security() (*Security, error) { 183 | secobj := new(Security) 184 | path := db.path().addRaw("_security").path() 185 | resp, err := db.request("GET", path, nil) 186 | if err != nil { 187 | return nil, err 188 | } 189 | // The extra check for io.EOF is there because empty responses are OK. 190 | // CouchDB returns an empty response if no security object has been set. 191 | if err = readBody(resp, secobj); err != nil && err != io.EOF { 192 | return nil, err 193 | } 194 | return secobj, nil 195 | } 196 | 197 | // PutSecurity sets the database security object. 198 | func (db *DB) PutSecurity(secobj *Security) error { 199 | json, _ := json.Marshal(secobj) 200 | body := bytes.NewReader(json) 201 | path := db.path().addRaw("_security").path() 202 | _, err := db.request("PUT", path, body) 203 | return err 204 | } 205 | 206 | var viewJsonKeys = []string{"startkey", "start_key", "key", "endkey", "end_key"} 207 | 208 | // View invokes a view. 209 | // The ddoc parameter must be the full name of the design document 210 | // containing the view definition, including the _design/ prefix. 211 | // 212 | // The output of the query is unmarshalled into the given result. 213 | // The format of the result depends on the options. Please 214 | // refer to the CouchDB HTTP API documentation for all the possible 215 | // options that can be set. 216 | // 217 | // http://docs.couchdb.org/en/latest/api/ddoc/views.html 218 | func (db *DB) View(ddoc, view string, result interface{}, opts Options) error { 219 | if !strings.HasPrefix(ddoc, "_design/") { 220 | return errors.New("couchdb.View: design doc name must start with _design/") 221 | } 222 | path, err := db.path().docID(ddoc).addRaw("_view").add(view).options(opts, viewJsonKeys) 223 | if err != nil { 224 | return err 225 | } 226 | resp, err := db.request("GET", path, nil) 227 | if err != nil { 228 | return err 229 | } 230 | return readBody(resp, &result) 231 | } 232 | 233 | // AllDocs invokes the _all_docs view of a database. 234 | // 235 | // The output of the query is unmarshalled into the given result. 236 | // The format of the result depends on the options. Please 237 | // refer to the CouchDB HTTP API documentation for all the possible 238 | // options that can be set. 239 | // 240 | // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-all-docs 241 | func (db *DB) AllDocs(result interface{}, opts Options) error { 242 | path, err := db.path().addRaw("_all_docs").options(opts, viewJsonKeys) 243 | if err != nil { 244 | return err 245 | } 246 | resp, err := db.request("GET", path, nil) 247 | if err != nil { 248 | return err 249 | } 250 | return readBody(resp, &result) 251 | } 252 | -------------------------------------------------------------------------------- /couchdb_test.go: -------------------------------------------------------------------------------- 1 | package couchdb_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | . "net/http" 8 | "net/url" 9 | "regexp" 10 | "strconv" 11 | "testing" 12 | 13 | "github.com/fjl/go-couchdb" 14 | ) 15 | 16 | type roundTripperFunc func(*Request) (*Response, error) 17 | 18 | func (f roundTripperFunc) RoundTrip(r *Request) (*Response, error) { 19 | return f(r) 20 | } 21 | 22 | func TestNewClient(t *testing.T) { 23 | tests := []struct { 24 | URL string 25 | SetAuth couchdb.Auth 26 | ExpectURL, ExpectAuthHeader string 27 | }{ 28 | // No Auth 29 | { 30 | URL: "http://127.0.0.1:5984/", 31 | ExpectURL: "http://127.0.0.1:5984", 32 | }, 33 | { 34 | URL: "http://hostname:5984/foobar?query=1", 35 | ExpectURL: "http://hostname:5984/foobar", 36 | }, 37 | // Credentials in URL 38 | { 39 | URL: "http://user:password@hostname:5984/", 40 | ExpectURL: "http://hostname:5984", 41 | ExpectAuthHeader: "Basic dXNlcjpwYXNzd29yZA==", 42 | }, 43 | // Credentials in URL and explicit SetAuth, SetAuth credentials win 44 | { 45 | URL: "http://urluser:urlpassword@hostname:5984/", 46 | SetAuth: couchdb.BasicAuth("user", "password"), 47 | ExpectURL: "http://hostname:5984", 48 | ExpectAuthHeader: "Basic dXNlcjpwYXNzd29yZA==", 49 | }, 50 | } 51 | 52 | for i, test := range tests { 53 | rt := roundTripperFunc(func(r *Request) (*Response, error) { 54 | a := r.Header.Get("Authorization") 55 | if a != test.ExpectAuthHeader { 56 | t.Errorf("test %d: auth header mismatch: got %q, want %q", i, a, test.ExpectAuthHeader) 57 | } 58 | return nil, errors.New("nothing to see here, move along") 59 | }) 60 | c, err := couchdb.NewClient(test.URL, rt) 61 | if err != nil { 62 | t.Fatalf("test %d: NewClient returned unexpected error: %v", i, err) 63 | } 64 | if c.URL() != test.ExpectURL { 65 | t.Errorf("test %d: ServerURL mismatch: got %q, want %q", i, c.URL(), test.ExpectURL) 66 | } 67 | if test.SetAuth != nil { 68 | c.SetAuth(test.SetAuth) 69 | } 70 | c.Ping() // trigger round trip 71 | } 72 | } 73 | 74 | func TestServerURL(t *testing.T) { 75 | c := newTestClient(t) 76 | check(t, "c.URL()", "http://testClient:5984", c.URL()) 77 | } 78 | 79 | func TestPing(t *testing.T) { 80 | c := newTestClient(t) 81 | c.Handle("HEAD /", func(resp ResponseWriter, req *Request) {}) 82 | 83 | if err := c.Ping(); err != nil { 84 | t.Fatal(err) 85 | } 86 | } 87 | 88 | func TestCreateDB(t *testing.T) { 89 | c := newTestClient(t) 90 | c.Handle("PUT /db", func(resp ResponseWriter, req *Request) {}) 91 | 92 | db, err := c.CreateDB("db") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | check(t, "db.Name()", "db", db.Name()) 98 | } 99 | 100 | func TestDeleteDB(t *testing.T) { 101 | c := newTestClient(t) 102 | c.Handle("DELETE /db", func(resp ResponseWriter, req *Request) {}) 103 | if err := c.DeleteDB("db"); err != nil { 104 | t.Fatal(err) 105 | } 106 | } 107 | 108 | func TestAllDBs(t *testing.T) { 109 | c := newTestClient(t) 110 | c.Handle("GET /_all_dbs", func(resp ResponseWriter, req *Request) { 111 | io.WriteString(resp, `["a","b","c"]`) 112 | }) 113 | 114 | names, err := c.AllDBs() 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | check(t, "returned names", []string{"a", "b", "c"}, names) 119 | } 120 | 121 | // those are re-used across several tests 122 | var securityObjectJSON = regexp.MustCompile(`\s`).ReplaceAllString( 123 | `{ 124 | "admins": { 125 | "names": ["adminName1", "adminName2"] 126 | }, 127 | "members": { 128 | "names": ["memberName1"], 129 | "roles": ["memberRole1"] 130 | } 131 | }`, "") 132 | var securityObject = &couchdb.Security{ 133 | Admins: couchdb.Members{ 134 | Names: []string{"adminName1", "adminName2"}, 135 | Roles: nil, 136 | }, 137 | Members: couchdb.Members{ 138 | Names: []string{"memberName1"}, 139 | Roles: []string{"memberRole1"}, 140 | }, 141 | } 142 | 143 | func TestSecurity(t *testing.T) { 144 | c := newTestClient(t) 145 | c.Handle("GET /db/_security", func(resp ResponseWriter, req *Request) { 146 | resp.Header().Set("content-length", strconv.Itoa(len(securityObjectJSON))) 147 | io.WriteString(resp, securityObjectJSON) 148 | }) 149 | 150 | secobj, err := c.DB("db").Security() 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | check(t, "secobj", securityObject, secobj) 155 | } 156 | 157 | func TestEmptySecurity(t *testing.T) { 158 | c := newTestClient(t) 159 | c.Handle("GET /db/_security", func(resp ResponseWriter, req *Request) { 160 | // CouchDB returns an empty response if no security object has been set. 161 | resp.Header().Set("content-length", "0") 162 | resp.WriteHeader(200) 163 | }) 164 | 165 | secobj, err := c.DB("db").Security() 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | check(t, "secobj", &couchdb.Security{}, secobj) 170 | } 171 | 172 | func TestPutSecurity(t *testing.T) { 173 | c := newTestClient(t) 174 | c.Handle("PUT /db/_security", func(resp ResponseWriter, req *Request) { 175 | body, _ := ioutil.ReadAll(req.Body) 176 | check(t, "request body", securityObjectJSON, string(body)) 177 | resp.WriteHeader(200) 178 | }) 179 | 180 | err := c.DB("db").PutSecurity(securityObject) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | } 185 | 186 | type testDocument struct { 187 | Rev string `json:"_rev,omitempty"` 188 | Field int64 `json:"field"` 189 | } 190 | 191 | func TestGetExistingDoc(t *testing.T) { 192 | c := newTestClient(t) 193 | c.Handle("GET /db/doc", func(resp ResponseWriter, req *Request) { 194 | io.WriteString(resp, `{ 195 | "_id": "doc", 196 | "_rev": "1-619db7ba8551c0de3f3a178775509611", 197 | "field": 999 198 | }`) 199 | }) 200 | 201 | var doc testDocument 202 | if err := c.DB("db").Get("doc", &doc, nil); err != nil { 203 | t.Fatal(err) 204 | } 205 | check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) 206 | check(t, "doc.Field", int64(999), doc.Field) 207 | } 208 | 209 | func TestGetDesignDoc(t *testing.T) { 210 | c := newTestClient(t) 211 | c.Handle("GET /db/_design/doc", func(resp ResponseWriter, req *Request) { 212 | io.WriteString(resp, `{ 213 | "_id": "doc", 214 | "_rev": "1-619db7ba8551c0de3f3a178775509611" 215 | }`) 216 | }) 217 | 218 | var doc testDocument 219 | if err := c.DB("db").Get("_design/doc", &doc, nil); err != nil { 220 | t.Fatal(err) 221 | } 222 | check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) 223 | } 224 | 225 | func TestGetNonexistingDoc(t *testing.T) { 226 | c := newTestClient(t) 227 | c.Handle("GET /db/doc", func(resp ResponseWriter, req *Request) { 228 | resp.WriteHeader(404) 229 | io.WriteString(resp, `{"error":"not_found","reason":"error reason"}`) 230 | }) 231 | 232 | var doc testDocument 233 | err := c.DB("db").Get("doc", doc, nil) 234 | check(t, "couchdb.NotFound(err)", true, couchdb.NotFound(err)) 235 | } 236 | 237 | func TestRev(t *testing.T) { 238 | c := newTestClient(t) 239 | db := c.DB("db") 240 | c.Handle("HEAD /db/ok", func(resp ResponseWriter, req *Request) { 241 | resp.Header().Set("ETag", `"1-619db7ba8551c0de3f3a178775509611"`) 242 | }) 243 | c.Handle("HEAD /db/404", func(resp ResponseWriter, req *Request) { 244 | NotFound(resp, req) 245 | }) 246 | 247 | rev, err := db.Rev("ok") 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | check(t, "rev", "1-619db7ba8551c0de3f3a178775509611", rev) 252 | 253 | errorRev, err := db.Rev("404") 254 | check(t, "errorRev", "", errorRev) 255 | check(t, "couchdb.NotFound(err)", true, couchdb.NotFound(err)) 256 | if _, ok := err.(*couchdb.Error); !ok { 257 | t.Errorf("expected couchdb.Error, got %#+v", err) 258 | } 259 | } 260 | 261 | func TestRevDBSlash(t *testing.T) { 262 | c := newTestClient(t) 263 | c.Handle("HEAD /test%2Fdb/doc%2Fid", func(resp ResponseWriter, req *Request) { 264 | resp.Header().Set("ETag", `"1-619db7ba8551c0de3f3a178775509611"`) 265 | }) 266 | 267 | db := c.DB("test/db") 268 | rev, err := db.Rev("doc/id") 269 | if err != nil { 270 | t.Fatal(err) 271 | } 272 | check(t, "rev", "1-619db7ba8551c0de3f3a178775509611", rev) 273 | } 274 | 275 | func TestPut(t *testing.T) { 276 | c := newTestClient(t) 277 | c.Handle("PUT /db/doc", func(resp ResponseWriter, req *Request) { 278 | body, _ := ioutil.ReadAll(req.Body) 279 | check(t, "request body", `{"field":999}`, string(body)) 280 | 281 | resp.Header().Set("ETag", `"1-619db7ba8551c0de3f3a178775509611"`) 282 | resp.WriteHeader(StatusCreated) 283 | io.WriteString(resp, `{ 284 | "id": "doc", 285 | "ok": true, 286 | "rev": "1-619db7ba8551c0de3f3a178775509611" 287 | }`) 288 | }) 289 | 290 | doc := &testDocument{Field: 999} 291 | rev, err := c.DB("db").Put("doc", doc, "") 292 | if err != nil { 293 | t.Fatal(err) 294 | } 295 | check(t, "returned rev", "1-619db7ba8551c0de3f3a178775509611", rev) 296 | } 297 | 298 | func TestPutWithRev(t *testing.T) { 299 | c := newTestClient(t) 300 | c.Handle("PUT /db/doc", func(resp ResponseWriter, req *Request) { 301 | check(t, "request query string", 302 | "rev=1-619db7ba8551c0de3f3a178775509611", 303 | req.URL.RawQuery) 304 | 305 | body, _ := ioutil.ReadAll(req.Body) 306 | check(t, "request body", `{"field":999}`, string(body)) 307 | 308 | resp.Header().Set("ETag", `"2-619db7ba8551c0de3f3a178775509611"`) 309 | resp.WriteHeader(StatusCreated) 310 | io.WriteString(resp, `{ 311 | "id": "doc", 312 | "ok": true, 313 | "rev": "2-619db7ba8551c0de3f3a178775509611" 314 | }`) 315 | }) 316 | 317 | doc := &testDocument{Field: 999} 318 | rev, err := c.DB("db").Put("doc", doc, "1-619db7ba8551c0de3f3a178775509611") 319 | if err != nil { 320 | t.Fatal(err) 321 | } 322 | check(t, "returned rev", "2-619db7ba8551c0de3f3a178775509611", rev) 323 | } 324 | 325 | func TestDelete(t *testing.T) { 326 | c := newTestClient(t) 327 | c.Handle("DELETE /db/doc", func(resp ResponseWriter, req *Request) { 328 | check(t, "request query string", 329 | "rev=1-619db7ba8551c0de3f3a178775509611", 330 | req.URL.RawQuery) 331 | 332 | resp.Header().Set("ETag", `"2-619db7ba8551c0de3f3a178775509611"`) 333 | resp.WriteHeader(StatusOK) 334 | io.WriteString(resp, `{ 335 | "id": "doc", 336 | "ok": true, 337 | "rev": "2-619db7ba8551c0de3f3a178775509611" 338 | }`) 339 | }) 340 | 341 | delrev := "1-619db7ba8551c0de3f3a178775509611" 342 | if rev, err := c.DB("db").Delete("doc", delrev); err != nil { 343 | t.Fatal(err) 344 | } else { 345 | check(t, "returned rev", "2-619db7ba8551c0de3f3a178775509611", rev) 346 | } 347 | } 348 | 349 | func TestView(t *testing.T) { 350 | c := newTestClient(t) 351 | c.Handle("GET /db/_design/test/_view/testview", 352 | func(resp ResponseWriter, req *Request) { 353 | expected := url.Values{ 354 | "offset": {"5"}, 355 | "limit": {"100"}, 356 | "reduce": {"false"}, 357 | } 358 | check(t, "request query values", expected, req.URL.Query()) 359 | 360 | io.WriteString(resp, `{ 361 | "offset": 5, 362 | "rows": [ 363 | { 364 | "id": "SpaghettiWithMeatballs", 365 | "key": "meatballs", 366 | "value": 1 367 | }, 368 | { 369 | "id": "SpaghettiWithMeatballs", 370 | "key": "spaghetti", 371 | "value": 1 372 | }, 373 | { 374 | "id": "SpaghettiWithMeatballs", 375 | "key": "tomato sauce", 376 | "value": 1 377 | } 378 | ], 379 | "total_rows": 3 380 | }`) 381 | }) 382 | 383 | type row struct { 384 | ID, Key string 385 | Value int 386 | } 387 | type testviewResult struct { 388 | TotalRows int `json:"total_rows"` 389 | Offset int 390 | Rows []row 391 | } 392 | 393 | var result testviewResult 394 | err := c.DB("db").View("_design/test", "testview", &result, couchdb.Options{ 395 | "offset": 5, 396 | "limit": 100, 397 | "reduce": false, 398 | }) 399 | if err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | expected := testviewResult{ 404 | TotalRows: 3, 405 | Offset: 5, 406 | Rows: []row{ 407 | {"SpaghettiWithMeatballs", "meatballs", 1}, 408 | {"SpaghettiWithMeatballs", "spaghetti", 1}, 409 | {"SpaghettiWithMeatballs", "tomato sauce", 1}, 410 | }, 411 | } 412 | check(t, "result", expected, result) 413 | } 414 | 415 | func TestAllDocs(t *testing.T) { 416 | c := newTestClient(t) 417 | c.Handle("GET /db/_all_docs", 418 | func(resp ResponseWriter, req *Request) { 419 | expected := url.Values{ 420 | "offset": {"5"}, 421 | "limit": {"100"}, 422 | "startkey": {"[\"Zingylemontart\",\"Yogurtraita\"]"}, 423 | } 424 | check(t, "request query values", expected, req.URL.Query()) 425 | 426 | io.WriteString(resp, `{ 427 | "total_rows": 2666, 428 | "rows": [ 429 | { 430 | "value": { 431 | "rev": "1-a3544d296de19e6f5b932ea77d886942" 432 | }, 433 | "id": "Zingylemontart", 434 | "key": "Zingylemontart" 435 | }, 436 | { 437 | "value": { 438 | "rev": "1-91635098bfe7d40197a1b98d7ee085fc" 439 | }, 440 | "id": "Yogurtraita", 441 | "key": "Yogurtraita" 442 | } 443 | ], 444 | "offset" : 5 445 | }`) 446 | }) 447 | 448 | type alldocsResult struct { 449 | TotalRows int `json:"total_rows"` 450 | Offset int 451 | Rows []map[string]interface{} 452 | } 453 | 454 | var result alldocsResult 455 | err := c.DB("db").AllDocs(&result, couchdb.Options{ 456 | "offset": 5, 457 | "limit": 100, 458 | "startkey": []string{"Zingylemontart", "Yogurtraita"}, 459 | }) 460 | if err != nil { 461 | t.Fatal(err) 462 | } 463 | 464 | expected := alldocsResult{ 465 | TotalRows: 2666, 466 | Offset: 5, 467 | Rows: []map[string]interface{}{ 468 | { 469 | "key": "Zingylemontart", 470 | "id": "Zingylemontart", 471 | "value": map[string]interface{}{ 472 | "rev": "1-a3544d296de19e6f5b932ea77d886942", 473 | }, 474 | }, 475 | { 476 | "key": "Yogurtraita", 477 | "id": "Yogurtraita", 478 | "value": map[string]interface{}{ 479 | "rev": "1-91635098bfe7d40197a1b98d7ee085fc", 480 | }, 481 | }, 482 | }, 483 | } 484 | check(t, "result", expected, result) 485 | } 486 | -------------------------------------------------------------------------------- /feeds.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // DBUpdatesFeed is an iterator for the _db_updates feed. 10 | // This feed receives an event whenever any database is created, updated 11 | // or deleted. On each call to the Next method, the event fields are updated 12 | // for the current event. 13 | // 14 | // feed, err := client.DbUpdates(nil) 15 | // ... 16 | // for feed.Next() { 17 | // fmt.Printf("changed: %s %s", feed.Event, feed.Db) 18 | // } 19 | // err = feed.Err() 20 | // ... 21 | type DBUpdatesFeed struct { 22 | Event string `json:"type"` // "created" | "updated" | "deleted" 23 | DB string `json:"db_name"` // Event database name 24 | Seq interface{} `json:"seq"` // DB update sequence of the event. 25 | OK bool `json:"ok"` // Event operation status (deprecated) 26 | 27 | end bool 28 | err error 29 | conn io.Closer 30 | dec *json.Decoder 31 | } 32 | 33 | // DBUpdates opens the _db_updates feed. 34 | // For the possible options, please see the CouchDB documentation. 35 | // Pleas note that the "feed" option is currently always set to "continuous". 36 | // 37 | // http://docs.couchdb.org/en/latest/api/server/common.html#db-updates 38 | func (c *Client) DBUpdates(options Options) (*DBUpdatesFeed, error) { 39 | newopts := options.clone() 40 | newopts["feed"] = "continuous" 41 | path, err := new(pathBuilder).addRaw("_db_updates").options(newopts, nil) 42 | if err != nil { 43 | return nil, err 44 | } 45 | resp, err := c.request("GET", path, nil) 46 | if err != nil { 47 | return nil, err 48 | } 49 | feed := &DBUpdatesFeed{ 50 | conn: resp.Body, 51 | dec: json.NewDecoder(resp.Body), 52 | } 53 | return feed, nil 54 | } 55 | 56 | // Next decodes the next event in a _db_updates feed. It returns false when 57 | // the feeds end has been reached or an error has occurred. 58 | func (f *DBUpdatesFeed) Next() bool { 59 | if f.end { 60 | return false 61 | } 62 | f.Event, f.DB, f.Seq, f.OK = "", "", nil, false 63 | if f.err = f.dec.Decode(f); f.err != nil { 64 | if f.err == io.EOF { 65 | f.err = nil 66 | } 67 | f.Close() 68 | } 69 | return !f.end 70 | } 71 | 72 | // Err returns the last error that occurred during iteration. 73 | func (f *DBUpdatesFeed) Err() error { 74 | return f.err 75 | } 76 | 77 | // Close terminates the connection of a feed. 78 | func (f *DBUpdatesFeed) Close() error { 79 | f.end = true 80 | return f.conn.Close() 81 | } 82 | 83 | // ChangesFeed is an iterator for the _changes feed of a database. 84 | // On each call to the Next method, the event fields are updated 85 | // for the current event. Next is designed to be used in a for loop: 86 | // 87 | // feed, err := client.Changes("db", nil) 88 | // ... 89 | // for feed.Next() { 90 | // fmt.Printf("changed: %s", feed.ID) 91 | // } 92 | // err = feed.Err() 93 | // ... 94 | type ChangesFeed struct { 95 | // DB is the database. Since all events in a _changes feed 96 | // belong to the same database, this field is always equivalent to the 97 | // database from the DB.Changes call that created the feed object 98 | DB *DB `json:"-"` 99 | 100 | // ID is the document ID of the current event. 101 | ID string `json:"id"` 102 | 103 | // Deleted is true when the event represents a deleted document. 104 | Deleted bool `json:"deleted"` 105 | 106 | // Seq is the database update sequence number of the current event. 107 | // This is usually a string, but may also be a number for couchdb 0.x servers. 108 | // 109 | // For poll-style feeds (feed modes "normal", "longpoll"), this is set to the 110 | // last_seq value sent by CouchDB after all feed rows have been read. 111 | Seq interface{} `json:"seq"` 112 | 113 | // Pending is the count of remaining items in the feed. This is set for poll-style 114 | // feeds (feed modes "normal", "longpoll") after the last element has been 115 | // processed. 116 | Pending int64 `json:"pending"` 117 | 118 | // Changes is the list of the document's leaf revisions. 119 | Changes []struct { 120 | Rev string `json:"rev"` 121 | } `json:"changes"` 122 | 123 | // The document. This is populated only if the feed option 124 | // "include_docs" is true. 125 | Doc json.RawMessage `json:"doc"` 126 | 127 | end bool 128 | err error 129 | conn io.Closer 130 | parser func() error 131 | } 132 | 133 | // changesRow is the JSON structure of a changes feed row. 134 | type changesRow struct { 135 | ID string `json:"id"` 136 | Deleted bool `json:"deleted"` 137 | Seq interface{} `json:"seq"` 138 | Changes []struct { 139 | Rev string `json:"rev"` 140 | } `json:"changes"` 141 | Doc json.RawMessage `json:"doc"` 142 | LastSeq bool `json:"last_seq"` 143 | } 144 | 145 | // apply sets the row as the current event of the feed. 146 | func (d *changesRow) apply(f *ChangesFeed) error { 147 | f.Seq = d.Seq 148 | f.ID = d.ID 149 | f.Deleted = d.Deleted 150 | f.Doc = d.Doc 151 | f.Changes = d.Changes 152 | return nil 153 | } 154 | 155 | // reset resets the iterator outputs to zero. 156 | func (f *ChangesFeed) reset() { 157 | f.ID, f.Deleted, f.Changes, f.Doc = "", false, nil, nil 158 | } 159 | 160 | // Changes opens the _changes feed of a database. This feed receives an event 161 | // whenever a document is created, updated or deleted. 162 | // 163 | // The implementation supports both poll-style and continuous feeds. 164 | // The default feed mode is "normal", which retrieves changes up to some point 165 | // and then closes the feed. If you want a never-ending feed, set the "feed" 166 | // option to "continuous": 167 | // 168 | // feed, err := client.Changes("db", couchdb.Options{"feed": "continuous"}) 169 | // 170 | // There are many other options that allow you to customize what the 171 | // feed returns. For information on all of them, see the official CouchDB 172 | // documentation: 173 | // 174 | // http://docs.couchdb.org/en/latest/api/database/changes.html#db-changes 175 | func (db *DB) Changes(options Options) (*ChangesFeed, error) { 176 | path, err := db.path().addRaw("_changes").options(options, nil) 177 | if err != nil { 178 | return nil, err 179 | } 180 | resp, err := db.request("GET", path, nil) 181 | if err != nil { 182 | return nil, err 183 | } 184 | feed := &ChangesFeed{DB: db, conn: resp.Body} 185 | 186 | switch options["feed"] { 187 | case nil, "normal", "longpoll": 188 | feed.parser, err = feed.pollParser(resp.Body) 189 | if err != nil { 190 | feed.Close() 191 | return nil, err 192 | } 193 | case "continuous": 194 | feed.parser = feed.contParser(resp.Body) 195 | default: 196 | err := fmt.Errorf(`couchdb: unsupported value for option "feed": %#v`, options["feed"]) 197 | feed.Close() 198 | return nil, err 199 | } 200 | 201 | return feed, nil 202 | } 203 | 204 | // Next decodes the next event. It returns false when the feeds end has been 205 | // reached or an error has occurred. 206 | func (f *ChangesFeed) Next() bool { 207 | if f.end { 208 | return false 209 | } 210 | if f.err = f.parser(); f.err != nil || f.end { 211 | f.Close() 212 | } 213 | return !f.end 214 | } 215 | 216 | // Err returns the last error that occurred during iteration. 217 | func (f *ChangesFeed) Err() error { 218 | return f.err 219 | } 220 | 221 | // Close terminates the connection of the feed. 222 | // If Next returns false, the feed has already been closed. 223 | func (f *ChangesFeed) Close() error { 224 | f.end = true 225 | return f.conn.Close() 226 | } 227 | 228 | // ChangesRevs returns the rev list of the current result row. 229 | func (f *ChangesFeed) ChangesRevs() []string { 230 | revs := make([]string, len(f.Changes)) 231 | for i, x := range f.Changes { 232 | revs[i] = x.Rev 233 | } 234 | return revs 235 | } 236 | 237 | func (f *ChangesFeed) contParser(r io.Reader) func() error { 238 | dec := json.NewDecoder(r) 239 | return func() error { 240 | var row changesRow 241 | if err := dec.Decode(&row); err != nil { 242 | return err 243 | } 244 | if err := row.apply(f); err != nil { 245 | return err 246 | } 247 | if row.LastSeq { 248 | f.end = true 249 | return nil 250 | } 251 | return nil 252 | } 253 | } 254 | 255 | func (f *ChangesFeed) pollParser(r io.Reader) (func() error, error) { 256 | dec := json.NewDecoder(r) 257 | if err := expectTokens(dec, json.Delim('{'), "results", json.Delim('[')); err != nil { 258 | return nil, err 259 | } 260 | 261 | next := func() error { 262 | f.reset() 263 | 264 | // Decode next row. 265 | if dec.More() { 266 | var row changesRow 267 | if err := dec.Decode(&row); err != nil { 268 | return err 269 | } 270 | return row.apply(f) 271 | } 272 | 273 | // End of results reached, decode trailing object keys. 274 | if err := expectTokens(dec, json.Delim(']')); err != nil { 275 | return err 276 | } 277 | f.end = true 278 | for dec.More() { 279 | key, err := dec.Token() 280 | if err != nil { 281 | return err 282 | } 283 | switch key { 284 | case "last_seq": 285 | if err := dec.Decode(&f.Seq); err != nil { 286 | return fmt.Errorf(`can't decode "last_seq" feed key: %v`, err) 287 | } 288 | case "pending": 289 | if err := dec.Decode(&f.Pending); err != nil { 290 | return fmt.Errorf(`can't decode "pending" feed key: %v`, err) 291 | } 292 | default: 293 | if err := skipValue(dec); err != nil { 294 | return fmt.Errorf(`can't skip over %q feed key: %v`, key, err) 295 | } 296 | } 297 | } 298 | return nil 299 | } 300 | return next, nil 301 | } 302 | 303 | // tokens verifies that the given tokens are present in the 304 | // input stream. Whitespace between tokens is skipped. 305 | func expectTokens(dec *json.Decoder, toks ...json.Token) error { 306 | for _, tok := range toks { 307 | tokin, err := dec.Token() 308 | if err != nil { 309 | return err 310 | } 311 | if tokin != tok { 312 | return fmt.Errorf("unexpected token: found %v, want %v", tokin, tok) 313 | } 314 | } 315 | return nil 316 | } 317 | 318 | // skipValue skips over the next JSON value in the decoder. 319 | func skipValue(dec *json.Decoder) error { 320 | firstDelim, err := nextDelim(dec) 321 | if err != nil || firstDelim == 0 { 322 | // If the value is not an object or array, we're done skipping it. 323 | return err 324 | } 325 | var nesting = 1 326 | for nesting > 0 { 327 | d, err := nextDelim(dec) 328 | if err != nil { 329 | return err 330 | } 331 | switch d { 332 | case '{', '[': 333 | nesting++ 334 | case '}', ']': 335 | nesting-- 336 | default: 337 | // just skip 338 | } 339 | } 340 | return nil 341 | } 342 | 343 | // nextDelim decodes the next token and returns it as a delimiter. 344 | // If the token is not a delimiter, it returns zero. 345 | func nextDelim(dec *json.Decoder) (json.Delim, error) { 346 | tok, err := dec.Token() 347 | if err != nil { 348 | return 0, err 349 | } 350 | d, _ := tok.(json.Delim) 351 | return d, nil 352 | } 353 | -------------------------------------------------------------------------------- /feeds_test.go: -------------------------------------------------------------------------------- 1 | package couchdb_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | . "net/http" 7 | "testing" 8 | 9 | "github.com/fjl/go-couchdb" 10 | ) 11 | 12 | func TestDBUpdatesFeed(t *testing.T) { 13 | c := newTestClient(t) 14 | c.Handle("GET /_db_updates", func(resp ResponseWriter, req *Request) { 15 | check(t, "request query string", "feed=continuous", req.URL.RawQuery) 16 | io.WriteString(resp, `{ 17 | "db_name": "db", 18 | "seq": "1-...", 19 | "type": "created" 20 | }`+"\n") 21 | io.WriteString(resp, `{ 22 | "db_name": "db2", 23 | "seq": "4-...", 24 | "type": "deleted" 25 | }`+"\n") 26 | }) 27 | 28 | feed, err := c.DBUpdates(nil) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | t.Log("-- first event") 34 | check(t, "feed.Next()", true, feed.Next()) 35 | check(t, "feed.Err", error(nil), feed.Err()) 36 | check(t, "feed.DB", "db", feed.DB) 37 | check(t, "feed.Event", "created", feed.Event) 38 | check(t, "feed.Seq", "1-...", feed.Seq) 39 | 40 | t.Log("-- second event") 41 | check(t, "feed.Next()", true, feed.Next()) 42 | check(t, "feed.Err", error(nil), feed.Err()) 43 | check(t, "feed.DB", "db2", feed.DB) 44 | check(t, "feed.Event", "deleted", feed.Event) 45 | check(t, "feed.Seq", "4-...", feed.Seq) 46 | 47 | t.Log("-- end of feed") 48 | check(t, "feed.Next()", false, feed.Next()) 49 | check(t, "feed.Err()", error(nil), feed.Err()) 50 | check(t, "feed.DB", "", feed.DB) 51 | check(t, "feed.Event", "", feed.Event) 52 | check(t, "feed.Seq", nil, feed.Seq) 53 | check(t, "feed.OK", false, feed.OK) 54 | 55 | if err := feed.Close(); err != nil { 56 | t.Fatalf("feed.Close err: %v", err) 57 | } 58 | } 59 | 60 | // This test checks that the poll parser skips over unexpected object 61 | // keys at the end of feed data. 62 | func TestChangesFeedPoll_UnexpectedKeys(t *testing.T) { 63 | c := newTestClient(t) 64 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 65 | check(t, "request query string", "", req.URL.RawQuery) 66 | io.WriteString(resp, `{ 67 | "results": [ 68 | ], 69 | "last_seq": "99-...", "foobar": {"x": [1, "y"]}, "pending": 1 70 | }`) 71 | }) 72 | feed, err := c.DB("db").Changes(nil) 73 | if err != nil { 74 | t.Fatalf("client.Changes error: %v", err) 75 | } 76 | 77 | t.Log("-- end of feed") 78 | check(t, "feed.Next()", false, feed.Next()) 79 | check(t, "feed.Err()", error(nil), feed.Err()) 80 | check(t, "feed.Seq", "99-...", feed.Seq) 81 | check(t, "feed.Pending", int64(1), feed.Pending) 82 | } 83 | 84 | func TestChangesFeedPoll_Doc(t *testing.T) { 85 | c := newTestClient(t) 86 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 87 | check(t, "request query string", "include_docs=true", req.URL.RawQuery) 88 | io.WriteString(resp, `{ 89 | "results": [ 90 | { 91 | "seq": "1-...", 92 | "id": "doc", 93 | "doc": {"x": "y"}, 94 | "deleted": true, 95 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 96 | } 97 | ], 98 | "last_seq": "99-..." 99 | }`) 100 | }) 101 | opt := couchdb.Options{"include_docs": true} 102 | feed, err := c.DB("db").Changes(opt) 103 | if err != nil { 104 | t.Fatalf("client.Changes error: %v", err) 105 | } 106 | 107 | t.Log("-- first event") 108 | check(t, "feed.Next()", true, feed.Next()) 109 | check(t, "feed.Err()", error(nil), feed.Err()) 110 | check(t, "feed.ID", "doc", feed.ID) 111 | check(t, "feed.Seq", "1-...", feed.Seq) 112 | check(t, "feed.Deleted", true, feed.Deleted) 113 | check(t, "feed.Doc", json.RawMessage(`{"x": "y"}`), feed.Doc) 114 | check(t, "feed.ChangesRevs", []string{"1-619db7ba8551c0de3f3a178775509611"}, feed.ChangesRevs()) 115 | 116 | t.Log("-- end of feed") 117 | check(t, "feed.Next()", false, feed.Next()) 118 | check(t, "feed.Err()", error(nil), feed.Err()) 119 | check(t, "feed.ID", "", feed.ID) 120 | check(t, "feed.Seq", "99-...", feed.Seq) 121 | check(t, "feed.Deleted", false, feed.Deleted) 122 | 123 | if err := feed.Close(); err != nil { 124 | t.Fatalf("feed.Close error: %v", err) 125 | } 126 | } 127 | 128 | func TestChangesFeedPoll_SeqInteger(t *testing.T) { 129 | c := newTestClient(t) 130 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 131 | check(t, "request query string", "", req.URL.RawQuery) 132 | io.WriteString(resp, `{ 133 | "results": [ 134 | { 135 | "seq": 1, 136 | "id": "doc", 137 | "deleted": true, 138 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 139 | }, 140 | { 141 | "seq": 2, 142 | "id": "doc", 143 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 144 | } 145 | ], 146 | "last_seq": 99 147 | }`) 148 | }) 149 | 150 | feed, err := c.DB("db").Changes(nil) 151 | if err != nil { 152 | t.Fatalf("client.Changes error: %v", err) 153 | } 154 | 155 | t.Log("-- first event") 156 | check(t, "feed.Next()", true, feed.Next()) 157 | check(t, "feed.Err()", error(nil), feed.Err()) 158 | check(t, "feed.ID", "doc", feed.ID) 159 | check(t, "feed.Seq", float64(1), feed.Seq) 160 | check(t, "feed.Deleted", true, feed.Deleted) 161 | 162 | t.Log("-- second event") 163 | check(t, "feed.Next()", true, feed.Next()) 164 | check(t, "feed.Err()", error(nil), feed.Err()) 165 | check(t, "feed.ID", "doc", feed.ID) 166 | check(t, "feed.Seq", float64(2), feed.Seq) 167 | check(t, "feed.Deleted", false, feed.Deleted) 168 | 169 | t.Log("-- end of feed") 170 | check(t, "feed.Next()", false, feed.Next()) 171 | check(t, "feed.Err()", error(nil), feed.Err()) 172 | check(t, "feed.ID", "", feed.ID) 173 | check(t, "feed.Seq", float64(99), feed.Seq) 174 | check(t, "feed.Deleted", false, feed.Deleted) 175 | 176 | if err := feed.Close(); err != nil { 177 | t.Fatalf("feed.Close error: %v", err) 178 | } 179 | } 180 | 181 | func TestChangesFeedPoll_SeqString(t *testing.T) { 182 | c := newTestClient(t) 183 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 184 | check(t, "request query string", "", req.URL.RawQuery) 185 | io.WriteString(resp, `{ 186 | "results": [ 187 | { 188 | "seq": "1-...", 189 | "id": "doc", 190 | "deleted": true, 191 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 192 | }, 193 | { 194 | "seq": "2-...", 195 | "id": "doc", 196 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 197 | } 198 | ], 199 | "last_seq": "99-..." 200 | }`) 201 | }) 202 | 203 | feed, err := c.DB("db").Changes(nil) 204 | if err != nil { 205 | t.Fatalf("client.Changes error: %v", err) 206 | } 207 | 208 | t.Log("-- first event") 209 | check(t, "feed.Next()", true, feed.Next()) 210 | check(t, "feed.Err()", error(nil), feed.Err()) 211 | check(t, "feed.ID", "doc", feed.ID) 212 | check(t, "feed.Seq", "1-...", feed.Seq) 213 | check(t, "feed.Deleted", true, feed.Deleted) 214 | 215 | t.Log("-- second event") 216 | check(t, "feed.Next()", true, feed.Next()) 217 | check(t, "feed.Err()", error(nil), feed.Err()) 218 | check(t, "feed.ID", "doc", feed.ID) 219 | check(t, "feed.Seq", "2-...", feed.Seq) 220 | check(t, "feed.Deleted", false, feed.Deleted) 221 | 222 | t.Log("-- end of feed") 223 | check(t, "feed.Next()", false, feed.Next()) 224 | check(t, "feed.Err()", error(nil), feed.Err()) 225 | check(t, "feed.ID", "", feed.ID) 226 | check(t, "feed.Seq", "99-...", feed.Seq) 227 | check(t, "feed.Deleted", false, feed.Deleted) 228 | 229 | if err := feed.Close(); err != nil { 230 | t.Fatalf("feed.Close error: %v", err) 231 | } 232 | } 233 | 234 | func TestChangesFeedCont_SeqInteger(t *testing.T) { 235 | c := newTestClient(t) 236 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 237 | check(t, "request query string", "feed=continuous", req.URL.RawQuery) 238 | io.WriteString(resp, `{ 239 | "seq": 1, 240 | "id": "doc", 241 | "deleted": true, 242 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 243 | }`+"\n") 244 | io.WriteString(resp, `{ 245 | "seq": 2, 246 | "id": "doc", 247 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 248 | }`+"\n") 249 | io.WriteString(resp, `{ 250 | "seq": 99, 251 | "last_seq": true 252 | }`+"\n") 253 | }) 254 | 255 | feed, err := c.DB("db").Changes(couchdb.Options{"feed": "continuous"}) 256 | if err != nil { 257 | t.Fatalf("client.Changes error: %v", err) 258 | } 259 | 260 | t.Log("-- first event") 261 | check(t, "feed.Next()", true, feed.Next()) 262 | check(t, "feed.Err()", error(nil), feed.Err()) 263 | check(t, "feed.ID", "doc", feed.ID) 264 | check(t, "feed.Seq", float64(1), feed.Seq) 265 | check(t, "feed.Deleted", true, feed.Deleted) 266 | 267 | t.Log("-- second event") 268 | check(t, "feed.Next()", true, feed.Next()) 269 | check(t, "feed.Err()", error(nil), feed.Err()) 270 | check(t, "feed.ID", "doc", feed.ID) 271 | check(t, "feed.Seq", float64(2), feed.Seq) 272 | check(t, "feed.Deleted", false, feed.Deleted) 273 | 274 | t.Log("-- end of feed") 275 | check(t, "feed.Next()", false, feed.Next()) 276 | check(t, "feed.Err()", error(nil), feed.Err()) 277 | check(t, "feed.ID", "", feed.ID) 278 | check(t, "feed.Seq", float64(99), feed.Seq) 279 | check(t, "feed.Deleted", false, feed.Deleted) 280 | 281 | if err := feed.Close(); err != nil { 282 | t.Fatalf("feed.Close error: %v", err) 283 | } 284 | } 285 | 286 | func TestChangesFeedCont_Doc(t *testing.T) { 287 | c := newTestClient(t) 288 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 289 | check(t, "request query string", "feed=continuous&include_docs=true", req.URL.RawQuery) 290 | io.WriteString(resp, `{ 291 | "seq": "1-...", 292 | "id": "doc", 293 | "doc": {"x": "y"}, 294 | "deleted": true, 295 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 296 | }`+"\n") 297 | io.WriteString(resp, `{ 298 | "seq": "99-...", 299 | "last_seq": true 300 | }`+"\n") 301 | }) 302 | 303 | opt := couchdb.Options{"include_docs": true, "feed": "continuous"} 304 | feed, err := c.DB("db").Changes(opt) 305 | if err != nil { 306 | t.Fatalf("client.Changes error: %v", err) 307 | } 308 | 309 | t.Log("-- first event") 310 | check(t, "feed.Next()", true, feed.Next()) 311 | check(t, "feed.Err()", error(nil), feed.Err()) 312 | check(t, "feed.ID", "doc", feed.ID) 313 | check(t, "feed.Seq", "1-...", feed.Seq) 314 | check(t, "feed.Deleted", true, feed.Deleted) 315 | check(t, "feed.Doc", json.RawMessage(`{"x": "y"}`), feed.Doc) 316 | check(t, "feed.ChangesRevs", []string{"1-619db7ba8551c0de3f3a178775509611"}, feed.ChangesRevs()) 317 | 318 | t.Log("-- end of feed") 319 | check(t, "feed.Next()", false, feed.Next()) 320 | check(t, "feed.Err()", error(nil), feed.Err()) 321 | check(t, "feed.ID", "", feed.ID) 322 | check(t, "feed.Seq", "99-...", feed.Seq) 323 | check(t, "feed.Deleted", false, feed.Deleted) 324 | 325 | if err := feed.Close(); err != nil { 326 | t.Fatalf("feed.Close error: %v", err) 327 | } 328 | } 329 | 330 | func TestChangesFeedCont_SeqString(t *testing.T) { 331 | c := newTestClient(t) 332 | c.Handle("GET /db/_changes", func(resp ResponseWriter, req *Request) { 333 | check(t, "request query string", "feed=continuous", req.URL.RawQuery) 334 | io.WriteString(resp, `{ 335 | "seq": "1-...", 336 | "id": "doc", 337 | "deleted": true, 338 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 339 | }`+"\n") 340 | io.WriteString(resp, `{ 341 | "seq": "2-...", 342 | "id": "doc", 343 | "changes": [{"rev":"1-619db7ba8551c0de3f3a178775509611"}] 344 | }`+"\n") 345 | io.WriteString(resp, `{ 346 | "seq": "99-...", 347 | "last_seq": true 348 | }`+"\n") 349 | }) 350 | 351 | feed, err := c.DB("db").Changes(couchdb.Options{"feed": "continuous"}) 352 | if err != nil { 353 | t.Fatalf("client.Changes error: %v", err) 354 | } 355 | 356 | t.Log("-- first event") 357 | check(t, "feed.Next()", true, feed.Next()) 358 | check(t, "feed.Err()", error(nil), feed.Err()) 359 | check(t, "feed.ID", "doc", feed.ID) 360 | check(t, "feed.Seq", "1-...", feed.Seq) 361 | check(t, "feed.Deleted", true, feed.Deleted) 362 | 363 | t.Log("-- second event") 364 | check(t, "feed.Next()", true, feed.Next()) 365 | check(t, "feed.Err()", error(nil), feed.Err()) 366 | check(t, "feed.ID", "doc", feed.ID) 367 | check(t, "feed.Seq", "2-...", feed.Seq) 368 | check(t, "feed.Deleted", false, feed.Deleted) 369 | 370 | t.Log("-- end of feed") 371 | check(t, "feed.Next()", false, feed.Next()) 372 | check(t, "feed.Err()", error(nil), feed.Err()) 373 | check(t, "feed.ID", "", feed.ID) 374 | check(t, "feed.Seq", "99-...", feed.Seq) 375 | check(t, "feed.Deleted", false, feed.Deleted) 376 | 377 | if err := feed.Close(); err != nil { 378 | t.Fatalf("feed.Close error: %v", err) 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fjl/go-couchdb 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "reflect" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | // Options represents CouchDB query string parameters. 19 | type Options map[string]interface{} 20 | 21 | // clone creates a shallow copy of an Options map 22 | func (opts Options) clone() (result Options) { 23 | result = make(Options) 24 | for k, v := range opts { 25 | result[k] = v 26 | } 27 | return 28 | } 29 | 30 | type transport struct { 31 | prefix string // URL prefix 32 | http *http.Client 33 | mu sync.RWMutex 34 | auth Auth 35 | } 36 | 37 | func newTransport(prefix string, rt http.RoundTripper, auth Auth) *transport { 38 | return &transport{ 39 | prefix: strings.TrimRight(prefix, "/"), 40 | http: &http.Client{Transport: rt}, 41 | auth: auth, 42 | } 43 | } 44 | 45 | func (t *transport) setAuth(a Auth) { 46 | t.mu.Lock() 47 | t.auth = a 48 | t.mu.Unlock() 49 | } 50 | 51 | func (t *transport) newRequest(method, path string, body io.Reader) (*http.Request, error) { 52 | req, err := http.NewRequest(method, t.prefix+path, body) 53 | if err != nil { 54 | return nil, err 55 | } 56 | t.mu.RLock() 57 | defer t.mu.RUnlock() 58 | if t.auth != nil { 59 | t.auth.AddAuth(req) 60 | } 61 | return req, nil 62 | } 63 | 64 | // request sends an HTTP request to a CouchDB server. 65 | // The request URL is constructed from the server's 66 | // prefix and the given path, which may contain an 67 | // encoded query string. 68 | // 69 | // Status codes >= 400 are treated as errors. 70 | func (t *transport) request(method, path string, body io.Reader) (*http.Response, error) { 71 | req, err := t.newRequest(method, path, body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if body != nil { 76 | req.Header.Set("content-type", "application/json") 77 | } 78 | 79 | resp, err := t.http.Do(req) 80 | if err != nil { 81 | return nil, err 82 | } else if resp.StatusCode >= 400 { 83 | return nil, parseError(req, resp) // the Body is closed by parseError 84 | } else { 85 | return resp, nil 86 | } 87 | } 88 | 89 | // closedRequest sends an HTTP request and discards the response body. 90 | func (t *transport) closedRequest(method, path string, body io.Reader) (*http.Response, error) { 91 | resp, err := t.request(method, path, body) 92 | if err == nil { 93 | resp.Body.Close() 94 | } 95 | return resp, err 96 | } 97 | 98 | // pathBuilder assists with constructing CouchDB request paths. 99 | type pathBuilder struct { 100 | buf bytes.Buffer 101 | inQuery bool 102 | } 103 | 104 | // dbpath returns the root path to a database. 105 | func dbpath(name string) string { 106 | // TODO: would be nice to use url.PathEscape here, 107 | // but it only became available in Go 1.8. 108 | return "/" + url.QueryEscape(name) 109 | } 110 | 111 | // path returns the built path. 112 | func (p *pathBuilder) path() string { 113 | return p.buf.String() 114 | } 115 | 116 | func (p *pathBuilder) checkNotInQuery() { 117 | if p.inQuery { 118 | panic("can't add path elements after query string") 119 | } 120 | } 121 | 122 | // docID adds a document ID to the path. 123 | func (p *pathBuilder) docID(id string) *pathBuilder { 124 | p.checkNotInQuery() 125 | 126 | if len(id) > 0 && id[0] != '_' { 127 | // Normal document IDs can't start with _, only 'reserved' document IDs can. 128 | p.add(id) 129 | return p 130 | } 131 | // However, it is still useful to be able to retrieve reserved documents such as 132 | // design documents (path: _design/doc). Avoid escaping the first '/', but do escape 133 | // anything after that. 134 | slash := strings.IndexByte(id, '/') 135 | if slash == -1 { 136 | p.add(id) 137 | return p 138 | } 139 | p.addRaw(id[:slash]) 140 | p.add(id[slash+1:]) 141 | return p 142 | } 143 | 144 | // add adds a segment to the path. 145 | func (p *pathBuilder) add(segment string) *pathBuilder { 146 | p.checkNotInQuery() 147 | p.buf.WriteByte('/') 148 | // TODO: would be nice to use url.PathEscape here, 149 | // but it only became available in Go 1.8. 150 | p.buf.WriteString(url.QueryEscape(segment)) 151 | return p 152 | } 153 | 154 | // addRaw adds an unescaped segment to the path. 155 | func (p *pathBuilder) addRaw(path string) *pathBuilder { 156 | p.checkNotInQuery() 157 | p.buf.WriteByte('/') 158 | p.buf.WriteString(path) 159 | return p 160 | } 161 | 162 | // rev adds a revision to the query string. 163 | // It returns the built path. 164 | func (p *pathBuilder) rev(rev string) string { 165 | p.checkNotInQuery() 166 | p.inQuery = true 167 | if rev != "" { 168 | p.buf.WriteString("?rev=") 169 | p.buf.WriteString(url.QueryEscape(rev)) 170 | } 171 | return p.path() 172 | } 173 | 174 | // options encodes the given options to the query. 175 | func (p *pathBuilder) options(opts Options, jskeys []string) (string, error) { 176 | p.checkNotInQuery() 177 | p.inQuery = true 178 | 179 | // Sort keys by name. 180 | var keys = make([]string, len(opts)) 181 | var i int 182 | for k := range opts { 183 | keys[i] = k 184 | i++ 185 | } 186 | sort.Strings(keys) 187 | 188 | // Encode to query string. 189 | p.buf.WriteByte('?') 190 | amp := false 191 | for _, k := range keys { 192 | if amp { 193 | p.buf.WriteByte('&') 194 | } 195 | p.buf.WriteString(url.QueryEscape(k)) 196 | p.buf.WriteByte('=') 197 | isjson := false 198 | for _, jskey := range jskeys { 199 | if k == jskey { 200 | isjson = true 201 | break 202 | } 203 | } 204 | if isjson { 205 | jsonv, err := json.Marshal(opts[k]) 206 | if err != nil { 207 | return "", fmt.Errorf("invalid option %q: %v", k, err) 208 | } 209 | p.buf.WriteString(url.QueryEscape(string(jsonv))) 210 | } else { 211 | if err := encval(&p.buf, k, opts[k]); err != nil { 212 | return "", fmt.Errorf("invalid option %q: %v", k, err) 213 | } 214 | } 215 | amp = true 216 | } 217 | return p.path(), nil 218 | } 219 | 220 | func encval(w io.Writer, k string, v interface{}) error { 221 | if v == nil { 222 | return errors.New("value is nil") 223 | } 224 | rv := reflect.ValueOf(v) 225 | var str string 226 | switch rv.Kind() { 227 | case reflect.String: 228 | str = url.QueryEscape(rv.String()) 229 | case reflect.Bool: 230 | str = strconv.FormatBool(rv.Bool()) 231 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 232 | str = strconv.FormatInt(rv.Int(), 10) 233 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 234 | str = strconv.FormatUint(rv.Uint(), 10) 235 | case reflect.Float32: 236 | str = strconv.FormatFloat(rv.Float(), 'f', -1, 32) 237 | case reflect.Float64: 238 | str = strconv.FormatFloat(rv.Float(), 'f', -1, 64) 239 | default: 240 | return fmt.Errorf("unsupported type: %s", rv.Type()) 241 | } 242 | _, err := io.WriteString(w, str) 243 | return err 244 | } 245 | 246 | // responseRev returns the unquoted Etag of a response. 247 | func responseRev(resp *http.Response, err error) (string, error) { 248 | if err != nil { 249 | return "", err 250 | } else if etag := resp.Header.Get("Etag"); etag == "" { 251 | return "", fmt.Errorf("couchdb: missing Etag header in response") 252 | } else { 253 | return etag[1 : len(etag)-1], nil 254 | } 255 | } 256 | 257 | func readBody(resp *http.Response, v interface{}) error { 258 | if err := json.NewDecoder(resp.Body).Decode(v); err != nil { 259 | resp.Body.Close() 260 | return err 261 | } 262 | return resp.Body.Close() 263 | } 264 | 265 | // Error represents API-level errors, reported by CouchDB as 266 | // {"error": , "reason": } 267 | type Error struct { 268 | Method string // HTTP method of the request 269 | URL string // HTTP URL of the request 270 | StatusCode int // HTTP status code of the response 271 | 272 | // These two fields will be empty for HEAD requests. 273 | ErrorCode string // Error reason provided by CouchDB 274 | Reason string // Error message provided by CouchDB 275 | } 276 | 277 | func (e *Error) Error() string { 278 | if e.ErrorCode == "" { 279 | return fmt.Sprintf("%v %v: %v", e.Method, e.URL, e.StatusCode) 280 | } 281 | return fmt.Sprintf("%v %v: (%v) %v: %v", 282 | e.Method, e.URL, e.StatusCode, e.ErrorCode, e.Reason) 283 | } 284 | 285 | // NotFound checks whether the given errors is a DatabaseError 286 | // with StatusCode == 404. This is useful for conditional creation 287 | // of databases and documents. 288 | func NotFound(err error) bool { 289 | return ErrorStatus(err, http.StatusNotFound) 290 | } 291 | 292 | // Unauthorized checks whether the given error is a DatabaseError 293 | // with StatusCode == 401. 294 | func Unauthorized(err error) bool { 295 | return ErrorStatus(err, http.StatusUnauthorized) 296 | } 297 | 298 | // Conflict checks whether the given error is a DatabaseError 299 | // with StatusCode == 409. 300 | func Conflict(err error) bool { 301 | return ErrorStatus(err, http.StatusConflict) 302 | } 303 | 304 | // ErrorStatus checks whether the given error is a DatabaseError 305 | // with a matching statusCode. 306 | func ErrorStatus(err error, statusCode int) bool { 307 | dberr, ok := err.(*Error) 308 | return ok && dberr.StatusCode == statusCode 309 | } 310 | 311 | func parseError(req *http.Request, resp *http.Response) error { 312 | var reply struct{ Error, Reason string } 313 | if req.Method != "HEAD" { 314 | if err := readBody(resp, &reply); err != nil { 315 | return fmt.Errorf("couldn't decode CouchDB error: %v", err) 316 | } 317 | } 318 | return &Error{ 319 | Method: req.Method, 320 | URL: req.URL.String(), 321 | StatusCode: resp.StatusCode, 322 | ErrorCode: reply.Error, 323 | Reason: reply.Reason, 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package couchdb_test 2 | 3 | import ( 4 | . "net/http" 5 | "testing" 6 | ) 7 | 8 | type testauth struct{ called bool } 9 | 10 | func (a *testauth) AddAuth(*Request) { 11 | a.called = true 12 | } 13 | 14 | func TestClientSetAuth(t *testing.T) { 15 | c := newTestClient(t) 16 | c.Handle("HEAD /", func(resp ResponseWriter, req *Request) {}) 17 | 18 | auth := new(testauth) 19 | c.SetAuth(auth) 20 | if err := c.Ping(); err != nil { 21 | t.Fatal(err) 22 | } 23 | if !auth.called { 24 | t.Error("AddAuth was not called") 25 | } 26 | 27 | auth.called = false 28 | c.SetAuth(nil) 29 | if err := c.Ping(); err != nil { 30 | t.Fatal(err) 31 | } 32 | if auth.called { 33 | t.Error("AddAuth was called after removing Auth instance") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x_test.go: -------------------------------------------------------------------------------- 1 | // This file contains stuff that is used across all the tests. 2 | 3 | package couchdb_test 4 | 5 | import ( 6 | "bytes" 7 | . "net/http" 8 | "net/http/httptest" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/fjl/go-couchdb" 13 | ) 14 | 15 | // testClient is a very special couchdb.Client that also implements 16 | // the http.RoundTripper interface. The tests can register HTTP 17 | // handlers on the testClient. Any requests made through the client are 18 | // dispatched to a matching handler. This allows us to test what the 19 | // HTTP client in the couchdb package does without actually using the network. 20 | // 21 | // If no handler matches the requests method/path combination, the test 22 | // fails with a descriptive error. 23 | type testClient struct { 24 | *couchdb.Client 25 | t *testing.T 26 | handlers map[string]Handler 27 | } 28 | 29 | func (s *testClient) Handle(pat string, f func(ResponseWriter, *Request)) { 30 | s.handlers[pat] = HandlerFunc(f) 31 | } 32 | 33 | func (s *testClient) ClearHandlers() { 34 | s.handlers = make(map[string]Handler) 35 | } 36 | 37 | func (s *testClient) RoundTrip(req *Request) (*Response, error) { 38 | handler, ok := s.handlers[req.Method+" "+req.URL.EscapedPath()] 39 | if !ok { 40 | s.t.Fatalf("unhandled request: %s %s", req.Method, req.URL.EscapedPath()) 41 | return nil, nil 42 | } 43 | recorder := httptest.NewRecorder() 44 | recorder.Body = new(bytes.Buffer) 45 | handler.ServeHTTP(recorder, req) 46 | return recorder.Result(), nil 47 | } 48 | 49 | func newTestClient(t *testing.T) *testClient { 50 | tc := &testClient{t: t, handlers: make(map[string]Handler)} 51 | client, err := couchdb.NewClient("http://testClient:5984/", tc) 52 | if err != nil { 53 | t.Fatalf("couchdb.NewClient returned error: %v", err) 54 | } 55 | tc.Client = client 56 | return tc 57 | } 58 | 59 | func check(t *testing.T, field string, expected, actual interface{}) { 60 | if !reflect.DeepEqual(expected, actual) { 61 | t.Errorf("%s mismatch:\nwant %#v\ngot %#v", field, expected, actual) 62 | } 63 | } 64 | --------------------------------------------------------------------------------