├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── auth.go ├── bulk_docs.go ├── bulk_docs_test.go ├── connection.go ├── connection_test.go ├── couchdb.go ├── couchdb2.1-install.sh └── couchdb_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | #Misc junk 27 | *.swp 28 | 29 | #Idea stuff 30 | .idea/ 31 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6 4 | - 1.7 5 | - 1.8 6 | - 1.9 7 | sudo: required 8 | dist: trusty 9 | before_install: 10 | - ./couchdb2.1-install.sh 11 | before_script: 12 | - curl -X PUT http://127.0.0.1:5984/_node/_local/_config/admins/adminuser -d '"password"' 13 | script: 14 | - go get github.com/twinj/uuid 15 | - go test -v ./... 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 James Adam 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | couchdb-go 2 | ========== 3 | 4 | [![Build Status](https://travis-ci.org/rhinoman/couchdb-go.svg?branch=master)](https://travis-ci.org/rhinoman/couchdb-go) 5 | 6 | **NOTE**: Use the v1.0 Tag for CouchDB 1.0. The current master is being used for CouchDB 2.x work (still a work in progress). 7 | 8 | Description 9 | ----------- 10 | 11 | This is my golang CouchDB driver. There are many like it, but this one is mine. 12 | 13 | 14 | Installation 15 | ------------ 16 | 17 | ``` 18 | go get github.com/rhinoman/couchdb-go 19 | ``` 20 | 21 | Documentation 22 | ------------- 23 | 24 | See the Godoc: http://godoc.org/github.com/rhinoman/couchdb-go 25 | 26 | Example Usage 27 | ------------- 28 | 29 | Connect to a server and create a new document: 30 | 31 | ```go 32 | 33 | type TestDocument struct { 34 | Title string 35 | Note string 36 | } 37 | 38 | ... 39 | 40 | var timeout = time.Duration(500 * time.Millisecond) 41 | conn, err := couchdb.NewConnection("127.0.0.1",5984,timeout) 42 | auth := couchdb.BasicAuth{Username: "user", Password: "password" } 43 | db := conn.SelectDB("myDatabase", &auth) 44 | 45 | theDoc := TestDocument{ 46 | Title: "My Document", 47 | Note: "This is a note", 48 | } 49 | 50 | theId := genUuid() //use whatever method you like to generate a uuid 51 | //The third argument here would be a revision, if you were updating an existing document 52 | rev, err := db.Save(theDoc, theId, "") 53 | //If all is well, rev should contain the revision of the newly created 54 | //or updated Document 55 | ``` 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | //Basic interface for Auth 11 | type Auth interface { 12 | //Adds authentication headers to a request 13 | AddAuthHeaders(*http.Request) 14 | //Extracts Updated auth info from Couch Response 15 | UpdateAuth(*http.Response) 16 | //Sets updated auth (headers, cookies, etc.) in an http response 17 | //For the update function, the map keys are cookie and/or header names 18 | GetUpdatedAuth() map[string]string 19 | //Purely for debug purposes. Do not call, ever. 20 | DebugString() string 21 | } 22 | 23 | //HTTP Basic Authentication support 24 | type BasicAuth struct { 25 | Username string 26 | Password string 27 | } 28 | 29 | //Pass-through Auth header 30 | type PassThroughAuth struct { 31 | AuthHeader string 32 | } 33 | 34 | //Cookie-based auth (for sessions) 35 | type CookieAuth struct { 36 | AuthToken string 37 | UpdatedAuthToken string 38 | } 39 | 40 | //Proxy authentication 41 | type ProxyAuth struct { 42 | Username string 43 | Roles []string 44 | AuthToken string 45 | } 46 | 47 | //Adds Basic Authentication headers to an http request 48 | func (ba *BasicAuth) AddAuthHeaders(req *http.Request) { 49 | authString := []byte(ba.Username + ":" + ba.Password) 50 | header := "Basic " + base64.StdEncoding.EncodeToString(authString) 51 | req.Header.Set("Authorization", string(header)) 52 | } 53 | 54 | //Use if you already have an Authentication header you want to pass through to couchdb 55 | func (pta *PassThroughAuth) AddAuthHeaders(req *http.Request) { 56 | req.Header.Set("Authorization", pta.AuthHeader) 57 | } 58 | 59 | //Adds session token to request 60 | func (ca *CookieAuth) AddAuthHeaders(req *http.Request) { 61 | authString := "AuthSession=" + ca.AuthToken 62 | req.Header.Set("Cookie", authString) 63 | req.Header.Set("X-CouchDB-WWW-Authenticate", "Cookie") 64 | } 65 | 66 | func (pa *ProxyAuth) AddAuthHeaders(req *http.Request) { 67 | req.Header.Set("X-Auth-CouchDB-Username", pa.Username) 68 | rolesString := strings.Join(pa.Roles, ",") 69 | req.Header.Set("X-Auth-CouchDB-Roles", rolesString) 70 | if pa.AuthToken != "" { 71 | req.Header.Set("X-Auth-CouchDB-Token", pa.AuthToken) 72 | } 73 | } 74 | 75 | //Update Auth Data 76 | //If couchdb generates a new token, place it in a separate field so that 77 | //it is available to an application 78 | 79 | //do nothing for basic auth 80 | func (ba *BasicAuth) UpdateAuth(resp *http.Response) {} 81 | 82 | //Couchdb returns updated AuthSession tokens 83 | func (ca *CookieAuth) UpdateAuth(resp *http.Response) { 84 | for _, cookie := range resp.Cookies() { 85 | if cookie.Name == "AuthSession" { 86 | ca.UpdatedAuthToken = cookie.Value 87 | } 88 | } 89 | } 90 | 91 | //do nothing for pass through 92 | func (pta *PassThroughAuth) UpdateAuth(resp *http.Response) {} 93 | 94 | //do nothing for proxy auth 95 | func (pa *ProxyAuth) UpdateAuth(resp *http.Response) {} 96 | 97 | //Get Updated Auth 98 | //Does nothing for BasicAuth 99 | func (ba *BasicAuth) GetUpdatedAuth() map[string]string { 100 | return nil 101 | } 102 | 103 | //Does nothing for PassThroughAuth 104 | func (pta *PassThroughAuth) GetUpdatedAuth() map[string]string { 105 | return nil 106 | } 107 | 108 | //Set AuthSession Cookie 109 | func (ca *CookieAuth) GetUpdatedAuth() map[string]string { 110 | am := make(map[string]string) 111 | if ca.UpdatedAuthToken != "" { 112 | am["AuthSession"] = ca.UpdatedAuthToken 113 | } 114 | return am 115 | } 116 | 117 | //do nothing for Proxy Auth 118 | func (pa *ProxyAuth) GetUpdatedAuth() map[string]string { 119 | return nil 120 | } 121 | 122 | //Return a Debug string 123 | 124 | func (ba *BasicAuth) DebugString() string { 125 | return fmt.Sprintf("Username: %v, Password: %v", ba.Username, ba.Password) 126 | } 127 | 128 | func (pta *PassThroughAuth) DebugString() string { 129 | return fmt.Sprintf("Authorization Header: %v", pta.AuthHeader) 130 | } 131 | 132 | func (ca *CookieAuth) DebugString() string { 133 | return fmt.Sprintf("AuthToken: %v, Updated AuthToken: %v", 134 | ca.AuthToken, ca.UpdatedAuthToken) 135 | } 136 | 137 | func (pa *ProxyAuth) DebugString() string { 138 | return fmt.Sprintf("Username: %v, Roles: %v, AuthToken: %v", 139 | pa.Username, pa.Roles, pa.AuthToken) 140 | } 141 | 142 | //TODO: Add support for other Authentication methods supported by Couch: 143 | //OAuth, etc. 144 | -------------------------------------------------------------------------------- /bulk_docs.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | ) 11 | 12 | type bulkDoc struct { 13 | _id string 14 | _rev string 15 | _deleted bool 16 | doc interface{} 17 | } 18 | 19 | func (b bulkDoc) MarshalJSON() ([]byte, error) { 20 | out := make(map[string]interface{}) 21 | if !b._deleted { 22 | bValue := reflect.Indirect(reflect.ValueOf(b.doc)) 23 | bType := bValue.Type() 24 | for i := 0; i < bType.NumField(); i++ { 25 | field := bType.Field(i) 26 | name := bType.Field(i).Name 27 | jsonKey := field.Tag.Get("json") 28 | if jsonKey == "" { 29 | jsonKey = name 30 | } 31 | out[jsonKey] = bValue.FieldByName(name).Interface() 32 | } 33 | } else { 34 | out["_deleted"] = true 35 | } 36 | out["_id"] = b._id 37 | if b._rev != "" { 38 | out["_rev"] = b._rev 39 | } 40 | return json.Marshal(out) 41 | } 42 | 43 | // BulkDocument Bulk Document API 44 | // http://docs.couchdb.org/en/1.6.1/api/database/bulk-api.html#db-bulk-docs 45 | type BulkDocument struct { 46 | docs []bulkDoc 47 | db *Database 48 | closed bool 49 | } 50 | 51 | // NewBulkDocument New BulkDocument instance 52 | func (db *Database) NewBulkDocument() *BulkDocument { 53 | b := &BulkDocument{} 54 | b.db = db 55 | return b 56 | } 57 | 58 | // Save Save document 59 | func (b *BulkDocument) Save(doc interface{}, id, rev string) error { 60 | if id == "" { 61 | return fmt.Errorf("No ID specified") 62 | } 63 | b.docs = append(b.docs, bulkDoc{id, rev, false, doc}) 64 | return nil 65 | } 66 | 67 | // Delete Delete document 68 | func (b *BulkDocument) Delete(id, rev string) error { 69 | if id == "" { 70 | return fmt.Errorf("No ID specified") 71 | } 72 | if rev == "" { 73 | return fmt.Errorf("No Revision specified") 74 | } 75 | b.docs = append(b.docs, bulkDoc{id, rev, true, nil}) 76 | return nil 77 | } 78 | 79 | // BulkDocumentResult Bulk Document Response 80 | type BulkDocumentResult struct { 81 | Ok bool `json:"ok"` 82 | ID string `json:"id"` 83 | Revision string `json:"rev"` 84 | Error *string `json:"error"` 85 | Reason *string `json:"reason"` 86 | } 87 | 88 | func getBulkDocumentResult(resp *http.Response) ([]BulkDocumentResult, error) { 89 | body, err := ioutil.ReadAll(resp.Body) 90 | if err != nil { 91 | return nil, err 92 | } 93 | var results []BulkDocumentResult 94 | err = json.Unmarshal(body, &results) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return results, nil 99 | } 100 | 101 | // Commit POST /{db}/_bulk_docs 102 | func (b *BulkDocument) Commit() ([]BulkDocumentResult, error) { 103 | if !b.closed { 104 | b.closed = true 105 | url, err := buildUrl(b.db.dbName, "_bulk_docs") 106 | if err != nil { 107 | return nil, err 108 | } 109 | var headers = make(map[string]string) 110 | headers["Content-Type"] = "application/json" 111 | headers["Accept"] = "application/json" 112 | bd := make(map[string]interface{}) 113 | bd["docs"] = b.docs 114 | data, numBytes, err := encodeData(bd) 115 | if err != nil { 116 | return nil, err 117 | } 118 | headers["Content-Length"] = strconv.Itoa(numBytes) 119 | //Yes, this needs to be here. 120 | //Yes, I know the Golang http.Client doesn't support expect/continue 121 | //This is here to work around a bug in CouchDB. It shouldn't work, and yet it does. 122 | //See: http://stackoverflow.com/questions/30541591/large-put-requests-from-go-to-couchdb 123 | //Also, I filed a bug report: https://issues.apache.org/jira/browse/COUCHDB-2704 124 | //Go net/http needs to support the HTTP/1.1 spec, or CouchDB needs to get fixed. 125 | //If either of those happens in the future, I can revisit this. 126 | //Unless I forget, which I'm sure I will. 127 | if numBytes > 4000 { 128 | headers["Expect"] = "100-continue" 129 | } 130 | resp, err := b.db.connection.request("POST", url, data, headers, b.db.auth) 131 | if err != nil { 132 | return nil, err 133 | } 134 | defer resp.Body.Close() 135 | return getBulkDocumentResult(resp) 136 | } 137 | return nil, fmt.Errorf("CouchDB: Bulk Document has already been executed") 138 | } 139 | -------------------------------------------------------------------------------- /bulk_docs_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import "testing" 4 | 5 | func TestBulkDocumentClosed(t *testing.T) { 6 | var err error 7 | dbName := createTestDb(t) 8 | conn := getConnection(t) 9 | db := conn.SelectDB(dbName, adminAuth) 10 | bulk := db.NewBulkDocument() 11 | 12 | theDoc := TestDocument{ 13 | Title: "My Document", 14 | Note: "This is my note", 15 | } 16 | theID := getUuid() 17 | err = bulk.Save(theDoc, theID, "") 18 | errorify(t, err) 19 | 20 | // first time 21 | _, err = bulk.Commit() 22 | errorify(t, err) 23 | 24 | // second times 25 | _, err = bulk.Commit() 26 | if err == nil { 27 | t.Log("ERROR: Must be caused exception when Commit() for second times") 28 | } 29 | 30 | deleteTestDb(t, dbName) 31 | } 32 | 33 | func TestBulkDocumentInsertUpdateDelete(t *testing.T) { 34 | var err error 35 | dbName := createTestDb(t) 36 | conn := getConnection(t) 37 | db := conn.SelectDB(dbName, adminAuth) 38 | 39 | theID1 := getUuid() 40 | theRev1 := "" 41 | theDoc1 := TestDocument{ 42 | Title: "My Document " + theID1, 43 | Note: "This is my note", 44 | } 45 | theID2 := getUuid() 46 | theRev2 := "" 47 | theDoc2 := TestDocument{ 48 | Title: "My Document " + theID2, 49 | Note: "This is my note", 50 | } 51 | 52 | bulkInsert := db.NewBulkDocument() 53 | okInsertTheDoc1 := false 54 | err = bulkInsert.Save(theDoc1, theID1, "") 55 | errorify(t, err) 56 | okInsertTheDoc2 := false 57 | err = bulkInsert.Save(theDoc2, theID2, "") 58 | errorify(t, err) 59 | insertResults, err := bulkInsert.Commit() 60 | errorify(t, err) 61 | for _, insertResult := range insertResults { 62 | if insertResult.ID == theID1 { 63 | theRev1 = insertResult.Revision 64 | okInsertTheDoc1 = insertResult.Ok 65 | } else if insertResult.ID == theID2 { 66 | theRev2 = insertResult.Revision 67 | okInsertTheDoc2 = insertResult.Ok 68 | } 69 | } 70 | if !(okInsertTheDoc1 && okInsertTheDoc2) { 71 | t.Log("ERROR: failed to insert documents") 72 | } 73 | 74 | bulkUpdate := db.NewBulkDocument() 75 | okUpdateTheDoc1 := false 76 | theDoc1.Note = theDoc1.Note + " " + theRev1 77 | err = bulkUpdate.Save(theDoc1, theID1, theRev1) 78 | errorify(t, err) 79 | okUpdateTheDoc2 := false 80 | theDoc2.Note = theDoc2.Note + " " + theRev2 81 | err = bulkUpdate.Save(theDoc2, theID2, theRev2) 82 | errorify(t, err) 83 | updateResults, err := bulkUpdate.Commit() 84 | errorify(t, err) 85 | for _, updateResult := range updateResults { 86 | if updateResult.ID == theID1 { 87 | theRev1 = updateResult.Revision 88 | okUpdateTheDoc1 = updateResult.Ok 89 | } else if updateResult.ID == theID2 { 90 | theRev2 = updateResult.Revision 91 | okUpdateTheDoc2 = updateResult.Ok 92 | } 93 | } 94 | if !(okUpdateTheDoc1 && okUpdateTheDoc2) { 95 | t.Log("ERROR: failed to update documents") 96 | } 97 | 98 | bulkDelete := db.NewBulkDocument() 99 | okDeleteTheDoc1 := false 100 | err = bulkDelete.Delete(theID1, theRev1) 101 | errorify(t, err) 102 | okDeleteTheDoc2 := false 103 | err = bulkDelete.Delete(theID2, theRev2) 104 | errorify(t, err) 105 | deleteResults, err := bulkDelete.Commit() 106 | errorify(t, err) 107 | for _, deleteResult := range deleteResults { 108 | if deleteResult.ID == theID1 { 109 | theRev1 = deleteResult.Revision 110 | okDeleteTheDoc1 = deleteResult.Ok 111 | } else if deleteResult.ID == theID2 { 112 | theRev2 = deleteResult.Revision 113 | okDeleteTheDoc2 = deleteResult.Ok 114 | } 115 | } 116 | if !(okDeleteTheDoc1 && okDeleteTheDoc2) { 117 | t.Log("ERROR: failed to delete documents") 118 | } 119 | 120 | deleteTestDb(t, dbName) 121 | } 122 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | //represents a couchdb 'connection' 16 | type connection struct { 17 | url string 18 | client *http.Client 19 | } 20 | 21 | //processes a request 22 | func (conn *connection) request(method, path string, 23 | body io.Reader, headers map[string]string, auth Auth) (*http.Response, error) { 24 | 25 | req, err := http.NewRequest(method, conn.url+path, body) 26 | //set headers 27 | for k, v := range headers { 28 | req.Header.Set(k, v) 29 | } 30 | if err != nil { 31 | return nil, err 32 | } 33 | if auth != nil { 34 | auth.AddAuthHeaders(req) 35 | } 36 | resp, err := conn.processResponse(0, req) 37 | if err == nil && resp != nil && auth != nil { 38 | auth.UpdateAuth(resp) 39 | } 40 | return resp, err 41 | } 42 | 43 | //Returns a result from couchdb directly to a requesting client 44 | //Useful for downloading large files 45 | func (conn *connection) reverseProxyRequest(w http.ResponseWriter, 46 | r *http.Request, path string, auth Auth) error { 47 | target, err := url.Parse(conn.url) 48 | if err != nil { 49 | return err 50 | } 51 | if auth != nil { 52 | auth.AddAuthHeaders(r) 53 | } 54 | director := func(req *http.Request) { 55 | req.URL.Scheme = target.Scheme 56 | req.URL.Host = target.Host 57 | req.URL.Path = singleJoiningSlash(target.Path, path) 58 | } 59 | rp := &httputil.ReverseProxy{Director: director} 60 | rp.ServeHTTP(w, r) 61 | return nil 62 | } 63 | 64 | func singleJoiningSlash(a, b string) string { 65 | aslash := strings.HasSuffix(a, "/") 66 | bslash := strings.HasPrefix(b, "/") 67 | switch { 68 | case aslash && bslash: 69 | return a + b[1:] 70 | case !aslash && !bslash: 71 | return a + "/" + b 72 | } 73 | return a + b 74 | } 75 | 76 | func (conn *connection) processResponse(numTries int, 77 | req *http.Request) (*http.Response, error) { 78 | resp, err := conn.client.Do(req) 79 | if err != nil { 80 | errStr := err.Error() 81 | // Because sometimes couchdb rudely 82 | // slams the connection shut and we get a race condition. 83 | // Of course, Go http presents one of two possibilities 84 | // for error strings, so we check for both. 85 | if (strings.Contains(errStr, "EOF") || 86 | strings.Contains(errStr, "broken connection")) && numTries < 3 { 87 | //wait a bit and try again 88 | fmt.Printf("\nERROR! %v\n", errStr) 89 | time.Sleep(10 * time.Millisecond) 90 | numTries += 1 91 | return conn.processResponse(numTries, req) 92 | } else { 93 | return nil, err 94 | } 95 | } else if resp.StatusCode >= 400 { 96 | return resp, parseError(resp) 97 | } else { 98 | return resp, nil 99 | } 100 | } 101 | 102 | type Error struct { 103 | StatusCode int 104 | URL string 105 | Method string 106 | ErrorCode string //empty for HEAD requests 107 | Reason string //empty for HEAD requests 108 | } 109 | 110 | //stringify the error 111 | func (err *Error) Error() string { 112 | return fmt.Sprintf("[Error]:%v: %v %v - %v %v", 113 | err.StatusCode, err.Method, err.URL, err.ErrorCode, err.Reason) 114 | } 115 | 116 | //extracts rev code from header 117 | func getRevInfo(resp *http.Response) (string, error) { 118 | if rev := resp.Header.Get("ETag"); rev == "" { 119 | var dbResponse struct { 120 | Ok bool `json:"ok"` 121 | Id string `json:"id"` 122 | Rev string `json:"rev"` 123 | } 124 | // if ETag header isn't present, attempt to get the rev from the response body 125 | // see: https://issues.apache.org/jira/browse/COUCHDB-2853 126 | json.NewDecoder(resp.Body).Decode(&dbResponse) 127 | if dbResponse.Rev != "" { 128 | return dbResponse.Rev, nil 129 | } 130 | return "", fmt.Errorf("CouchDB did not return rev info") 131 | } else { 132 | return rev[1 : len(rev)-1], nil 133 | } 134 | } 135 | 136 | //unmarshalls a JSON Response Body 137 | func parseBody(resp *http.Response, o interface{}) error { 138 | err := json.NewDecoder(resp.Body).Decode(&o) 139 | if err != nil { 140 | resp.Body.Close() 141 | return err 142 | } else { 143 | return resp.Body.Close() 144 | } 145 | } 146 | 147 | // encodes a struct to JSON and returns an io.Reader, 148 | // the buffer size, and an error (if any) 149 | func encodeData(o interface{}) (io.Reader, int, error) { 150 | if o == nil { 151 | return nil, 0, nil 152 | } 153 | buf, err := json.Marshal(&o) 154 | if err != nil { 155 | return nil, 0, err 156 | } else { 157 | return bytes.NewReader(buf), len(buf), nil 158 | } 159 | } 160 | 161 | //Parse a CouchDB error response 162 | func parseError(resp *http.Response) error { 163 | var couchReply struct{ Error, Reason string } 164 | if resp.Request.Method != "HEAD" { 165 | err := parseBody(resp, &couchReply) 166 | if err != nil { 167 | return fmt.Errorf("Unknown error accessing CouchDB: %v", err) 168 | } 169 | } 170 | return &Error{ 171 | StatusCode: resp.StatusCode, 172 | URL: resp.Request.URL.String(), 173 | Method: resp.Request.Method, 174 | ErrorCode: couchReply.Error, 175 | Reason: couchReply.Reason, 176 | } 177 | } 178 | 179 | //smooshes url segments together 180 | func buildString(pathSegments []string) string { 181 | pathSegments = makeSegments(pathSegments) 182 | urlString := "" 183 | for _, pathSegment := range pathSegments { 184 | urlString += "/" 185 | urlString += url.QueryEscape(pathSegment) 186 | } 187 | return urlString 188 | } 189 | 190 | func makeSegments(pathSegments []string) []string { 191 | segments := []string{} 192 | for _, segment := range pathSegments { 193 | segments = append(segments, strings.Split(segment, "/")...) 194 | } 195 | return segments 196 | } 197 | 198 | //Build Url 199 | func buildUrl(pathSegments ...string) (string, error) { 200 | var Url *url.URL 201 | urlString := buildString(pathSegments) 202 | Url, err := url.Parse(urlString) 203 | if err != nil { 204 | return "", err 205 | } 206 | return Url.String(), nil 207 | } 208 | 209 | //Build Url with query arguments 210 | func buildParamUrl(params url.Values, pathSegments ...string) (string, error) { 211 | var Url *url.URL 212 | urlString := buildString(pathSegments) 213 | Url, err := url.Parse(urlString) 214 | if err != nil { 215 | return "", err 216 | } 217 | Url.RawQuery = params.Encode() 218 | return Url.String(), nil 219 | } 220 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | type couchWelcome struct { 10 | Couchdb string `json:"couchdb"` 11 | Uuid string `json:"uuid"` 12 | Version string `json:"version"` 13 | Vendor interface{} `json:"vendor"` 14 | } 15 | 16 | var serverUrl = "http://127.0.0.1:5984" 17 | var couchReply couchWelcome 18 | 19 | func TestUrlBuilding(t *testing.T) { 20 | params := url.Values{} 21 | params.Add("Hello", "42") 22 | params.Add("crazy", "\"me&bo\"") 23 | stringified, err := buildParamUrl(params, "theDb", "funny?chars") 24 | if err != nil { 25 | t.Fail() 26 | } 27 | t.Logf("The URL: %s\n", stringified) 28 | //make sure everything is escaped 29 | if stringified != "/theDb/funny%3Fchars?Hello=42&crazy=%22me%26bo%22" { 30 | t.Fail() 31 | } 32 | } 33 | 34 | func TestConnection(t *testing.T) { 35 | client := &http.Client{} 36 | c := connection{ 37 | url: serverUrl, 38 | client: client, 39 | } 40 | resp, err := c.request("GET", "/", nil, nil, nil) 41 | if err != nil { 42 | t.Logf("Error: %v\n", err) 43 | t.Fail() 44 | } else if resp == nil { 45 | t.Fail() 46 | } else { 47 | jsonError := parseBody(resp, &couchReply) 48 | if jsonError != nil { 49 | t.Fail() 50 | } else { 51 | if resp.StatusCode != 200 || 52 | couchReply.Couchdb != "Welcome" { 53 | t.Fail() 54 | } 55 | t.Logf("STATUS: %v\n", resp.StatusCode) 56 | t.Logf("couchdb: %v", couchReply.Couchdb) 57 | t.Logf("uuid: %v", couchReply.Uuid) 58 | t.Logf("version: %v", couchReply.Version) 59 | t.Logf("vendor: %v", couchReply.Vendor) 60 | } 61 | } 62 | } 63 | 64 | func TestBasicAuth(t *testing.T) { 65 | client := &http.Client{} 66 | auth := BasicAuth{Username: "adminuser", Password: "password"} 67 | c := connection{ 68 | url: serverUrl, 69 | client: client, 70 | } 71 | resp, err := c.request("GET", "/", nil, nil, &auth) 72 | if err != nil { 73 | t.Logf("Error: %v", err) 74 | t.Fail() 75 | } else if resp == nil { 76 | t.Logf("Response was nil") 77 | t.Fail() 78 | } 79 | } 80 | 81 | func TestProxyAuth(t *testing.T) { 82 | client := &http.Client{} 83 | pAuth := ProxyAuth{ 84 | Username: "adminuser", 85 | Roles: []string{"admin", "master", "_admin"}, 86 | } 87 | c := connection{ 88 | url: serverUrl, 89 | client: client, 90 | } 91 | resp, err := c.request("GET", "/", nil, nil, &pAuth) 92 | if err != nil { 93 | t.Logf("Error: %v", err) 94 | t.Fail() 95 | } else if resp == nil { 96 | t.Logf("Response was nil") 97 | t.Fail() 98 | } 99 | } 100 | 101 | func TestBadAuth(t *testing.T) { 102 | client := &http.Client{} 103 | auth := BasicAuth{Username: "notauser", Password: "what?"} 104 | c := connection{ 105 | url: serverUrl, 106 | client: client, 107 | } 108 | resp, err := c.request("GET", "/", nil, nil, &auth) 109 | if err == nil { 110 | t.Fail() 111 | } else if resp.StatusCode != 401 { 112 | t.Logf("Wrong Status: %v", resp.StatusCode) 113 | t.Fail() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /couchdb.go: -------------------------------------------------------------------------------- 1 | //Package couchdb provides a simple REST client for CouchDB 2 | package couchdb 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type Connection struct{ *connection } 18 | 19 | type Database struct { 20 | dbName string 21 | connection *Connection 22 | auth Auth 23 | } 24 | 25 | //Creates a regular http connection. 26 | //Timeout sets the timeout for the http Client 27 | func NewConnection(address string, port int, 28 | timeout time.Duration) (*Connection, error) { 29 | 30 | url := "http://" + address + ":" + strconv.Itoa(port) 31 | return createConnection(url, timeout) 32 | } 33 | 34 | //Creates an https connection. 35 | //Timeout sets the timeout for the http Client 36 | func NewSSLConnection(address string, port int, 37 | timeout time.Duration) (*Connection, error) { 38 | 39 | url := "https://" + address + ":" + strconv.Itoa(port) 40 | return createConnection(url, timeout) 41 | } 42 | 43 | func createConnection(rawUrl string, timeout time.Duration) (*Connection, error) { 44 | //check that the url is valid 45 | theUrl, err := url.Parse(rawUrl) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &Connection{ 50 | &connection{ 51 | url: theUrl.String(), 52 | client: &http.Client{Timeout: timeout}, 53 | }, 54 | }, nil 55 | 56 | } 57 | 58 | //Use to check if database server is alive. 59 | func (conn *Connection) Ping() error { 60 | resp, err := conn.request("HEAD", "/", nil, nil, nil) 61 | if err == nil { 62 | resp.Body.Close() 63 | } 64 | return err 65 | } 66 | 67 | //DATABASES. 68 | //Return a list of all databases on the server 69 | func (conn *Connection) GetDBList() (dbList []string, err error) { 70 | resp, err := conn.request("GET", "/_all_dbs", nil, nil, nil) 71 | if err != nil { 72 | return dbList, err 73 | } 74 | err = parseBody(resp, &dbList) 75 | return dbList, err 76 | } 77 | 78 | //Create a new Database. 79 | func (conn *Connection) CreateDB(name string, auth Auth) error { 80 | url, err := buildUrl(name) 81 | if err != nil { 82 | return err 83 | } 84 | resp, err := conn.request("PUT", url, nil, nil, auth) 85 | if err == nil { 86 | resp.Body.Close() 87 | } 88 | return err 89 | } 90 | 91 | //Delete a Database. 92 | func (conn *Connection) DeleteDB(name string, auth Auth) error { 93 | url, err := buildUrl(name) 94 | if err != nil { 95 | return err 96 | } 97 | resp, err := conn.request("DELETE", url, nil, nil, auth) 98 | if err == nil { 99 | resp.Body.Close() 100 | } 101 | return err 102 | } 103 | 104 | //Set a CouchDB configuration option 105 | func (conn *Connection) SetConfig(section string, 106 | option string, value string, auth Auth) error { 107 | url, err := buildUrl("_node/_local/_config", section, option) 108 | if err != nil { 109 | return err 110 | } 111 | body := strings.NewReader("\"" + value + "\"") 112 | resp, err := conn.request("PUT", url, body, nil, auth) 113 | if err == nil { 114 | resp.Body.Close() 115 | } 116 | return err 117 | } 118 | 119 | //Gets a CouchDB configuration option 120 | func (conn *Connection) GetConfigOption(section string, 121 | option string, auth Auth) (string, error) { 122 | url, err := buildUrl("_node/_local/_config", section, option) 123 | if err != nil { 124 | return "", err 125 | } 126 | resp, err := conn.request("GET", url, nil, nil, auth) 127 | var val interface{} 128 | parseBody(resp, &val) 129 | if num, ok := val.(int); ok == true { 130 | return strconv.Itoa(num), nil 131 | } 132 | if str, ok := val.(string); ok == true { 133 | return str, nil 134 | } 135 | return "", nil 136 | } 137 | 138 | type UserRecord struct { 139 | Name string `json:"name"` 140 | Password string `json:"password,omitempty"` 141 | Roles []string `json:"roles"` 142 | TheType string `json:"type"` //apparently type is a keyword in Go :) 143 | 144 | } 145 | 146 | //Add a User. 147 | //This is a convenience method for adding a simple user to CouchDB. 148 | //If you need a User with custom fields, etc., you'll just have to use the 149 | //ordinary document methods on the "_users" database. 150 | func (conn *Connection) AddUser(username string, password string, 151 | roles []string, auth Auth) (string, error) { 152 | 153 | userData := UserRecord{ 154 | Name: username, 155 | Password: password, 156 | Roles: roles, 157 | TheType: "user"} 158 | userDb := conn.SelectDB("_users", auth) 159 | namestring := "org.couchdb.user:" + userData.Name 160 | return userDb.Save(&userData, namestring, "") 161 | 162 | } 163 | 164 | //Grants a role to a user 165 | func (conn *Connection) GrantRole(username string, role string, 166 | auth Auth) (string, error) { 167 | userDb := conn.SelectDB("_users", auth) 168 | namestring := "org.couchdb.user:" + username 169 | var userData interface{} 170 | 171 | rev, err := userDb.Read(namestring, &userData, nil) 172 | if err != nil { 173 | return "", err 174 | } 175 | if reflect.ValueOf(userData).Kind() != reflect.Map { 176 | return "", errors.New("Type Error") 177 | } 178 | userMap := userData.(map[string]interface{}) 179 | if reflect.ValueOf(userMap["roles"]).Kind() != reflect.Slice { 180 | return "", errors.New("Type Error") 181 | } 182 | userRoles := userMap["roles"].([]interface{}) 183 | //Check if our role is already in the array, so we don't add it twice 184 | for _, r := range userRoles { 185 | if r == role { 186 | return rev, nil 187 | } 188 | } 189 | userMap["roles"] = append(userRoles, role) 190 | return userDb.Save(&userMap, namestring, rev) 191 | } 192 | 193 | //Revoke a user role 194 | func (conn *Connection) RevokeRole(username string, role string, 195 | auth Auth) (string, error) { 196 | 197 | userDb := conn.SelectDB("_users", auth) 198 | namestring := "org.couchdb.user:" + username 199 | var userData interface{} 200 | rev, err := userDb.Read(namestring, &userData, nil) 201 | if err != nil { 202 | return "", err 203 | } 204 | if reflect.ValueOf(userData).Kind() != reflect.Map { 205 | return "", errors.New("Type Error") 206 | } 207 | userMap := userData.(map[string]interface{}) 208 | if reflect.ValueOf(userMap["roles"]).Kind() != reflect.Slice { 209 | return "", errors.New("Type Error") 210 | } 211 | userRoles := userMap["roles"].([]interface{}) 212 | found := false 213 | for i, r := range userRoles { 214 | if r == role { 215 | userRoles = append(userRoles[:i], userRoles[i+1:]...) 216 | found = true 217 | break 218 | } 219 | } 220 | userMap["roles"] = userRoles 221 | if found == false { 222 | return "", nil 223 | } else { 224 | return userDb.Save(&userMap, namestring, rev) 225 | } 226 | } 227 | 228 | type UserContext struct { 229 | Name string `json:"name"` 230 | Roles []string `json:"roles"` 231 | } 232 | 233 | type AuthInfo struct { 234 | Authenticated string `json:"authenticated"` 235 | AuthenticationDb string `json:"authentication_db"` 236 | AuthenticationHandlers []string `json:"authentication_handlers"` 237 | } 238 | 239 | type AuthInfoResponse struct { 240 | Info AuthInfo `json:"info"` 241 | Ok bool `json:"ok"` 242 | UserCtx UserContext `json:"userCtx"` 243 | } 244 | 245 | //Creates a session using the Couchdb session api. Returns auth token on success 246 | func (conn *Connection) CreateSession(username string, 247 | password string) (*CookieAuth, error) { 248 | sessUrl, err := buildUrl("_session") 249 | if err != nil { 250 | return &CookieAuth{}, err 251 | } 252 | var headers = make(map[string]string) 253 | body := "name=" + username + "&password=" + password 254 | headers["Content-Type"] = "application/x-www-form-urlencoded" 255 | resp, err := conn.request("POST", sessUrl, 256 | strings.NewReader(body), headers, nil) 257 | if err != nil { 258 | return &CookieAuth{}, err 259 | } 260 | defer resp.Body.Close() 261 | authToken := func() string { 262 | for _, cookie := range resp.Cookies() { 263 | if cookie.Name == "AuthSession" { 264 | return cookie.Value 265 | } 266 | } 267 | return "" 268 | }() 269 | return &CookieAuth{AuthToken: authToken}, nil 270 | } 271 | 272 | //Destroys a session (user log out, etc.) 273 | func (conn *Connection) DestroySession(auth *CookieAuth) error { 274 | sessUrl, err := buildUrl("_session") 275 | if err != nil { 276 | return err 277 | } 278 | var headers = make(map[string]string) 279 | headers["Accept"] = "application/json" 280 | resp, err := conn.request("DELETE", sessUrl, nil, headers, auth) 281 | if err != nil { 282 | return err 283 | } 284 | defer resp.Body.Close() 285 | return nil 286 | } 287 | 288 | //Returns auth information for a user 289 | func (conn *Connection) GetAuthInfo(auth Auth) (*AuthInfoResponse, error) { 290 | authInfo := AuthInfoResponse{} 291 | sessUrl, err := buildUrl("_session") 292 | if err != nil { 293 | return nil, err 294 | } 295 | var headers = make(map[string]string) 296 | headers["Accept"] = "application/json" 297 | resp, err := conn.request("GET", sessUrl, nil, headers, auth) 298 | if err != nil { 299 | return nil, err 300 | } 301 | defer resp.Body.Close() 302 | err = parseBody(resp, &authInfo) 303 | if err != nil { 304 | return nil, err 305 | } 306 | return &authInfo, nil 307 | } 308 | 309 | //Fetch a user record 310 | func (conn *Connection) GetUser(username string, userData interface{}, 311 | auth Auth) (string, error) { 312 | userDb := conn.SelectDB("_users", auth) 313 | namestring := "org.couchdb.user:" + username 314 | return userDb.Read(namestring, &userData, nil) 315 | } 316 | 317 | //Delete a user. 318 | func (conn *Connection) DeleteUser(username string, rev string, auth Auth) (string, error) { 319 | userDb := conn.SelectDB("_users", auth) 320 | namestring := "org.couchdb.user:" + username 321 | return userDb.Delete(namestring, rev) 322 | } 323 | 324 | //Select a Database. 325 | func (conn *Connection) SelectDB(dbName string, auth Auth) *Database { 326 | return &Database{ 327 | dbName: dbName, 328 | connection: conn, 329 | auth: auth, 330 | } 331 | } 332 | 333 | //DbExists checks if the database exists 334 | func (db *Database) DbExists() error { 335 | resp, err := db.connection.request("HEAD", "/"+db.dbName, nil, nil, db.auth) 336 | if err != nil { 337 | if resp != nil { 338 | resp.Body.Close() 339 | } 340 | } 341 | return err 342 | } 343 | 344 | //Compact the current database. 345 | func (db *Database) Compact() (resp string, e error) { 346 | url, err := buildUrl(db.dbName, "_compact") 347 | fmt.Println(url) 348 | if err != nil { 349 | return "", err 350 | } 351 | 352 | var headers = make(map[string]string) 353 | headers["Accept"] = "application/json" 354 | headers["Content-Type"] = "application/json" 355 | 356 | emtpyBody := "" 357 | 358 | dbResponse, err := db.connection.request("POST", url, strings.NewReader(emtpyBody), headers, db.auth) 359 | defer dbResponse.Body.Close() 360 | 361 | buf := new(bytes.Buffer) 362 | buf.ReadFrom(dbResponse.Body) 363 | strResp := buf.String() 364 | 365 | return strResp, err 366 | } 367 | 368 | //Save a document to the database. 369 | //If you're creating a new document, pass an empty string for rev. 370 | //If updating, you must specify the current rev. 371 | //Returns the revision number assigned to the doc by CouchDB. 372 | func (db *Database) Save(doc interface{}, id string, rev string) (string, error) { 373 | url, err := buildUrl(db.dbName, id) 374 | if err != nil { 375 | return "", err 376 | } 377 | var headers = make(map[string]string) 378 | headers["Content-Type"] = "application/json" 379 | headers["Accept"] = "application/json" 380 | if id == "" { 381 | return "", fmt.Errorf("No ID specified") 382 | } 383 | if rev != "" { 384 | headers["If-Match"] = rev 385 | } 386 | data, numBytes, err := encodeData(doc) 387 | if err != nil { 388 | return "", err 389 | } 390 | headers["Content-Length"] = strconv.Itoa(numBytes) 391 | //Yes, this needs to be here. 392 | //Yes, I know the Golang http.Client doesn't support expect/continue 393 | //This is here to work around a bug in CouchDB. It shouldn't work, and yet it does. 394 | //See: http://stackoverflow.com/questions/30541591/large-put-requests-from-go-to-couchdb 395 | //Also, I filed a bug report: https://issues.apache.org/jira/browse/COUCHDB-2704 396 | //Go net/http needs to support the HTTP/1.1 spec, or CouchDB needs to get fixed. 397 | //If either of those happens in the future, I can revisit this. 398 | //Unless I forget, which I'm sure I will. 399 | if numBytes > 4000 { 400 | headers["Expect"] = "100-continue" 401 | } 402 | resp, err := db.connection.request("PUT", url, data, headers, db.auth) 403 | if err != nil { 404 | return "", err 405 | } 406 | defer resp.Body.Close() 407 | return getRevInfo(resp) 408 | } 409 | 410 | //Copies a document into a new... document. 411 | //Returns the revision of the newly created document 412 | func (db *Database) Copy(fromId string, fromRev string, toId string) (string, error) { 413 | url, err := buildUrl(db.dbName, fromId) 414 | if err != nil { 415 | return "", err 416 | } 417 | var headers = make(map[string]string) 418 | headers["Accept"] = "application/json" 419 | if fromId == "" || toId == "" { 420 | return "", fmt.Errorf("Invalid request. Ids must be specified") 421 | } 422 | if fromRev != "" { 423 | headers["If-Match"] = fromRev 424 | } 425 | headers["Destination"] = toId 426 | resp, err := db.connection.request("COPY", url, nil, headers, db.auth) 427 | if err != nil { 428 | return "", err 429 | } 430 | defer resp.Body.Close() 431 | return getRevInfo(resp) 432 | } 433 | 434 | //Fetches a document from the database. 435 | //Pass it a &struct to hold the contents of the fetched document (doc). 436 | //Returns the current revision and/or error 437 | func (db *Database) Read(id string, doc interface{}, params *url.Values) (string, error) { 438 | var headers = make(map[string]string) 439 | headers["Accept"] = "application/json" 440 | var url string 441 | var err error 442 | if params == nil { 443 | url, err = buildUrl(db.dbName, id) 444 | } else { 445 | url, err = buildParamUrl(*params, db.dbName, id) 446 | } 447 | if err != nil { 448 | return "", err 449 | } 450 | resp, err := db.connection.request("GET", url, nil, headers, db.auth) 451 | if err != nil { 452 | return "", err 453 | } 454 | defer resp.Body.Close() 455 | if err = parseBody(resp, &doc); err != nil { 456 | return "", err 457 | } 458 | return getRevInfo(resp) 459 | } 460 | 461 | //Fetches multiple documents in a single request given a set of arbitrary _ids 462 | func (db *Database) ReadMultiple(ids []string, results interface{}) error { 463 | type RequestBody struct { 464 | Keys []string `json:"keys"` 465 | } 466 | parameters := url.Values{} 467 | parameters.Set("include_docs", "true") 468 | url, err := buildParamUrl(parameters, db.dbName, "_all_docs") 469 | if err != nil { 470 | return err 471 | } 472 | var headers = make(map[string]string) 473 | reqBody := RequestBody{Keys: ids} 474 | requestBody, numBytes, err := encodeData(reqBody) 475 | if err != nil { 476 | return err 477 | } 478 | headers["Content-Type"] = "application/json" 479 | headers["Content-Length"] = strconv.Itoa(numBytes) 480 | if numBytes > 4000 { 481 | headers["Expect"] = "100-continue" 482 | } 483 | headers["Accept"] = "application/json" 484 | if resp, err := 485 | db.connection.request("POST", url, requestBody, 486 | headers, db.auth); err == nil { 487 | defer resp.Body.Close() 488 | return parseBody(resp, &results) 489 | } else { 490 | return err 491 | } 492 | } 493 | 494 | //Deletes a document. 495 | //Or rather, tells CouchDB to mark the document as deleted. 496 | //Yes, CouchDB will return a new revision, so this function returns it. 497 | func (db *Database) Delete(id string, rev string) (string, error) { 498 | url, err := buildUrl(db.dbName, id) 499 | if err != nil { 500 | return "", err 501 | } 502 | var headers = make(map[string]string) 503 | headers["Accept"] = "application/json" 504 | headers["If-Match"] = rev 505 | resp, err := db.connection.request("DELETE", url, nil, headers, db.auth) 506 | if err != nil { 507 | return "", err 508 | } 509 | defer resp.Body.Close() 510 | return getRevInfo(resp) 511 | } 512 | 513 | //Saves an attachment. 514 | //docId and docRev refer to the parent document. 515 | //attType is the MIME type of the attachment (ex: image/jpeg) or some such. 516 | //attContent is a byte array containing the actual content. 517 | func (db *Database) SaveAttachment(docId string, 518 | docRev string, attName string, 519 | attType string, attContent io.Reader) (string, error) { 520 | url, err := buildUrl(db.dbName, docId, attName) 521 | if err != nil { 522 | return "", err 523 | } 524 | var headers = make(map[string]string) 525 | headers["Accept"] = "application/json" 526 | headers["Content-Type"] = attType 527 | headers["If-Match"] = docRev 528 | headers["Expect"] = "100-continue" 529 | 530 | resp, err := db.connection.request("PUT", url, attContent, headers, db.auth) 531 | if err != nil { 532 | return "", err 533 | } 534 | defer resp.Body.Close() 535 | return getRevInfo(resp) 536 | } 537 | 538 | //Gets an attachment. 539 | //Returns an io.Reader -- the onus is on the caller to close it. 540 | //Please close it. 541 | func (db *Database) GetAttachment(docId string, docRev string, 542 | attType string, attName string) (io.ReadCloser, error) { 543 | url, err := buildUrl(db.dbName, docId, attName) 544 | if err != nil { 545 | return nil, err 546 | } 547 | var headers = make(map[string]string) 548 | headers["Accept"] = attType 549 | if docRev != "" { 550 | headers["If-Match"] = docRev 551 | } 552 | resp, err := db.connection.request("GET", url, nil, headers, db.auth) 553 | if err != nil { 554 | return nil, err 555 | } 556 | return resp.Body, nil 557 | } 558 | 559 | //Fetches an attachment and proxies the result 560 | func (db *Database) GetAttachmentByProxy(docId string, docRev string, 561 | attType string, attName string, r *http.Request, w http.ResponseWriter) error { 562 | path, err := buildUrl(db.dbName, docId, attName) 563 | if err != nil { 564 | return err 565 | } 566 | var headers = make(map[string]string) 567 | headers["Accept"] = attType 568 | if docRev != "" { 569 | headers["If-Match"] = docRev 570 | } 571 | for k, v := range headers { 572 | r.Header.Set(k, v) 573 | } 574 | return db.connection.reverseProxyRequest(w, r, path, db.auth) 575 | } 576 | 577 | //Deletes an attachment 578 | func (db *Database) DeleteAttachment(docId string, docRev string, 579 | attName string) (string, error) { 580 | url, err := buildUrl(db.dbName, docId, attName) 581 | if err != nil { 582 | return "", err 583 | } 584 | var headers = make(map[string]string) 585 | headers["Accept"] = "application/json" 586 | headers["If-Match"] = docRev 587 | resp, err := db.connection.request("DELETE", url, nil, headers, db.auth) 588 | if err != nil { 589 | return "", err 590 | } 591 | defer resp.Body.Close() 592 | return getRevInfo(resp) 593 | } 594 | 595 | type Members struct { 596 | Users []string `json:"names,omitempty"` 597 | Roles []string `json:"roles,omitempty"` 598 | } 599 | 600 | type Security struct { 601 | Members Members `json:"members"` 602 | Admins Members `json:"admins"` 603 | } 604 | 605 | //Returns the Security document from the database. 606 | func (db *Database) GetSecurity() (*Security, error) { 607 | url, err := buildUrl(db.dbName, "_security") 608 | if err != nil { 609 | return nil, err 610 | } 611 | var headers = make(map[string]string) 612 | sec := Security{} 613 | headers["Accept"] = "application/json" 614 | resp, err := db.connection.request("GET", url, nil, headers, db.auth) 615 | if err != nil { 616 | return nil, err 617 | } 618 | defer resp.Body.Close() 619 | err = parseBody(resp, &sec) 620 | if err != nil { 621 | return nil, err 622 | } 623 | return &sec, err 624 | } 625 | 626 | //Save a security document to the database. 627 | func (db *Database) SaveSecurity(sec Security) error { 628 | url, err := buildUrl(db.dbName, "_security") 629 | if err != nil { 630 | return err 631 | } 632 | var headers = make(map[string]string) 633 | headers["Accept"] = "application/json" 634 | data, numBytes, err := encodeData(sec) 635 | if err != nil { 636 | return err 637 | } 638 | headers["Content-Length"] = strconv.Itoa(numBytes) 639 | if numBytes > 4000 { 640 | headers["Expect"] = "100-continue" 641 | } 642 | resp, err := db.connection.request("PUT", url, data, headers, db.auth) 643 | if err == nil { 644 | resp.Body.Close() 645 | } 646 | return err 647 | } 648 | 649 | // Security helper function. 650 | // Adds a role to a database security doc. 651 | func (db *Database) AddRole(role string, isAdmin bool) error { 652 | sec, err := db.GetSecurity() 653 | if err != nil { 654 | return err 655 | } 656 | roles := func() *[]string { 657 | if isAdmin { 658 | return &sec.Admins.Roles 659 | } else { 660 | return &sec.Members.Roles 661 | } 662 | } 663 | //Make sure the role isn't already there (couchdb will let you add it twice :/ ) 664 | for _, r := range *roles() { 665 | if r == role { 666 | //already there, just return 667 | return nil 668 | } 669 | } 670 | rolesarr := roles() 671 | *rolesarr = append(*rolesarr, role) 672 | return db.SaveSecurity(*sec) 673 | } 674 | 675 | // Security helper function. 676 | // Removes a role from a database security doc. 677 | func (db *Database) RemoveRole(role string) error { 678 | sec, err := db.GetSecurity() 679 | if err != nil { 680 | return err 681 | } 682 | remove := func(isAdmin bool) bool { 683 | var rolesPtr *[]string 684 | if isAdmin { 685 | rolesPtr = &sec.Admins.Roles 686 | } else { 687 | rolesPtr = &sec.Members.Roles 688 | } 689 | roles := *rolesPtr 690 | for i, r := range roles { 691 | if r == role { 692 | *rolesPtr = append(roles[:i], roles[i+1:]...) 693 | return true 694 | } 695 | } 696 | return false 697 | } 698 | var removed bool = false 699 | if removed = remove(false); !removed { 700 | removed = remove(true) 701 | } 702 | if removed { 703 | return db.SaveSecurity(*sec) 704 | } 705 | return nil 706 | } 707 | 708 | //Get the results of a view. 709 | func (db *Database) GetView(designDoc string, view string, 710 | results interface{}, params *url.Values) error { 711 | var err error 712 | var url string 713 | if params == nil { 714 | url, err = buildUrl(db.dbName, "_design", designDoc, "_view", view) 715 | } else { 716 | url, err = buildParamUrl(*params, db.dbName, "_design", 717 | designDoc, "_view", view) 718 | } 719 | if err != nil { 720 | return err 721 | } 722 | var headers = make(map[string]string) 723 | headers["Accept"] = "application/json" 724 | resp, err := db.connection.request("GET", url, nil, headers, db.auth) 725 | if err != nil { 726 | return err 727 | } 728 | defer resp.Body.Close() 729 | 730 | err = parseBody(resp, &results) 731 | if err != nil { 732 | return err 733 | } 734 | return nil 735 | } 736 | 737 | //Get multiple results of a view. 738 | func (db *Database) GetMultipleFromView(designDoc string, view string, 739 | results interface{}, keys []string) error { 740 | var err error 741 | var url string 742 | type RequestBody struct { 743 | Keys []string `json:"keys"` 744 | } 745 | url, err = buildUrl(db.dbName, "_design", designDoc, "_view", view) 746 | if err != nil { 747 | return err 748 | } 749 | fmt.Errorf("url: " + url) 750 | var headers = make(map[string]string) 751 | reqBody := RequestBody{Keys: keys} 752 | requestBody, numBytes, err := encodeData(reqBody) 753 | if err != nil { 754 | return err 755 | } 756 | headers["Content-Type"] = "application/json" 757 | headers["Content-Length"] = strconv.Itoa(numBytes) 758 | if numBytes > 4000 { 759 | headers["Expect"] = "100-continue" 760 | } 761 | headers["Accept"] = "application/json" 762 | if resp, err := 763 | db.connection.request("POST", url, requestBody, 764 | headers, db.auth); err == nil { 765 | defer resp.Body.Close() 766 | return parseBody(resp, &results) 767 | } else { 768 | return err 769 | } 770 | } 771 | 772 | //Get the result of a list operation 773 | //This assumes your list function in couchdb returns JSON 774 | func (db *Database) GetList(designDoc string, list string, 775 | view string, results interface{}, params *url.Values) error { 776 | var err error 777 | var url string 778 | if params == nil { 779 | url, err = buildUrl(db.dbName, "_design", designDoc, "_list", 780 | list, view) 781 | } else { 782 | url, err = buildParamUrl(*params, db.dbName, "_design", designDoc, 783 | "_list", list, view) 784 | } 785 | if err != nil { 786 | return err 787 | } 788 | var headers = make(map[string]string) 789 | headers["Accept"] = "application/json" 790 | resp, err := db.connection.request("GET", url, nil, nil, db.auth) 791 | if err != nil { 792 | return err 793 | } 794 | defer resp.Body.Close() 795 | err = parseBody(resp, &results) 796 | if err != nil { 797 | return err 798 | } 799 | return nil 800 | } 801 | 802 | type FindQueryParams struct { 803 | Selector interface{} `json:"selector"` 804 | Limit int `json:"limit,omitempty"` 805 | Skip int `json:"skip,omitempty"` 806 | Sort interface{} `json:"sort,omitempty"` 807 | Fields []string `json:"fields,omitempty"` 808 | UseIndex interface{} `json:"user_index,omitempty"` 809 | } 810 | 811 | func (db *Database) Find(results interface{}, params *FindQueryParams) error { 812 | var err error 813 | var url string 814 | url, err = buildUrl(db.dbName, "_find") 815 | if err != nil { 816 | return err 817 | } 818 | 819 | requestBody, numBytes, err := encodeData(params) 820 | if err != nil { 821 | return err 822 | } 823 | 824 | var headers = make(map[string]string) 825 | headers["Content-Type"] = "application/json" 826 | headers["Accept"] = "application/json" 827 | headers["Content-Length"] = strconv.Itoa(numBytes) 828 | 829 | resp, err := db.connection.request("POST", url, requestBody, headers, db.auth) 830 | if err != nil { 831 | return err 832 | } 833 | defer resp.Body.Close() 834 | 835 | err = parseBody(resp, &results) 836 | if err != nil { 837 | return err 838 | } 839 | return nil 840 | } 841 | 842 | //Save a design document. 843 | //If creating a new design doc, set rev to "". 844 | func (db *Database) SaveDesignDoc(name string, 845 | designDoc interface{}, rev string) (string, error) { 846 | path := "_design/" + name 847 | newRev, err := db.Save(designDoc, path, rev) 848 | if err != nil { 849 | return "", err 850 | } else if newRev == "" { 851 | return "", fmt.Errorf("CouchDB returned an empty revision string.") 852 | } 853 | return newRev, nil 854 | 855 | } 856 | -------------------------------------------------------------------------------- /couchdb2.1-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | sudo apt-get --no-install-recommends -y install \ 6 | build-essential pkg-config erlang \ 7 | libicu-dev libmozjs185-dev libcurl4-openssl-dev 8 | 9 | mkdir temp 10 | cd temp 11 | 12 | wget http://www.trieuvan.com/apache/couchdb/source/2.1.1/apache-couchdb-2.1.1.tar.gz 13 | 14 | tar -xzf apache-couchdb-2.1.1.tar.gz 15 | cd apache-couchdb-2.1.1 16 | ./configure 17 | make release 18 | nohup ./rel/couchdb/bin/couchdb > /dev/null & 19 | 20 | #Give couch a chance to start 21 | sleep 15 22 | 23 | curl -X PUT http://127.0.0.1:5984/_users 24 | 25 | curl -X PUT http://127.0.0.1:5984/_replicator 26 | 27 | cd .. 28 | -------------------------------------------------------------------------------- /couchdb_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/twinj/uuid" 7 | "io/ioutil" 8 | "math/rand" 9 | "strconv" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var timeout = time.Duration(500 * time.Millisecond) 15 | var unittestdb = "unittestdb" 16 | var server = "127.0.0.1" 17 | var numDbs = 1 18 | var adminAuth = &BasicAuth{Username: "adminuser", Password: "password"} 19 | 20 | type TestDocument struct { 21 | Title string 22 | Note string 23 | } 24 | 25 | type ViewResult struct { 26 | Id string `json:"id"` 27 | Key TestDocument `json:"key"` 28 | } 29 | 30 | type ViewResponse struct { 31 | TotalRows int `json:"total_rows"` 32 | Offset int `json:"offset"` 33 | Rows []ViewResult `json:"rows,omitempty"` 34 | } 35 | 36 | type MultiReadResponse struct { 37 | TotalRows int `json:"total_rows"` 38 | Offset int `json:"offset"` 39 | Rows []MultiReadRow `json:"rows"` 40 | } 41 | 42 | type MultiReadRow struct { 43 | Id string `json:"id"` 44 | Key string `json:"key"` 45 | Doc TestDocument `json:"doc"` 46 | } 47 | 48 | type ListResult struct { 49 | Id string `json:"id"` 50 | Key TestDocument `json:"key"` 51 | //Value string `json:"value"` 52 | } 53 | 54 | type ListResponse struct { 55 | TotalRows int `json:"total_rows"` 56 | Offset int `json:"offset"` 57 | Rows []ListResult `json:"rows,omitempty"` 58 | } 59 | 60 | type FindResponse struct { 61 | Docs []TestDocument `json:"docs"` 62 | } 63 | 64 | type View struct { 65 | Map string `json:"map"` 66 | Reduce string `json:"reduce,omitempty"` 67 | } 68 | 69 | type DesignDocument struct { 70 | Language string `json:"language"` 71 | Views map[string]View `json:"views"` 72 | Lists map[string]string `json:"lists"` 73 | } 74 | 75 | func getUuid() string { 76 | theUuid := uuid.NewV4() 77 | return uuid.Formatter(theUuid, uuid.FormatHex) 78 | } 79 | 80 | func getConnection(t *testing.T) *Connection { 81 | conn, err := NewConnection(server, 5984, timeout) 82 | if err != nil { 83 | t.Logf("ERROR: %v", err) 84 | t.Fail() 85 | } 86 | return conn 87 | } 88 | 89 | /*func getAuthConnection(t *testing.T) *Connection { 90 | auth := Auth{Username: "adminuser", Password: "password"} 91 | conn, err := NewConnection(server, 5984, timeout) 92 | if err != nil { 93 | t.Logf("ERROR: %v", err) 94 | t.Fail() 95 | } 96 | return conn 97 | }*/ 98 | 99 | func createTestDb(t *testing.T) string { 100 | conn := getConnection(t) 101 | dbName := unittestdb + strconv.Itoa(numDbs) 102 | err := conn.CreateDB(dbName, adminAuth) 103 | errorify(t, err) 104 | numDbs += 1 105 | return dbName 106 | } 107 | 108 | func deleteTestDb(t *testing.T, dbName string) { 109 | conn := getConnection(t) 110 | err := conn.DeleteDB(dbName, adminAuth) 111 | errorify(t, err) 112 | } 113 | 114 | func genRandomText(n int) string { 115 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 116 | 117 | b := make([]rune, n) 118 | for i := range b { 119 | b[i] = letters[rand.Intn(len(letters))] 120 | } 121 | return string(b) 122 | } 123 | 124 | func createLotsDocs(t *testing.T, db *Database) { 125 | for i := 0; i < 10; i++ { 126 | id := getUuid() 127 | note := "purple" 128 | if i%2 == 0 { 129 | note = "magenta" 130 | } 131 | testDoc := TestDocument{ 132 | Title: "TheDoc -- " + strconv.Itoa(i), 133 | Note: note, 134 | } 135 | _, err := db.Save(testDoc, id, "") 136 | errorify(t, err) 137 | } 138 | } 139 | 140 | func errorify(t *testing.T, err error) { 141 | if err != nil { 142 | t.Logf("ERROR: %v", err) 143 | t.Fail() 144 | } 145 | } 146 | 147 | func TestPing(t *testing.T) { 148 | conn := getConnection(t) 149 | pingErr := conn.Ping() 150 | errorify(t, pingErr) 151 | } 152 | 153 | func TestBadPing(t *testing.T) { 154 | conn, err := NewConnection("unpingable", 1234, timeout) 155 | errorify(t, err) 156 | pingErr := conn.Ping() 157 | if pingErr == nil { 158 | t.Fail() 159 | } 160 | } 161 | 162 | func TestGetDBList(t *testing.T) { 163 | conn := getConnection(t) 164 | dbList, err := conn.GetDBList() 165 | errorify(t, err) 166 | if len(dbList) <= 0 { 167 | t.Logf("No results!") 168 | t.Fail() 169 | } else { 170 | for i, dbName := range dbList { 171 | t.Logf("Database %v: %v\n", i, dbName) 172 | } 173 | } 174 | } 175 | 176 | func TestCreateDB(t *testing.T) { 177 | conn := getConnection(t) 178 | err := conn.CreateDB("testcreatedb", adminAuth) 179 | errorify(t, err) 180 | //try to create it again --- should fail 181 | err = conn.CreateDB("testcreatedb", adminAuth) 182 | if err == nil { 183 | t.Fail() 184 | } 185 | //now delete it 186 | err = conn.DeleteDB("testcreatedb", adminAuth) 187 | errorify(t, err) 188 | } 189 | 190 | func TestSave(t *testing.T) { 191 | dbName := createTestDb(t) 192 | conn := getConnection(t) 193 | //Create a new document 194 | theDoc := TestDocument{ 195 | Title: "My Document", 196 | Note: "This is my note", 197 | } 198 | db := conn.SelectDB(dbName, nil) 199 | theId := getUuid() 200 | //Save it 201 | t.Logf("Saving first\n") 202 | rev, err := db.Save(theDoc, theId, "") 203 | errorify(t, err) 204 | t.Logf("New Document ID: %s\n", theId) 205 | t.Logf("New Document Rev: %s\n", rev) 206 | t.Logf("New Document Title: %v\n", theDoc.Title) 207 | t.Logf("New Document Note: %v\n", theDoc.Note) 208 | if theDoc.Title != "My Document" || 209 | theDoc.Note != "This is my note" || rev == "" { 210 | t.Fail() 211 | } 212 | //Now, let's try updating it 213 | theDoc.Note = "A new note" 214 | t.Logf("Saving again\n") 215 | rev, err = db.Save(theDoc, theId, rev) 216 | errorify(t, err) 217 | t.Logf("Updated Document Id: %s\n", theId) 218 | t.Logf("Updated Document Rev: %s\n", rev) 219 | t.Logf("Updated Document Title: %v\n", theDoc.Title) 220 | t.Logf("Updated Document Note: %v\n", theDoc.Note) 221 | if theDoc.Note != "A new note" { 222 | t.Fail() 223 | } 224 | deleteTestDb(t, dbName) 225 | } 226 | 227 | func TestAttachment(t *testing.T) { 228 | dbName := createTestDb(t) 229 | conn := getConnection(t) 230 | //Create a new document 231 | theDoc := TestDocument{ 232 | Title: "My Document", 233 | Note: "This one has attachments", 234 | } 235 | db := conn.SelectDB(dbName, nil) 236 | theId := getUuid() 237 | //Save it 238 | t.Logf("Saving document\n") 239 | rev, err := db.Save(theDoc, theId, "") 240 | errorify(t, err) 241 | t.Logf("New Document Id: %s\n", theId) 242 | t.Logf("New Document Rev: %s\n", rev) 243 | t.Logf("New Document Title: %v\n", theDoc.Title) 244 | t.Logf("New Document Note: %v\n", theDoc.Note) 245 | //Create some content 246 | content := []byte("THIS IS MY ATTACHMENT") 247 | contentReader := bytes.NewReader(content) 248 | //Now Add an attachment 249 | uRev, err := db.SaveAttachment(theId, rev, "attachment", "text/plain", contentReader) 250 | errorify(t, err) 251 | t.Logf("Updated Rev: %s\n", uRev) 252 | //Now try to read it 253 | theContent, err := db.GetAttachment(theId, uRev, "text/plain", "attachment") 254 | errorify(t, err) 255 | defer theContent.Close() 256 | theBytes, err := ioutil.ReadAll(theContent) 257 | errorify(t, err) 258 | t.Logf("how much data: %v\n", len(theBytes)) 259 | data := string(theBytes[:]) 260 | if data != "THIS IS MY ATTACHMENT" { 261 | t.Fail() 262 | } 263 | t.Logf("The data: %v\n", data) 264 | //Now delete it 265 | dRev, err := db.DeleteAttachment(theId, uRev, "attachment") 266 | errorify(t, err) 267 | t.Logf("Deleted revision: %v\n", dRev) 268 | deleteTestDb(t, dbName) 269 | } 270 | 271 | func TestRead(t *testing.T) { 272 | dbName := createTestDb(t) 273 | conn := getConnection(t) 274 | db := conn.SelectDB(dbName, nil) 275 | //Create a test doc 276 | theDoc := TestDocument{ 277 | Title: "My Document", 278 | Note: "Time to read", 279 | } 280 | emptyDoc := TestDocument{} 281 | //Save it 282 | theId := getUuid() 283 | _, err := db.Save(theDoc, theId, "") 284 | errorify(t, err) 285 | //Now try to read it 286 | rev, err := db.Read(theId, &emptyDoc, nil) 287 | errorify(t, err) 288 | t.Logf("Document Id: %v\n", theId) 289 | t.Logf("Document Rev: %v\n", rev) 290 | t.Logf("Document Title: %v\n", emptyDoc.Title) 291 | t.Logf("Document Note: %v\n", emptyDoc.Note) 292 | deleteTestDb(t, dbName) 293 | } 294 | 295 | func TestMultiRead(t *testing.T) { 296 | dbName := createTestDb(t) 297 | conn := getConnection(t) 298 | db := conn.SelectDB(dbName, nil) 299 | //Create a test doc 300 | theDoc := TestDocument{ 301 | Title: "My Document", 302 | Note: "Time to read", 303 | } 304 | //Save it 305 | theId := getUuid() 306 | _, err := db.Save(theDoc, theId, "") 307 | errorify(t, err) 308 | //Create another test doc 309 | theOtherDoc := TestDocument{ 310 | Title: "My Other Document", 311 | Note: "TIme to unread", 312 | } 313 | //Save it 314 | otherId := getUuid() 315 | _, err = db.Save(theOtherDoc, otherId, "") 316 | errorify(t, err) 317 | //Now, try to read them 318 | readDocs := MultiReadResponse{} 319 | keys := []string{theId, otherId} 320 | err = db.ReadMultiple(keys, &readDocs) 321 | errorify(t, err) 322 | t.Logf("\nThe Docs! %v", readDocs) 323 | if len(readDocs.Rows) != 2 { 324 | t.Errorf("Should be 2 results!") 325 | } 326 | deleteTestDb(t, dbName) 327 | } 328 | 329 | func TestCopy(t *testing.T) { 330 | dbName := createTestDb(t) 331 | conn := getConnection(t) 332 | db := conn.SelectDB(dbName, nil) 333 | //Create a test doc 334 | theDoc := TestDocument{ 335 | Title: "My Document", 336 | Note: "Time to read", 337 | } 338 | emptyDoc := TestDocument{} 339 | //Save it 340 | theId := getUuid() 341 | rev, err := db.Save(theDoc, theId, "") 342 | errorify(t, err) 343 | //Now copy it 344 | copyId := getUuid() 345 | copyRev, err := db.Copy(theId, "", copyId) 346 | errorify(t, err) 347 | t.Logf("Document Id: %v\n", theId) 348 | t.Logf("Document Rev: %v\n", rev) 349 | //Now read the copy 350 | _, err = db.Read(copyId, &emptyDoc, nil) 351 | errorify(t, err) 352 | t.Logf("Document Title: %v\n", emptyDoc.Title) 353 | t.Logf("Document Note: %v\n", emptyDoc.Note) 354 | t.Logf("Copied Doc Rev: %v\n", copyRev) 355 | deleteTestDb(t, dbName) 356 | } 357 | 358 | func TestDelete(t *testing.T) { 359 | dbName := createTestDb(t) 360 | conn := getConnection(t) 361 | db := conn.SelectDB(dbName, nil) 362 | //Create a test doc 363 | theDoc := TestDocument{ 364 | Title: "My Document", 365 | Note: "Time to read", 366 | } 367 | theId := getUuid() 368 | rev, err := db.Save(theDoc, theId, "") 369 | errorify(t, err) 370 | //Now delete it 371 | newRev, err := db.Delete(theId, rev) 372 | errorify(t, err) 373 | t.Logf("Document Id: %v\n", theId) 374 | t.Logf("Document Rev: %v\n", rev) 375 | t.Logf("Deleted Rev: %v\n", newRev) 376 | if newRev == "" || newRev == rev { 377 | t.Fail() 378 | } 379 | deleteTestDb(t, dbName) 380 | } 381 | 382 | func TestUser(t *testing.T) { 383 | dbName := createTestDb(t) 384 | conn := getConnection(t) 385 | //Save a User 386 | t.Logf("AdminAuth: %v\n", adminAuth) 387 | rev, err := conn.AddUser("turd.ferguson", 388 | "password", []string{"loser"}, adminAuth) 389 | errorify(t, err) 390 | t.Logf("User Rev: %v\n", rev) 391 | if rev == "" { 392 | t.Fail() 393 | } 394 | //check user can access db 395 | db := conn.SelectDB(dbName, &BasicAuth{"turd.ferguson", "password"}) 396 | theId := getUuid() 397 | docRev, err := db.Save(&TestDocument{Title: "My doc"}, theId, "") 398 | errorify(t, err) 399 | t.Logf("Granting role to user") 400 | //check session info 401 | authInfo, err := conn.GetAuthInfo(&BasicAuth{"turd.ferguson", "password"}) 402 | errorify(t, err) 403 | t.Logf("AuthInfo: %v", authInfo) 404 | if authInfo.UserCtx.Name != "turd.ferguson" { 405 | t.Errorf("UserCtx name wrong: %v", authInfo.UserCtx.Name) 406 | } 407 | //grant a role 408 | rev, err = conn.GrantRole("turd.ferguson", 409 | "fool", adminAuth) 410 | errorify(t, err) 411 | t.Logf("Updated Rev: %v\n", rev) 412 | //read the user 413 | userData := UserRecord{} 414 | rev, err = conn.GetUser("turd.ferguson", &userData, adminAuth) 415 | errorify(t, err) 416 | if len(userData.Roles) != 2 { 417 | t.Error("Not enough roles") 418 | } 419 | t.Logf("Roles: %v", userData.Roles) 420 | //check user can access db 421 | docRev, err = db.Save(&TestDocument{Title: "My doc"}, theId, docRev) 422 | errorify(t, err) 423 | 424 | //revoke a role 425 | rev, err = conn.RevokeRole("turd.ferguson", 426 | "loser", adminAuth) 427 | errorify(t, err) 428 | t.Logf("Updated Rev: %v\n", rev) 429 | //read the user 430 | rev, err = conn.GetUser("turd.ferguson", &userData, adminAuth) 431 | errorify(t, err) 432 | if len(userData.Roles) != 1 { 433 | t.Error("should only be 1 role") 434 | } 435 | t.Logf("Roles: %v", userData.Roles) 436 | dRev, err := conn.DeleteUser("turd.ferguson", rev, adminAuth) 437 | errorify(t, err) 438 | t.Logf("Del User Rev: %v\n", dRev) 439 | if rev == dRev || dRev == "" { 440 | t.Fail() 441 | } 442 | deleteTestDb(t, dbName) 443 | } 444 | 445 | func TestSecurity(t *testing.T) { 446 | conn := getConnection(t) 447 | dbName := createTestDb(t) 448 | db := conn.SelectDB(dbName, adminAuth) 449 | 450 | members := Members{ 451 | Users: []string{"joe, bill"}, 452 | Roles: []string{"code monkeys"}, 453 | } 454 | admins := Members{ 455 | Users: []string{"bossman"}, 456 | Roles: []string{"boss"}, 457 | } 458 | security := Security{ 459 | Members: members, 460 | Admins: admins, 461 | } 462 | err := db.SaveSecurity(security) 463 | errorify(t, err) 464 | err = db.AddRole("sales", false) 465 | errorify(t, err) 466 | err = db.AddRole("uberboss", true) 467 | errorify(t, err) 468 | sec, err := db.GetSecurity() 469 | t.Logf("Security: %v\n", sec) 470 | if sec.Admins.Users[0] != "bossman" { 471 | t.Fail() 472 | } 473 | if sec.Admins.Roles[0] != "boss" { 474 | t.Fail() 475 | } 476 | if sec.Admins.Roles[1] != "uberboss" { 477 | t.Errorf("\nAdmin Roles nto right! %v\n", sec.Admins.Roles[1]) 478 | } 479 | if sec.Members.Roles[1] != "sales" { 480 | t.Errorf("\nRoles not right! %v\n", sec.Members.Roles[1]) 481 | } 482 | errorify(t, err) 483 | err = db.RemoveRole("sales") 484 | errorify(t, err) 485 | err = db.RemoveRole("uberboss") 486 | errorify(t, err) 487 | //try removing a role that ain't there 488 | err = db.RemoveRole("WHATROLE") 489 | errorify(t, err) 490 | sec, err = db.GetSecurity() 491 | t.Logf("Secuirty: %v\n", sec) 492 | if len(sec.Members.Roles) > 1 { 493 | t.Errorf("\nThe Role was not removed: %v\n", sec.Members.Roles) 494 | } else if sec.Members.Roles[0] == "sales" { 495 | t.Errorf("\nThe roles are all messed up: %v\n", sec.Members.Roles) 496 | } 497 | if len(sec.Admins.Roles) > 1 { 498 | t.Errorf("\nThe Admin Role was not removed: %v\n", sec.Admins.Roles) 499 | } 500 | deleteTestDb(t, dbName) 501 | } 502 | 503 | func TestSessions(t *testing.T) { 504 | conn := getConnection(t) 505 | dbName := createTestDb(t) 506 | defer deleteTestDb(t, dbName) 507 | //Save a User 508 | t.Logf("AdminAuth: %v\n", adminAuth) 509 | rev, err := conn.AddUser("turd.ferguson", 510 | "password", []string{"loser"}, adminAuth) 511 | errorify(t, err) 512 | t.Logf("User Rev: %v\n", rev) 513 | defer conn.DeleteUser("turd.ferguson", rev, adminAuth) 514 | if rev == "" { 515 | t.Fail() 516 | } 517 | //Create a session for the user 518 | cookieAuth, err := conn.CreateSession("turd.ferguson", "password") 519 | errorify(t, err) 520 | //sleep 521 | time.Sleep(time.Duration(2 * time.Second)) 522 | //Create something 523 | db := conn.SelectDB(dbName, cookieAuth) 524 | theId := getUuid() 525 | docRev, err := db.Save(&TestDocument{Title: "The test doc"}, theId, "") 526 | errorify(t, err) 527 | t.Logf("Document Rev: %v", docRev) 528 | t.Logf("Updated Auth: %v", cookieAuth.GetUpdatedAuth()["AuthSession"]) 529 | //Delete the user session 530 | err = conn.DestroySession(cookieAuth) 531 | errorify(t, err) 532 | } 533 | 534 | func TestSetConfig(t *testing.T) { 535 | conn := getConnection(t) 536 | err := conn.SetConfig("couch_httpd_auth", "timeout", "30", adminAuth) 537 | errorify(t, err) 538 | } 539 | 540 | func TestGetConfig(t *testing.T) { 541 | conn := getConnection(t) 542 | val, err := conn.GetConfigOption("couch_httpd_auth", "authentication_db", adminAuth) 543 | errorify(t, err) 544 | if val != "_users" { 545 | t.Error("The auth db is wrong: %v", val) 546 | } 547 | t.Logf("The auth db is : %v", val) 548 | val, err = conn.GetConfigOption("couch_httpd_auth", "auth_cache_size", adminAuth) 549 | if val != "50" { 550 | t.Error("The auth cache size is wrong: %v", val) 551 | } 552 | t.Logf("Auth cache size is : %v", val) 553 | val, err = conn.GetConfigOption("httpd", "allow_jsonp", adminAuth) 554 | if val != "false" { 555 | t.Error("allow jsonp value is wrong: %v", val) 556 | } 557 | t.Logf("Allow JSONP is : %v", val) 558 | } 559 | 560 | func TestDesignDocs(t *testing.T) { 561 | conn := getConnection(t) 562 | dbName := createTestDb(t) 563 | db := conn.SelectDB(dbName, adminAuth) 564 | createLotsDocs(t, db) 565 | 566 | view := View{ 567 | Map: "function(doc) {\n if (doc.Note === \"magenta\"){\n emit(doc)\n }\n}", 568 | } 569 | views := make(map[string]View) 570 | views["find_all_magenta"] = view 571 | lists := make(map[string]string) 572 | 573 | lists["getList"] = 574 | `function(head, req){ 575 | var row; 576 | var response={ 577 | total_rows:0, 578 | offset:0, 579 | rows:[] 580 | }; 581 | while(row=getRow()){ 582 | response.rows.push(row); 583 | } 584 | response.total_rows = response.rows.length; 585 | send(toJSON(response)) 586 | }` 587 | 588 | ddoc := DesignDocument{ 589 | Language: "javascript", 590 | Views: views, 591 | Lists: lists, 592 | } 593 | rev, err := db.SaveDesignDoc("colors", ddoc, "") 594 | errorify(t, err) 595 | if rev == "" { 596 | t.Fail() 597 | } else { 598 | t.Logf("Rev of design doc: %v\n", rev) 599 | } 600 | //Now, read the design doc 601 | readDdoc := DesignDocument{} 602 | _, err = db.Read("_design/colors", &readDdoc, nil) 603 | if err != nil { 604 | errorify(t, err) 605 | } 606 | result := ViewResponse{} 607 | //now try to query the view 608 | err = db.GetView("colors", "find_all_magenta", &result, nil) 609 | errorify(t, err) 610 | if len(result.Rows) != 5 { 611 | t.Logf("docList length: %v\n", len(result.Rows)) 612 | t.Fail() 613 | } else { 614 | t.Logf("Results: %v\n", result.Rows) 615 | } 616 | listResult := ListResponse{} 617 | err = db.GetList("colors", "getList", "find_all_magenta", &listResult, nil) 618 | if err != nil { 619 | t.Logf("ERROR: %v", err) 620 | } 621 | errorify(t, err) 622 | if len(listResult.Rows) != 5 { 623 | t.Logf("List Result: %v\n", listResult) 624 | t.Logf("docList length: %v\n", len(listResult.Rows)) 625 | t.Fail() 626 | } else { 627 | t.Logf("List Results: %v\n", listResult) 628 | } 629 | deleteTestDb(t, dbName) 630 | 631 | } 632 | 633 | //Test for a specific situation I've been having trouble with 634 | func TestAngryCouch(t *testing.T) { 635 | 636 | testDoc1 := TestDocument{ 637 | Title: "Test Doc 1", 638 | Note: genRandomText(8000), 639 | } 640 | testDoc2 := TestDocument{ 641 | Title: "Test Doc 2", 642 | Note: genRandomText(1000), 643 | } 644 | 645 | dbName := createTestDb(t) 646 | defer deleteTestDb(t, dbName) 647 | conn := getConnection(t) 648 | db := conn.SelectDB(dbName, nil) 649 | //client := &http.Client{} 650 | id1 := getUuid() 651 | id2 := getUuid() 652 | /*req, err := http.NewRequest( 653 | "PUT", 654 | "http://localhost:5984/"+dbName+"/"+id1, 655 | bytes.NewReader(testBody1), 656 | ) 657 | req.Header.Set("Content-Type", "application/json") 658 | req.Header.Set("Accept", "application/json") 659 | req.Header.Set("Expect", "100-continue")*/ 660 | rev, err := db.Save(testDoc1, id1, "") 661 | //resp, err := client.Do(req) 662 | errorify(t, err) 663 | //defer resp.Body.Close() 664 | t.Logf("Doc 1 Rev: %v\n", rev) 665 | errorify(t, err) 666 | rev, err = db.Save(testDoc2, id2, "") 667 | t.Logf("Doc 2 Rev: %v\n", rev) 668 | errorify(t, err) 669 | } 670 | 671 | func TestFind(t *testing.T) { 672 | conn := getConnection(t) 673 | dbName := createTestDb(t) 674 | db := conn.SelectDB(dbName, adminAuth) 675 | createLotsDocs(t, db) 676 | 677 | selector := `{"Note": {"$eq": "magenta"}}` 678 | 679 | var selectorObj interface{} 680 | 681 | err := json.Unmarshal([]byte(selector), &selectorObj) 682 | 683 | if err != nil { 684 | errorify(t, err) 685 | } 686 | 687 | //Get the results from find. 688 | findResult := FindResponse{} 689 | 690 | params := FindQueryParams{Selector: &selectorObj} 691 | 692 | err = db.Find(&findResult, ¶ms) 693 | 694 | if err != nil { 695 | errorify(t, err) 696 | } 697 | 698 | if len(findResult.Docs) != 5 { 699 | t.Logf("Find Results Length: %v\n", len(findResult.Docs)) 700 | t.Fail() 701 | } else { 702 | t.Logf("Results: %v\n", len(findResult.Docs)) 703 | } 704 | 705 | deleteTestDb(t, dbName) 706 | 707 | } 708 | --------------------------------------------------------------------------------