github.com/adnan-c/fabric_e2e_couchdb@v0.6.1-preview.0.20170228180935-21ce6b23cf91/core/ledger/util/couchdb/couchdb.go (about) 1 /* 2 Copyright IBM Corp. 2016, 2017 All Rights Reserved. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package couchdb 18 19 import ( 20 "bytes" 21 "compress/gzip" 22 "encoding/json" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "log" 27 "mime" 28 "mime/multipart" 29 "net/http" 30 "net/http/httputil" 31 "net/textproto" 32 "net/url" 33 "regexp" 34 "strconv" 35 "strings" 36 "unicode/utf8" 37 38 logging "github.com/op/go-logging" 39 ) 40 41 var logger = logging.MustGetLogger("couchdb") 42 43 // DBOperationResponse is body for successful database calls. 44 type DBOperationResponse struct { 45 Ok bool 46 id string 47 rev string 48 } 49 50 // DBInfo is body for database information. 51 type DBInfo struct { 52 DbName string `json:"db_name"` 53 UpdateSeq string `json:"update_seq"` 54 Sizes struct { 55 File int `json:"file"` 56 External int `json:"external"` 57 Active int `json:"active"` 58 } `json:"sizes"` 59 PurgeSeq int `json:"purge_seq"` 60 Other struct { 61 DataSize int `json:"data_size"` 62 } `json:"other"` 63 DocDelCount int `json:"doc_del_count"` 64 DocCount int `json:"doc_count"` 65 DiskSize int `json:"disk_size"` 66 DiskFormatVersion int `json:"disk_format_version"` 67 DataSize int `json:"data_size"` 68 CompactRunning bool `json:"compact_running"` 69 InstanceStartTime string `json:"instance_start_time"` 70 } 71 72 //ConnectionInfo is a structure for capturing the database info and version 73 type ConnectionInfo struct { 74 Couchdb string `json:"couchdb"` 75 Version string `json:"version"` 76 Vendor struct { 77 Name string `json:"name"` 78 } `json:"vendor"` 79 } 80 81 //RangeQueryResponse is used for processing REST range query responses from CouchDB 82 type RangeQueryResponse struct { 83 TotalRows int `json:"total_rows"` 84 Offset int `json:"offset"` 85 Rows []struct { 86 ID string `json:"id"` 87 Key string `json:"key"` 88 Value struct { 89 Rev string `json:"rev"` 90 } `json:"value"` 91 Doc json.RawMessage `json:"doc"` 92 } `json:"rows"` 93 } 94 95 //QueryResponse is used for processing REST query responses from CouchDB 96 type QueryResponse struct { 97 Warning string `json:"warning"` 98 Docs []json.RawMessage `json:"docs"` 99 } 100 101 //Doc is used for capturing if attachments are return in the query from CouchDB 102 type Doc struct { 103 ID string `json:"_id"` 104 Rev string `json:"_rev"` 105 Attachments json.RawMessage `json:"_attachments"` 106 } 107 108 //DocID is a minimal structure for capturing the ID from a query result 109 type DocID struct { 110 ID string `json:"_id"` 111 } 112 113 //QueryResult is used for returning query results from CouchDB 114 type QueryResult struct { 115 ID string 116 Value []byte 117 Attachments []Attachment 118 } 119 120 //CouchConnectionDef contains parameters 121 type CouchConnectionDef struct { 122 URL string 123 Username string 124 Password string 125 } 126 127 //CouchInstance represents a CouchDB instance 128 type CouchInstance struct { 129 conf CouchConnectionDef //connection configuration 130 } 131 132 //CouchDatabase represents a database within a CouchDB instance 133 type CouchDatabase struct { 134 couchInstance CouchInstance //connection configuration 135 dbName string 136 } 137 138 //DBReturn contains an error reported by CouchDB 139 type DBReturn struct { 140 StatusCode int `json:"status_code"` 141 Error string `json:"error"` 142 Reason string `json:"reason"` 143 } 144 145 //Attachment contains the definition for an attached file for couchdb 146 type Attachment struct { 147 Name string 148 ContentType string 149 Length uint64 150 AttachmentBytes []byte 151 } 152 153 //DocRev returns the Id and revision for a couchdb document 154 type DocRev struct { 155 Id string `json:"_id"` 156 Rev string `json:"_rev"` 157 } 158 159 //FileDetails defines the structure needed to send an attachment to couchdb 160 type FileDetails struct { 161 Follows bool `json:"follows"` 162 ContentType string `json:"content_type"` 163 Length int `json:"length"` 164 } 165 166 //CouchDoc defines the structure for a JSON document value 167 type CouchDoc struct { 168 JSONValue []byte 169 Attachments []Attachment 170 } 171 172 //CreateConnectionDefinition for a new client connection 173 func CreateConnectionDefinition(couchDBAddress, username, password string) (*CouchConnectionDef, error) { 174 175 logger.Debugf("Entering CreateConnectionDefinition()") 176 177 //connectURL := fmt.Sprintf("%s//%s", "http:", couchDBAddress) 178 //connectURL := couchDBAddress 179 180 connectURL := &url.URL{ 181 Host: couchDBAddress, 182 Scheme: "http", 183 } 184 185 //parse the constructed URL to verify no errors 186 finalURL, err := url.Parse(connectURL.String()) 187 if err != nil { 188 logger.Errorf("URL parse error: %s", err.Error()) 189 return nil, err 190 } 191 192 logger.Debugf("Created database configuration URL=[%s]", finalURL.String()) 193 logger.Debugf("Exiting CreateConnectionDefinition()") 194 195 //return an object containing the connection information 196 return &CouchConnectionDef{finalURL.String(), username, password}, nil 197 } 198 199 //CreateDatabaseIfNotExist method provides function to create database 200 func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse, error) { 201 202 logger.Debugf("Entering CreateDatabaseIfNotExist()") 203 204 dbInfo, couchDBReturn, err := dbclient.GetDatabaseInfo() 205 if err != nil { 206 if couchDBReturn == nil || couchDBReturn.StatusCode != 404 { 207 return nil, err 208 } 209 } 210 211 if dbInfo == nil && couchDBReturn.StatusCode == 404 { 212 213 logger.Debugf("Database %s does not exist.", dbclient.dbName) 214 215 connectURL, err := url.Parse(dbclient.couchInstance.conf.URL) 216 if err != nil { 217 logger.Errorf("URL parse error: %s", err.Error()) 218 return nil, err 219 } 220 connectURL.Path = dbclient.dbName 221 222 //process the URL with a PUT, creates the database 223 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "") 224 if err != nil { 225 return nil, err 226 } 227 defer resp.Body.Close() 228 229 //Get the response from the create REST call 230 dbResponse := &DBOperationResponse{} 231 json.NewDecoder(resp.Body).Decode(&dbResponse) 232 233 if dbResponse.Ok == true { 234 logger.Debugf("Created database %s ", dbclient.dbName) 235 } 236 237 logger.Debugf("Exiting CreateDatabaseIfNotExist()") 238 239 return dbResponse, nil 240 241 } 242 243 logger.Debugf("Database %s already exists", dbclient.dbName) 244 245 logger.Debugf("Exiting CreateDatabaseIfNotExist()") 246 247 return nil, nil 248 249 } 250 251 //GetDatabaseInfo method provides function to retrieve database information 252 func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { 253 254 connectURL, err := url.Parse(dbclient.couchInstance.conf.URL) 255 if err != nil { 256 logger.Errorf("URL parse error: %s", err.Error()) 257 return nil, nil, err 258 } 259 connectURL.Path = dbclient.dbName 260 261 resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") 262 if err != nil { 263 return nil, couchDBReturn, err 264 } 265 defer resp.Body.Close() 266 267 dbResponse := &DBInfo{} 268 json.NewDecoder(resp.Body).Decode(&dbResponse) 269 270 // trace the database info response 271 if logger.IsEnabledFor(logging.DEBUG) { 272 dbResponseJSON, err := json.Marshal(dbResponse) 273 if err == nil { 274 logger.Debugf("GetDatabaseInfo() dbResponseJSON: %s", dbResponseJSON) 275 } 276 } 277 278 return dbResponse, couchDBReturn, nil 279 280 } 281 282 //VerifyConnection method provides function to verify the connection information 283 func (couchInstance *CouchInstance) VerifyConnection() (*ConnectionInfo, *DBReturn, error) { 284 285 connectURL, err := url.Parse(couchInstance.conf.URL) 286 if err != nil { 287 logger.Errorf("URL parse error: %s", err.Error()) 288 return nil, nil, err 289 } 290 connectURL.Path = "/" 291 292 resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") 293 if err != nil { 294 return nil, couchDBReturn, err 295 } 296 defer resp.Body.Close() 297 298 dbResponse := &ConnectionInfo{} 299 errJSON := json.NewDecoder(resp.Body).Decode(&dbResponse) 300 if errJSON != nil { 301 return nil, nil, errJSON 302 } 303 304 // trace the database info response 305 if logger.IsEnabledFor(logging.DEBUG) { 306 dbResponseJSON, err := json.Marshal(dbResponse) 307 if err == nil { 308 logger.Debugf("VerifyConnection() dbResponseJSON: %s", dbResponseJSON) 309 } 310 } 311 312 return dbResponse, couchDBReturn, nil 313 314 } 315 316 //DropDatabase provides method to drop an existing database 317 func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) { 318 319 logger.Debugf("Entering DropDatabase()") 320 321 connectURL, err := url.Parse(dbclient.couchInstance.conf.URL) 322 if err != nil { 323 logger.Errorf("URL parse error: %s", err.Error()) 324 return nil, err 325 } 326 connectURL.Path = dbclient.dbName 327 328 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "") 329 if err != nil { 330 return nil, err 331 } 332 defer resp.Body.Close() 333 334 dbResponse := &DBOperationResponse{} 335 json.NewDecoder(resp.Body).Decode(&dbResponse) 336 337 if dbResponse.Ok == true { 338 logger.Debugf("Dropped database %s ", dbclient.dbName) 339 } 340 341 logger.Debugf("Exiting DropDatabase()") 342 343 if dbResponse.Ok == true { 344 345 return dbResponse, nil 346 347 } 348 349 return dbResponse, fmt.Errorf("Error dropping database") 350 351 } 352 353 // EnsureFullCommit calls _ensure_full_commit for explicit fsync 354 func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) { 355 356 logger.Debugf("Entering EnsureFullCommit()") 357 358 connectURL, err := url.Parse(dbclient.couchInstance.conf.URL) 359 if err != nil { 360 logger.Errorf("URL parse error: %s", err.Error()) 361 return nil, err 362 } 363 connectURL.Path = dbclient.dbName + "/_ensure_full_commit" 364 365 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "") 366 if err != nil { 367 logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error()) 368 return nil, err 369 } 370 defer resp.Body.Close() 371 372 dbResponse := &DBOperationResponse{} 373 json.NewDecoder(resp.Body).Decode(&dbResponse) 374 375 if dbResponse.Ok == true { 376 logger.Debugf("_ensure_full_commit database %s ", dbclient.dbName) 377 } 378 379 logger.Debugf("Exiting EnsureFullCommit()") 380 381 if dbResponse.Ok == true { 382 383 return dbResponse, nil 384 385 } 386 387 return dbResponse, fmt.Errorf("Error syncing database") 388 } 389 390 //SaveDoc method provides a function to save a document, id and byte array 391 func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc) (string, error) { 392 393 logger.Debugf("Entering SaveDoc() id=[%s]", id) 394 if !utf8.ValidString(id) { 395 return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 396 } 397 398 saveURL, err := url.Parse(dbclient.couchInstance.conf.URL) 399 if err != nil { 400 logger.Errorf("URL parse error: %s", err.Error()) 401 return "", err 402 } 403 404 saveURL.Path = dbclient.dbName 405 // id can contain a '/', so encode separately 406 saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)} 407 408 if rev == "" { 409 410 //See if the document already exists, we need the rev for save 411 _, revdoc, err2 := dbclient.ReadDoc(id) 412 if err2 != nil { 413 //set the revision to indicate that the document was not found 414 rev = "" 415 } else { 416 //set the revision to the rev returned from the document read 417 rev = revdoc 418 } 419 } 420 421 logger.Debugf(" rev=%s", rev) 422 423 //Set up a buffer for the data to be pushed to couchdb 424 data := new(bytes.Buffer) 425 426 //Set up a default boundary for use by multipart if sending attachments 427 defaultBoundary := "" 428 429 //check to see if attachments is nil, if so, then this is a JSON only 430 if couchDoc.Attachments == nil { 431 432 //Test to see if this is a valid JSON 433 if IsJSON(string(couchDoc.JSONValue)) != true { 434 return "", fmt.Errorf("JSON format is not valid") 435 } 436 437 // if there are no attachments, then use the bytes passed in as the JSON 438 data.ReadFrom(bytes.NewReader(couchDoc.JSONValue)) 439 440 } else { // there are attachments 441 442 //attachments are included, create the multipart definition 443 multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary) 444 if err3 != nil { 445 return "", err3 446 } 447 448 //Set the data buffer to the data from the create multi-part data 449 data.ReadFrom(&multipartData) 450 451 //Set the default boundary to the value generated in the multipart creation 452 defaultBoundary = multipartBoundary 453 454 } 455 456 //handle the request for saving the JSON or attachments 457 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary) 458 if err != nil { 459 return "", err 460 } 461 defer resp.Body.Close() 462 463 //get the revision and return 464 revision, err := getRevisionHeader(resp) 465 if err != nil { 466 return "", err 467 } 468 469 logger.Debugf("Exiting SaveDoc()") 470 471 return revision, nil 472 473 } 474 475 func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) { 476 477 //Create a buffer for writing the result 478 writeBuffer := new(bytes.Buffer) 479 480 // read the attachment and save as an attachment 481 writer := multipart.NewWriter(writeBuffer) 482 483 //retrieve the boundary for the multipart 484 defaultBoundary = writer.Boundary() 485 486 fileAttachments := map[string]FileDetails{} 487 488 for _, attachment := range couchDoc.Attachments { 489 fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)} 490 } 491 492 attachmentJSONMap := map[string]interface{}{ 493 "_attachments": fileAttachments} 494 495 //Add any data uploaded with the files 496 if couchDoc.JSONValue != nil { 497 498 //create a generic map 499 genericMap := make(map[string]interface{}) 500 //unmarshal the data into the generic map 501 json.Unmarshal(couchDoc.JSONValue, &genericMap) 502 503 //add all key/values to the attachmentJSONMap 504 for jsonKey, jsonValue := range genericMap { 505 attachmentJSONMap[jsonKey] = jsonValue 506 } 507 508 } 509 510 filesForUpload, _ := json.Marshal(attachmentJSONMap) 511 logger.Debugf(string(filesForUpload)) 512 513 //create the header for the JSON 514 header := make(textproto.MIMEHeader) 515 header.Set("Content-Type", "application/json") 516 517 part, err := writer.CreatePart(header) 518 if err != nil { 519 return *writeBuffer, defaultBoundary, err 520 } 521 522 part.Write(filesForUpload) 523 524 for _, attachment := range couchDoc.Attachments { 525 526 header := make(textproto.MIMEHeader) 527 part, err2 := writer.CreatePart(header) 528 if err2 != nil { 529 return *writeBuffer, defaultBoundary, err2 530 } 531 part.Write(attachment.AttachmentBytes) 532 533 } 534 535 err = writer.Close() 536 if err != nil { 537 return *writeBuffer, defaultBoundary, err 538 } 539 540 return *writeBuffer, defaultBoundary, nil 541 542 } 543 544 func getRevisionHeader(resp *http.Response) (string, error) { 545 546 revision := resp.Header.Get("Etag") 547 548 if revision == "" { 549 return "", fmt.Errorf("No revision tag detected") 550 } 551 552 reg := regexp.MustCompile(`"([^"]*)"`) 553 revisionNoQuotes := reg.ReplaceAllString(revision, "${1}") 554 return revisionNoQuotes, nil 555 556 } 557 558 //ReadDoc method provides function to retrieve a document from the database by id 559 func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) { 560 var couchDoc CouchDoc 561 logger.Debugf("Entering ReadDoc() id=[%s]", id) 562 if !utf8.ValidString(id) { 563 return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 564 } 565 566 readURL, err := url.Parse(dbclient.couchInstance.conf.URL) 567 if err != nil { 568 logger.Errorf("URL parse error: %s", err.Error()) 569 return nil, "", err 570 } 571 readURL.Path = dbclient.dbName 572 // id can contain a '/', so encode separately 573 readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)} 574 575 query := readURL.Query() 576 query.Add("attachments", "true") 577 578 readURL.RawQuery = query.Encode() 579 580 resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "") 581 if err != nil { 582 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 583 logger.Debug("Document not found (404), returning nil value instead of 404 error") 584 // non-existent document should return nil value instead of a 404 error 585 // for details see https://github.com/hyperledger-archives/fabric/issues/936 586 return nil, "", nil 587 } 588 logger.Debugf("couchDBReturn=%v\n", couchDBReturn) 589 return nil, "", err 590 } 591 defer resp.Body.Close() 592 593 //Get the media type from the Content-Type header 594 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 595 if err != nil { 596 log.Fatal(err) 597 } 598 599 //Get the revision from header 600 revision, err := getRevisionHeader(resp) 601 if err != nil { 602 return nil, "", err 603 } 604 605 //check to see if the is multipart, handle as attachment if multipart is detected 606 if strings.HasPrefix(mediaType, "multipart/") { 607 //Set up the multipart reader based on the boundary 608 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 609 for { 610 p, err := multipartReader.NextPart() 611 if err == io.EOF { 612 break // processed all parts 613 } 614 if err != nil { 615 return nil, "", err 616 } 617 618 defer p.Close() 619 620 logger.Debugf("part header=%s", p.Header) 621 switch p.Header.Get("Content-Type") { 622 case "application/json": 623 partdata, err := ioutil.ReadAll(p) 624 if err != nil { 625 return nil, "", err 626 } 627 couchDoc.JSONValue = partdata 628 default: 629 630 //Create an attachment structure and load it 631 attachment := Attachment{} 632 attachment.ContentType = p.Header.Get("Content-Type") 633 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 634 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 635 switch p.Header.Get("Content-Encoding") { 636 case "gzip": //See if the part is gzip encoded 637 638 var respBody []byte 639 640 gr, err := gzip.NewReader(p) 641 if err != nil { 642 return nil, "", err 643 } 644 respBody, err = ioutil.ReadAll(gr) 645 if err != nil { 646 return nil, "", err 647 } 648 649 logger.Debugf("Retrieved attachment data") 650 attachment.AttachmentBytes = respBody 651 attachment.Name = p.FileName() 652 couchDoc.Attachments = append(couchDoc.Attachments, attachment) 653 654 default: 655 656 //retrieve the data, this is not gzip 657 partdata, err := ioutil.ReadAll(p) 658 if err != nil { 659 return nil, "", err 660 } 661 logger.Debugf("Retrieved attachment data") 662 attachment.AttachmentBytes = partdata 663 attachment.Name = p.FileName() 664 couchDoc.Attachments = append(couchDoc.Attachments, attachment) 665 666 } // end content-encoding switch 667 } // end if attachment 668 } // end content-type switch 669 } // for all multiparts 670 671 return &couchDoc, revision, nil 672 } 673 674 //handle as JSON document 675 couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body) 676 if err != nil { 677 return nil, "", err 678 } 679 680 logger.Debugf("Exiting ReadDoc()") 681 return &couchDoc, revision, nil 682 } 683 684 //ReadDocRange method provides function to a range of documents based on the start and end keys 685 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 686 //TODO This function provides a limit option to specify the max number of entries. This will 687 //need to be added to configuration options. Skip will not be used by Fabric since a consistent 688 //result set is required 689 func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) { 690 691 logger.Debugf("Entering ReadDocRange() startKey=%s, endKey=%s", startKey, endKey) 692 693 var results []QueryResult 694 695 rangeURL, err := url.Parse(dbclient.couchInstance.conf.URL) 696 if err != nil { 697 logger.Errorf("URL parse error: %s", err.Error()) 698 return nil, err 699 } 700 rangeURL.Path = dbclient.dbName + "/_all_docs" 701 702 queryParms := rangeURL.Query() 703 queryParms.Set("limit", strconv.Itoa(limit)) 704 queryParms.Add("skip", strconv.Itoa(skip)) 705 queryParms.Add("include_docs", "true") 706 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 707 708 //Append the startKey if provided 709 710 if startKey != "" { 711 var err error 712 if startKey, err = encodeForJSON(startKey); err != nil { 713 return nil, err 714 } 715 queryParms.Add("startkey", "\""+startKey+"\"") 716 } 717 718 //Append the endKey if provided 719 if endKey != "" { 720 var err error 721 if endKey, err = encodeForJSON(endKey); err != nil { 722 return nil, err 723 } 724 queryParms.Add("endkey", "\""+endKey+"\"") 725 } 726 727 rangeURL.RawQuery = queryParms.Encode() 728 729 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "") 730 if err != nil { 731 return nil, err 732 } 733 defer resp.Body.Close() 734 735 if logger.IsEnabledFor(logging.DEBUG) { 736 dump, err2 := httputil.DumpResponse(resp, true) 737 if err2 != nil { 738 log.Fatal(err2) 739 } 740 logger.Debugf("%s", dump) 741 } 742 743 //handle as JSON document 744 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 745 if err != nil { 746 return nil, err 747 } 748 749 var jsonResponse = &RangeQueryResponse{} 750 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 751 if err2 != nil { 752 return nil, err2 753 } 754 755 logger.Debugf("Total Rows: %d", jsonResponse.TotalRows) 756 757 for _, row := range jsonResponse.Rows { 758 759 var jsonDoc = &Doc{} 760 err3 := json.Unmarshal(row.Doc, &jsonDoc) 761 if err3 != nil { 762 return nil, err3 763 } 764 765 if jsonDoc.Attachments != nil { 766 767 logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID) 768 769 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 770 if err != nil { 771 return nil, err 772 } 773 774 var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments} 775 results = append(results, *addDocument) 776 777 } else { 778 779 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 780 781 var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil} 782 results = append(results, *addDocument) 783 784 } 785 786 } 787 788 logger.Debugf("Exiting ReadDocRange()") 789 790 return &results, nil 791 792 } 793 794 //DeleteDoc method provides function to delete a document from the database by id 795 func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { 796 797 logger.Debugf("Entering DeleteDoc() id=%s", id) 798 799 deleteURL, err := url.Parse(dbclient.couchInstance.conf.URL) 800 if err != nil { 801 logger.Errorf("URL parse error: %s", err.Error()) 802 return err 803 } 804 805 deleteURL.Path = dbclient.dbName 806 // id can contain a '/', so encode separately 807 deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)} 808 809 if rev == "" { 810 811 //See if the document already exists, we need the rev for delete 812 _, revdoc, err2 := dbclient.ReadDoc(id) 813 if err2 != nil { 814 //set the revision to indicate that the document was not found 815 rev = "" 816 } else { 817 //set the revision to the rev returned from the document read 818 rev = revdoc 819 } 820 } 821 822 logger.Debugf(" rev=%s", rev) 823 824 resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "") 825 if err != nil { 826 fmt.Printf("couchDBReturn=%v", couchDBReturn) 827 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 828 logger.Debug("Document not found (404), returning nil value instead of 404 error") 829 // non-existent document should return nil value instead of a 404 error 830 // for details see https://github.com/hyperledger-archives/fabric/issues/936 831 return nil 832 } 833 return err 834 } 835 defer resp.Body.Close() 836 837 logger.Debugf("Exiting DeleteDoc()") 838 839 return nil 840 841 } 842 843 //QueryDocuments method provides function for processing a query 844 func (dbclient *CouchDatabase) QueryDocuments(query string, limit, skip int) (*[]QueryResult, error) { 845 846 logger.Debugf("Entering QueryDocuments() query=%s", query) 847 848 var results []QueryResult 849 850 queryURL, err := url.Parse(dbclient.couchInstance.conf.URL) 851 if err != nil { 852 logger.Errorf("URL parse error: %s", err.Error()) 853 return nil, err 854 } 855 856 queryURL.Path = dbclient.dbName + "/_find" 857 858 queryParms := queryURL.Query() 859 queryParms.Set("limit", strconv.Itoa(limit)) 860 queryParms.Add("skip", strconv.Itoa(skip)) 861 862 queryURL.RawQuery = queryParms.Encode() 863 864 //Set up a buffer for the data to be pushed to couchdb 865 data := new(bytes.Buffer) 866 867 data.ReadFrom(bytes.NewReader([]byte(query))) 868 869 resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, queryURL.String(), data, "", "") 870 if err != nil { 871 return nil, err 872 } 873 defer resp.Body.Close() 874 875 if logger.IsEnabledFor(logging.DEBUG) { 876 dump, err2 := httputil.DumpResponse(resp, true) 877 if err2 != nil { 878 log.Fatal(err2) 879 } 880 logger.Debugf("%s", dump) 881 } 882 883 //handle as JSON document 884 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 885 if err != nil { 886 return nil, err 887 } 888 889 var jsonResponse = &QueryResponse{} 890 891 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 892 if err2 != nil { 893 return nil, err2 894 } 895 896 for _, row := range jsonResponse.Docs { 897 898 var jsonDoc = &Doc{} 899 err3 := json.Unmarshal(row, &jsonDoc) 900 if err3 != nil { 901 return nil, err3 902 } 903 904 if jsonDoc.Attachments != nil { 905 906 logger.Debugf("Adding JSON docment and attachments for id: %s", jsonDoc.ID) 907 908 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 909 if err != nil { 910 return nil, err 911 } 912 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments} 913 results = append(results, *addDocument) 914 915 } else { 916 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 917 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: row, Attachments: nil} 918 919 results = append(results, *addDocument) 920 921 } 922 } 923 logger.Debugf("Exiting QueryDocuments()") 924 925 return &results, nil 926 927 } 928 929 //handleRequest method is a generic http request handler 930 func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) { 931 932 logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL) 933 934 //Create request based on URL for couchdb operation 935 req, err := http.NewRequest(method, connectURL, data) 936 if err != nil { 937 return nil, nil, err 938 } 939 940 //add content header for PUT 941 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 942 943 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 944 //to application/json. Else, this is contains an attachment and needs to be multipart 945 if multipartBoundary == "" { 946 req.Header.Set("Content-Type", "application/json") 947 } else { 948 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 949 } 950 951 //check to see if the revision is set, if so, pass as a header 952 if rev != "" { 953 req.Header.Set("If-Match", rev) 954 } 955 } 956 957 //add content header for PUT 958 if method == http.MethodPut || method == http.MethodPost { 959 req.Header.Set("Accept", "application/json") 960 } 961 962 //add content header for GET 963 if method == http.MethodGet { 964 req.Header.Set("Accept", "multipart/related") 965 } 966 967 //If username and password are set the use basic auth 968 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 969 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 970 } 971 972 if logger.IsEnabledFor(logging.DEBUG) { 973 dump, _ := httputil.DumpRequestOut(req, false) 974 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 975 logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 976 } 977 978 //Create the http client 979 client := &http.Client{} 980 981 transport := &http.Transport{Proxy: http.ProxyFromEnvironment} 982 transport.DisableCompression = false 983 client.Transport = transport 984 985 //Execute http request 986 resp, err := client.Do(req) 987 if err != nil { 988 return nil, nil, err 989 } 990 991 //create the return object for couchDB 992 couchDBReturn := &DBReturn{} 993 994 //set the return code for the couchDB request 995 couchDBReturn.StatusCode = resp.StatusCode 996 997 //check to see if the status code is 400 or higher 998 //in this case, the http request succeeded but CouchDB is reporing an error 999 if resp.StatusCode >= 400 { 1000 1001 jsonError, err := ioutil.ReadAll(resp.Body) 1002 if err != nil { 1003 return nil, nil, err 1004 } 1005 1006 logger.Debugf("Couch DB error status code=%v error=%s", resp.StatusCode, jsonError) 1007 1008 errorBytes := []byte(jsonError) 1009 1010 json.Unmarshal(errorBytes, &couchDBReturn) 1011 1012 return nil, couchDBReturn, fmt.Errorf("Couch DB Error: %s", couchDBReturn.Reason) 1013 1014 } 1015 1016 logger.Debugf("Exiting handleRequest()") 1017 1018 //If no errors, then return the results 1019 return resp, couchDBReturn, nil 1020 } 1021 1022 //IsJSON tests a string to determine if a valid JSON 1023 func IsJSON(s string) bool { 1024 var js map[string]interface{} 1025 return json.Unmarshal([]byte(s), &js) == nil 1026 } 1027 1028 // encodePathElement uses Golang for encoding and in addition, replaces a '/' by %2F. 1029 // Otherwise, in the regular encoding, a '/' is treated as a path separator in the url 1030 func encodePathElement(str string) string { 1031 u := &url.URL{} 1032 u.Path = str 1033 encodedStr := u.String() 1034 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1035 return encodedStr 1036 } 1037 1038 func encodeForJSON(str string) (string, error) { 1039 buf := &bytes.Buffer{} 1040 encoder := json.NewEncoder(buf) 1041 if err := encoder.Encode(str); err != nil { 1042 return "", err 1043 } 1044 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1045 buffer := buf.Bytes() 1046 return string(buffer[1 : len(buffer)-2]), nil 1047 }