github.com/leonlxy/hyperledger@v1.0.0-alpha.0.20170427033203-34922035d248/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/base64" 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "log" 28 "mime" 29 "mime/multipart" 30 "net/http" 31 "net/http/httputil" 32 "net/textproto" 33 "net/url" 34 "regexp" 35 "strconv" 36 "strings" 37 "time" 38 "unicode/utf8" 39 40 "github.com/hyperledger/fabric/common/flogging" 41 logging "github.com/op/go-logging" 42 ) 43 44 var logger = flogging.MustGetLogger("couchdb") 45 46 //time between retry attempts in milliseconds 47 const retryWaitTime = 125 48 49 // DBOperationResponse is body for successful database calls. 50 type DBOperationResponse struct { 51 Ok bool 52 id string 53 rev string 54 } 55 56 // DBInfo is body for database information. 57 type DBInfo struct { 58 DbName string `json:"db_name"` 59 UpdateSeq string `json:"update_seq"` 60 Sizes struct { 61 File int `json:"file"` 62 External int `json:"external"` 63 Active int `json:"active"` 64 } `json:"sizes"` 65 PurgeSeq int `json:"purge_seq"` 66 Other struct { 67 DataSize int `json:"data_size"` 68 } `json:"other"` 69 DocDelCount int `json:"doc_del_count"` 70 DocCount int `json:"doc_count"` 71 DiskSize int `json:"disk_size"` 72 DiskFormatVersion int `json:"disk_format_version"` 73 DataSize int `json:"data_size"` 74 CompactRunning bool `json:"compact_running"` 75 InstanceStartTime string `json:"instance_start_time"` 76 } 77 78 //ConnectionInfo is a structure for capturing the database info and version 79 type ConnectionInfo struct { 80 Couchdb string `json:"couchdb"` 81 Version string `json:"version"` 82 Vendor struct { 83 Name string `json:"name"` 84 } `json:"vendor"` 85 } 86 87 //RangeQueryResponse is used for processing REST range query responses from CouchDB 88 type RangeQueryResponse struct { 89 TotalRows int `json:"total_rows"` 90 Offset int `json:"offset"` 91 Rows []struct { 92 ID string `json:"id"` 93 Key string `json:"key"` 94 Value struct { 95 Rev string `json:"rev"` 96 } `json:"value"` 97 Doc json.RawMessage `json:"doc"` 98 } `json:"rows"` 99 } 100 101 //QueryResponse is used for processing REST query responses from CouchDB 102 type QueryResponse struct { 103 Warning string `json:"warning"` 104 Docs []json.RawMessage `json:"docs"` 105 } 106 107 //Doc is used for capturing if attachments are return in the query from CouchDB 108 type Doc struct { 109 ID string `json:"_id"` 110 Rev string `json:"_rev"` 111 Attachments json.RawMessage `json:"_attachments"` 112 } 113 114 //DocID is a minimal structure for capturing the ID from a query result 115 type DocID struct { 116 ID string `json:"_id"` 117 } 118 119 //QueryResult is used for returning query results from CouchDB 120 type QueryResult struct { 121 ID string 122 Value []byte 123 Attachments []*Attachment 124 } 125 126 //CouchConnectionDef contains parameters 127 type CouchConnectionDef struct { 128 URL string 129 Username string 130 Password string 131 MaxRetries int 132 MaxRetriesOnStartup int 133 RequestTimeout time.Duration 134 } 135 136 //CouchInstance represents a CouchDB instance 137 type CouchInstance struct { 138 conf CouchConnectionDef //connection configuration 139 client *http.Client // a client to connect to this instance 140 } 141 142 //CouchDatabase represents a database within a CouchDB instance 143 type CouchDatabase struct { 144 CouchInstance CouchInstance //connection configuration 145 DBName string 146 } 147 148 //DBReturn contains an error reported by CouchDB 149 type DBReturn struct { 150 StatusCode int `json:"status_code"` 151 Error string `json:"error"` 152 Reason string `json:"reason"` 153 } 154 155 //Attachment contains the definition for an attached file for couchdb 156 type Attachment struct { 157 Name string 158 ContentType string 159 Length uint64 160 AttachmentBytes []byte 161 } 162 163 //DocMetadata returns the ID, version and revision for a couchdb document 164 type DocMetadata struct { 165 ID string 166 Rev string 167 Version string 168 } 169 170 //FileDetails defines the structure needed to send an attachment to couchdb 171 type FileDetails struct { 172 Follows bool `json:"follows"` 173 ContentType string `json:"content_type"` 174 Length int `json:"length"` 175 } 176 177 //CouchDoc defines the structure for a JSON document value 178 type CouchDoc struct { 179 JSONValue []byte 180 Attachments []*Attachment 181 } 182 183 //BatchRetrieveDocMedatadataResponse is used for processing REST batch responses from CouchDB 184 type BatchRetrieveDocMedatadataResponse struct { 185 Rows []struct { 186 ID string `json:"id"` 187 Doc struct { 188 ID string `json:"_id"` 189 Rev string `json:"_rev"` 190 Version string `json:"version"` 191 } `json:"doc"` 192 } `json:"rows"` 193 } 194 195 //BatchUpdateResponse defines a structure for batch update response 196 type BatchUpdateResponse struct { 197 ID string `json:"id"` 198 Error string `json:"error"` 199 Reason string `json:"reason"` 200 Ok bool `json:"ok"` 201 Rev string `json:"rev"` 202 } 203 204 //Base64Attachment contains the definition for an attached file for couchdb 205 type Base64Attachment struct { 206 ContentType string `json:"content_type"` 207 AttachmentData string `json:"data"` 208 } 209 210 // closeResponseBody discards the body and then closes it to enable returning it to 211 // connection pool 212 func closeResponseBody(resp *http.Response) { 213 io.Copy(ioutil.Discard, resp.Body) // discard whatever is remaining of body 214 resp.Body.Close() 215 } 216 217 //CreateConnectionDefinition for a new client connection 218 func CreateConnectionDefinition(couchDBAddress, username, password string, maxRetries, 219 maxRetriesOnStartup int, requestTimeout time.Duration) (*CouchConnectionDef, error) { 220 221 logger.Debugf("Entering CreateConnectionDefinition()") 222 223 connectURL := &url.URL{ 224 Host: couchDBAddress, 225 Scheme: "http", 226 } 227 228 //parse the constructed URL to verify no errors 229 finalURL, err := url.Parse(connectURL.String()) 230 if err != nil { 231 logger.Errorf("URL parse error: %s", err.Error()) 232 return nil, err 233 } 234 235 logger.Debugf("Created database configuration URL=[%s]", finalURL.String()) 236 logger.Debugf("Exiting CreateConnectionDefinition()") 237 238 //return an object containing the connection information 239 return &CouchConnectionDef{finalURL.String(), username, password, maxRetries, 240 maxRetriesOnStartup, requestTimeout}, nil 241 242 } 243 244 //CreateDatabaseIfNotExist method provides function to create database 245 func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse, error) { 246 247 logger.Debugf("Entering CreateDatabaseIfNotExist()") 248 249 dbInfo, couchDBReturn, err := dbclient.GetDatabaseInfo() 250 if err != nil { 251 if couchDBReturn == nil || couchDBReturn.StatusCode != 404 { 252 return nil, err 253 } 254 } 255 256 if dbInfo == nil && couchDBReturn.StatusCode == 404 { 257 258 logger.Debugf("Database %s does not exist.", dbclient.DBName) 259 260 connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 261 if err != nil { 262 logger.Errorf("URL parse error: %s", err.Error()) 263 return nil, err 264 } 265 connectURL.Path = dbclient.DBName 266 267 //get the number of retries 268 maxRetries := dbclient.CouchInstance.conf.MaxRetries 269 270 //process the URL with a PUT, creates the database 271 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "", maxRetries) 272 if err != nil { 273 return nil, err 274 } 275 defer closeResponseBody(resp) 276 277 //Get the response from the create REST call 278 dbResponse := &DBOperationResponse{} 279 json.NewDecoder(resp.Body).Decode(&dbResponse) 280 281 if dbResponse.Ok == true { 282 logger.Debugf("Created database %s ", dbclient.DBName) 283 } 284 285 logger.Debugf("Exiting CreateDatabaseIfNotExist()") 286 287 return dbResponse, nil 288 289 } 290 291 logger.Debugf("Database %s already exists", dbclient.DBName) 292 293 logger.Debugf("Exiting CreateDatabaseIfNotExist()") 294 295 return nil, nil 296 297 } 298 299 //GetDatabaseInfo method provides function to retrieve database information 300 func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { 301 302 connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 303 if err != nil { 304 logger.Errorf("URL parse error: %s", err.Error()) 305 return nil, nil, err 306 } 307 connectURL.Path = dbclient.DBName 308 309 //get the number of retries 310 maxRetries := dbclient.CouchInstance.conf.MaxRetries 311 312 resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "", maxRetries) 313 if err != nil { 314 return nil, couchDBReturn, err 315 } 316 defer closeResponseBody(resp) 317 318 dbResponse := &DBInfo{} 319 json.NewDecoder(resp.Body).Decode(&dbResponse) 320 321 // trace the database info response 322 if logger.IsEnabledFor(logging.DEBUG) { 323 dbResponseJSON, err := json.Marshal(dbResponse) 324 if err == nil { 325 logger.Debugf("GetDatabaseInfo() dbResponseJSON: %s", dbResponseJSON) 326 } 327 } 328 329 return dbResponse, couchDBReturn, nil 330 331 } 332 333 //VerifyCouchConfig method provides function to verify the connection information 334 func (couchInstance *CouchInstance) VerifyCouchConfig() (*ConnectionInfo, *DBReturn, error) { 335 336 logger.Debugf("Entering VerifyCouchConfig()") 337 defer logger.Debugf("Exiting VerifyCouchConfig()") 338 339 connectURL, err := url.Parse(couchInstance.conf.URL) 340 if err != nil { 341 logger.Errorf("URL parse error: %s", err.Error()) 342 return nil, nil, err 343 } 344 connectURL.Path = "/" 345 346 //get the number of retries for startup 347 maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup 348 349 resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, 350 couchInstance.conf.Username, couchInstance.conf.Password, maxRetriesOnStartup) 351 352 if err != nil { 353 return nil, couchDBReturn, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", err.Error()) 354 } 355 defer closeResponseBody(resp) 356 357 dbResponse := &ConnectionInfo{} 358 errJSON := json.NewDecoder(resp.Body).Decode(&dbResponse) 359 if errJSON != nil { 360 return nil, nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", errJSON.Error()) 361 } 362 363 // trace the database info response 364 if logger.IsEnabledFor(logging.DEBUG) { 365 dbResponseJSON, err := json.Marshal(dbResponse) 366 if err == nil { 367 logger.Debugf("VerifyConnection() dbResponseJSON: %s", dbResponseJSON) 368 } 369 } 370 371 //check to see if the system databases exist 372 //Verifying the existence of the system database accomplishes two steps 373 //1. Ensures the system databases are created 374 //2. Verifies the username password provided in the CouchDB config are valid for system admin 375 err = CreateSystemDatabasesIfNotExist(*couchInstance) 376 if err != nil { 377 logger.Errorf("Unable to connect to CouchDB, error: %s Check the admin username and password.\n", err.Error()) 378 return nil, nil, fmt.Errorf("Unable to connect to CouchDB, error: %s Check the admin username and password.\n", err.Error()) 379 } 380 381 return dbResponse, couchDBReturn, nil 382 } 383 384 //DropDatabase provides method to drop an existing database 385 func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) { 386 387 logger.Debugf("Entering DropDatabase()") 388 389 connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 390 if err != nil { 391 logger.Errorf("URL parse error: %s", err.Error()) 392 return nil, err 393 } 394 connectURL.Path = dbclient.DBName 395 396 //get the number of retries 397 maxRetries := dbclient.CouchInstance.conf.MaxRetries 398 399 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "", maxRetries) 400 if err != nil { 401 return nil, err 402 } 403 defer closeResponseBody(resp) 404 405 dbResponse := &DBOperationResponse{} 406 json.NewDecoder(resp.Body).Decode(&dbResponse) 407 408 if dbResponse.Ok == true { 409 logger.Debugf("Dropped database %s ", dbclient.DBName) 410 } 411 412 logger.Debugf("Exiting DropDatabase()") 413 414 if dbResponse.Ok == true { 415 416 return dbResponse, nil 417 418 } 419 420 return dbResponse, fmt.Errorf("Error dropping database") 421 422 } 423 424 // EnsureFullCommit calls _ensure_full_commit for explicit fsync 425 func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) { 426 427 logger.Debugf("Entering EnsureFullCommit()") 428 429 connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 430 if err != nil { 431 logger.Errorf("URL parse error: %s", err.Error()) 432 return nil, err 433 } 434 connectURL.Path = dbclient.DBName + "/_ensure_full_commit" 435 436 //get the number of retries 437 maxRetries := dbclient.CouchInstance.conf.MaxRetries 438 439 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "", maxRetries) 440 if err != nil { 441 logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error()) 442 return nil, err 443 } 444 defer closeResponseBody(resp) 445 446 dbResponse := &DBOperationResponse{} 447 json.NewDecoder(resp.Body).Decode(&dbResponse) 448 449 if dbResponse.Ok == true { 450 logger.Debugf("_ensure_full_commit database %s ", dbclient.DBName) 451 } 452 453 logger.Debugf("Exiting EnsureFullCommit()") 454 455 if dbResponse.Ok == true { 456 457 return dbResponse, nil 458 459 } 460 461 return dbResponse, fmt.Errorf("Error syncing database") 462 } 463 464 //SaveDoc method provides a function to save a document, id and byte array 465 func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc) (string, error) { 466 467 logger.Debugf("Entering SaveDoc() id=[%s]", id) 468 if !utf8.ValidString(id) { 469 return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 470 } 471 472 saveURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 473 if err != nil { 474 logger.Errorf("URL parse error: %s", err.Error()) 475 return "", err 476 } 477 478 saveURL.Path = dbclient.DBName 479 // id can contain a '/', so encode separately 480 saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)} 481 482 if rev == "" { 483 484 //See if the document already exists, we need the rev for save 485 _, revdoc, err2 := dbclient.ReadDoc(id) 486 if err2 != nil { 487 //set the revision to indicate that the document was not found 488 rev = "" 489 } else { 490 //set the revision to the rev returned from the document read 491 rev = revdoc 492 } 493 } 494 495 logger.Debugf(" rev=%s", rev) 496 497 //Set up a buffer for the data to be pushed to couchdb 498 data := []byte{} 499 500 //Set up a default boundary for use by multipart if sending attachments 501 defaultBoundary := "" 502 503 //check to see if attachments is nil, if so, then this is a JSON only 504 if couchDoc.Attachments == nil { 505 506 //Test to see if this is a valid JSON 507 if IsJSON(string(couchDoc.JSONValue)) != true { 508 return "", fmt.Errorf("JSON format is not valid") 509 } 510 511 // if there are no attachments, then use the bytes passed in as the JSON 512 data = couchDoc.JSONValue 513 514 } else { // there are attachments 515 516 //attachments are included, create the multipart definition 517 multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary) 518 if err3 != nil { 519 return "", err3 520 } 521 522 //Set the data buffer to the data from the create multi-part data 523 data = multipartData.Bytes() 524 525 //Set the default boundary to the value generated in the multipart creation 526 defaultBoundary = multipartBoundary 527 528 } 529 530 //get the number of retries 531 maxRetries := dbclient.CouchInstance.conf.MaxRetries 532 533 //handle the request for saving the JSON or attachments 534 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary, maxRetries) 535 if err != nil { 536 return "", err 537 } 538 defer closeResponseBody(resp) 539 540 //get the revision and return 541 revision, err := getRevisionHeader(resp) 542 if err != nil { 543 return "", err 544 } 545 546 logger.Debugf("Exiting SaveDoc()") 547 548 return revision, nil 549 550 } 551 552 func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) { 553 554 //Create a buffer for writing the result 555 writeBuffer := new(bytes.Buffer) 556 557 // read the attachment and save as an attachment 558 writer := multipart.NewWriter(writeBuffer) 559 560 //retrieve the boundary for the multipart 561 defaultBoundary = writer.Boundary() 562 563 fileAttachments := map[string]FileDetails{} 564 565 for _, attachment := range couchDoc.Attachments { 566 fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)} 567 } 568 569 attachmentJSONMap := map[string]interface{}{ 570 "_attachments": fileAttachments} 571 572 //Add any data uploaded with the files 573 if couchDoc.JSONValue != nil { 574 575 //create a generic map 576 genericMap := make(map[string]interface{}) 577 578 //unmarshal the data into the generic map 579 decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.JSONValue)) 580 decoder.UseNumber() 581 decoder.Decode(&genericMap) 582 583 //add all key/values to the attachmentJSONMap 584 for jsonKey, jsonValue := range genericMap { 585 attachmentJSONMap[jsonKey] = jsonValue 586 } 587 588 } 589 590 filesForUpload, _ := json.Marshal(attachmentJSONMap) 591 logger.Debugf(string(filesForUpload)) 592 593 //create the header for the JSON 594 header := make(textproto.MIMEHeader) 595 header.Set("Content-Type", "application/json") 596 597 part, err := writer.CreatePart(header) 598 if err != nil { 599 return *writeBuffer, defaultBoundary, err 600 } 601 602 part.Write(filesForUpload) 603 604 for _, attachment := range couchDoc.Attachments { 605 606 header := make(textproto.MIMEHeader) 607 part, err2 := writer.CreatePart(header) 608 if err2 != nil { 609 return *writeBuffer, defaultBoundary, err2 610 } 611 part.Write(attachment.AttachmentBytes) 612 613 } 614 615 err = writer.Close() 616 if err != nil { 617 return *writeBuffer, defaultBoundary, err 618 } 619 620 return *writeBuffer, defaultBoundary, nil 621 622 } 623 624 func getRevisionHeader(resp *http.Response) (string, error) { 625 626 revision := resp.Header.Get("Etag") 627 628 if revision == "" { 629 return "", fmt.Errorf("No revision tag detected") 630 } 631 632 reg := regexp.MustCompile(`"([^"]*)"`) 633 revisionNoQuotes := reg.ReplaceAllString(revision, "${1}") 634 return revisionNoQuotes, nil 635 636 } 637 638 //ReadDoc method provides function to retrieve a document from the database by id 639 func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) { 640 var couchDoc CouchDoc 641 attachments := []*Attachment{} 642 643 logger.Debugf("Entering ReadDoc() id=[%s]", id) 644 if !utf8.ValidString(id) { 645 return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 646 } 647 648 readURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 649 if err != nil { 650 logger.Errorf("URL parse error: %s", err.Error()) 651 return nil, "", err 652 } 653 readURL.Path = dbclient.DBName 654 // id can contain a '/', so encode separately 655 readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)} 656 657 query := readURL.Query() 658 query.Add("attachments", "true") 659 660 readURL.RawQuery = query.Encode() 661 662 //get the number of retries 663 maxRetries := dbclient.CouchInstance.conf.MaxRetries 664 665 resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "", maxRetries) 666 if err != nil { 667 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 668 logger.Debug("Document not found (404), returning nil value instead of 404 error") 669 // non-existent document should return nil value instead of a 404 error 670 // for details see https://github.com/hyperledger-archives/fabric/issues/936 671 return nil, "", nil 672 } 673 logger.Debugf("couchDBReturn=%v\n", couchDBReturn) 674 return nil, "", err 675 } 676 defer closeResponseBody(resp) 677 678 //Get the media type from the Content-Type header 679 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 680 if err != nil { 681 log.Fatal(err) 682 } 683 684 //Get the revision from header 685 revision, err := getRevisionHeader(resp) 686 if err != nil { 687 return nil, "", err 688 } 689 690 //check to see if the is multipart, handle as attachment if multipart is detected 691 if strings.HasPrefix(mediaType, "multipart/") { 692 //Set up the multipart reader based on the boundary 693 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 694 for { 695 p, err := multipartReader.NextPart() 696 if err == io.EOF { 697 break // processed all parts 698 } 699 if err != nil { 700 return nil, "", err 701 } 702 703 defer p.Close() 704 705 logger.Debugf("part header=%s", p.Header) 706 switch p.Header.Get("Content-Type") { 707 case "application/json": 708 partdata, err := ioutil.ReadAll(p) 709 if err != nil { 710 return nil, "", err 711 } 712 couchDoc.JSONValue = partdata 713 default: 714 715 //Create an attachment structure and load it 716 attachment := &Attachment{} 717 attachment.ContentType = p.Header.Get("Content-Type") 718 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 719 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 720 switch p.Header.Get("Content-Encoding") { 721 case "gzip": //See if the part is gzip encoded 722 723 var respBody []byte 724 725 gr, err := gzip.NewReader(p) 726 if err != nil { 727 return nil, "", err 728 } 729 respBody, err = ioutil.ReadAll(gr) 730 if err != nil { 731 return nil, "", err 732 } 733 734 logger.Debugf("Retrieved attachment data") 735 attachment.AttachmentBytes = respBody 736 attachment.Name = p.FileName() 737 attachments = append(attachments, attachment) 738 739 default: 740 741 //retrieve the data, this is not gzip 742 partdata, err := ioutil.ReadAll(p) 743 if err != nil { 744 return nil, "", err 745 } 746 logger.Debugf("Retrieved attachment data") 747 attachment.AttachmentBytes = partdata 748 attachment.Name = p.FileName() 749 attachments = append(attachments, attachment) 750 751 } // end content-encoding switch 752 } // end if attachment 753 } // end content-type switch 754 } // for all multiparts 755 756 couchDoc.Attachments = attachments 757 758 return &couchDoc, revision, nil 759 } 760 761 //handle as JSON document 762 couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body) 763 if err != nil { 764 return nil, "", err 765 } 766 767 logger.Debugf("Exiting ReadDoc()") 768 return &couchDoc, revision, nil 769 } 770 771 //ReadDocRange method provides function to a range of documents based on the start and end keys 772 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 773 //This function provides a limit option to specify the max number of entries and is supplied by config. 774 //Skip is reserved for possible future future use. 775 func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) { 776 777 logger.Debugf("Entering ReadDocRange() startKey=%s, endKey=%s", startKey, endKey) 778 779 var results []QueryResult 780 781 rangeURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 782 if err != nil { 783 logger.Errorf("URL parse error: %s", err.Error()) 784 return nil, err 785 } 786 rangeURL.Path = dbclient.DBName + "/_all_docs" 787 788 queryParms := rangeURL.Query() 789 queryParms.Set("limit", strconv.Itoa(limit)) 790 queryParms.Add("skip", strconv.Itoa(skip)) 791 queryParms.Add("include_docs", "true") 792 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 793 794 //Append the startKey if provided 795 796 if startKey != "" { 797 var err error 798 if startKey, err = encodeForJSON(startKey); err != nil { 799 return nil, err 800 } 801 queryParms.Add("startkey", "\""+startKey+"\"") 802 } 803 804 //Append the endKey if provided 805 if endKey != "" { 806 var err error 807 if endKey, err = encodeForJSON(endKey); err != nil { 808 return nil, err 809 } 810 queryParms.Add("endkey", "\""+endKey+"\"") 811 } 812 813 rangeURL.RawQuery = queryParms.Encode() 814 815 //get the number of retries 816 maxRetries := dbclient.CouchInstance.conf.MaxRetries 817 818 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries) 819 if err != nil { 820 return nil, err 821 } 822 defer closeResponseBody(resp) 823 824 if logger.IsEnabledFor(logging.DEBUG) { 825 dump, err2 := httputil.DumpResponse(resp, true) 826 if err2 != nil { 827 log.Fatal(err2) 828 } 829 logger.Debugf("%s", dump) 830 } 831 832 //handle as JSON document 833 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 834 if err != nil { 835 return nil, err 836 } 837 838 var jsonResponse = &RangeQueryResponse{} 839 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 840 if err2 != nil { 841 return nil, err2 842 } 843 844 logger.Debugf("Total Rows: %d", jsonResponse.TotalRows) 845 846 for _, row := range jsonResponse.Rows { 847 848 var jsonDoc = &Doc{} 849 err3 := json.Unmarshal(row.Doc, &jsonDoc) 850 if err3 != nil { 851 return nil, err3 852 } 853 854 if jsonDoc.Attachments != nil { 855 856 logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID) 857 858 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 859 if err != nil { 860 return nil, err 861 } 862 863 var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments} 864 results = append(results, *addDocument) 865 866 } else { 867 868 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 869 870 var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil} 871 results = append(results, *addDocument) 872 873 } 874 875 } 876 877 logger.Debugf("Exiting ReadDocRange()") 878 879 return &results, nil 880 881 } 882 883 //DeleteDoc method provides function to delete a document from the database by id 884 func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { 885 886 logger.Debugf("Entering DeleteDoc() id=%s", id) 887 888 deleteURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 889 if err != nil { 890 logger.Errorf("URL parse error: %s", err.Error()) 891 return err 892 } 893 894 deleteURL.Path = dbclient.DBName 895 // id can contain a '/', so encode separately 896 deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)} 897 898 if rev == "" { 899 900 //See if the document already exists, we need the rev for delete 901 _, revdoc, err2 := dbclient.ReadDoc(id) 902 if err2 != nil { 903 //set the revision to indicate that the document was not found 904 rev = "" 905 } else { 906 //set the revision to the rev returned from the document read 907 rev = revdoc 908 } 909 } 910 911 logger.Debugf(" rev=%s", rev) 912 913 //get the number of retries 914 maxRetries := dbclient.CouchInstance.conf.MaxRetries 915 916 resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "", maxRetries) 917 if err != nil { 918 fmt.Printf("couchDBReturn=%v", couchDBReturn) 919 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 920 logger.Debug("Document not found (404), returning nil value instead of 404 error") 921 // non-existent document should return nil value instead of a 404 error 922 // for details see https://github.com/hyperledger-archives/fabric/issues/936 923 return nil 924 } 925 return err 926 } 927 defer closeResponseBody(resp) 928 929 logger.Debugf("Exiting DeleteDoc()") 930 931 return nil 932 933 } 934 935 //QueryDocuments method provides function for processing a query 936 func (dbclient *CouchDatabase) QueryDocuments(query string) (*[]QueryResult, error) { 937 938 logger.Debugf("Entering QueryDocuments() query=%s", query) 939 940 var results []QueryResult 941 942 queryURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 943 if err != nil { 944 logger.Errorf("URL parse error: %s", err.Error()) 945 return nil, err 946 } 947 948 queryURL.Path = dbclient.DBName + "/_find" 949 950 //get the number of retries 951 maxRetries := dbclient.CouchInstance.conf.MaxRetries 952 953 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, queryURL.String(), []byte(query), "", "", maxRetries) 954 if err != nil { 955 return nil, err 956 } 957 defer closeResponseBody(resp) 958 959 if logger.IsEnabledFor(logging.DEBUG) { 960 dump, err2 := httputil.DumpResponse(resp, true) 961 if err2 != nil { 962 log.Fatal(err2) 963 } 964 logger.Debugf("%s", dump) 965 } 966 967 //handle as JSON document 968 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 969 if err != nil { 970 return nil, err 971 } 972 973 var jsonResponse = &QueryResponse{} 974 975 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 976 if err2 != nil { 977 return nil, err2 978 } 979 980 for _, row := range jsonResponse.Docs { 981 982 var jsonDoc = &Doc{} 983 err3 := json.Unmarshal(row, &jsonDoc) 984 if err3 != nil { 985 return nil, err3 986 } 987 988 if jsonDoc.Attachments != nil { 989 990 logger.Debugf("Adding JSON docment and attachments for id: %s", jsonDoc.ID) 991 992 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 993 if err != nil { 994 return nil, err 995 } 996 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments} 997 results = append(results, *addDocument) 998 999 } else { 1000 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 1001 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: row, Attachments: nil} 1002 1003 results = append(results, *addDocument) 1004 1005 } 1006 } 1007 logger.Debugf("Exiting QueryDocuments()") 1008 1009 return &results, nil 1010 1011 } 1012 1013 //BatchRetrieveIDRevision - batch method to retrieve IDs and revisions 1014 func (dbclient *CouchDatabase) BatchRetrieveIDRevision(keys []string) ([]*DocMetadata, error) { 1015 1016 batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 1017 if err != nil { 1018 logger.Errorf("URL parse error: %s", err.Error()) 1019 return nil, err 1020 } 1021 batchURL.Path = dbclient.DBName + "/_all_docs" 1022 1023 queryParms := batchURL.Query() 1024 queryParms.Add("include_docs", "true") 1025 batchURL.RawQuery = queryParms.Encode() 1026 1027 keymap := make(map[string]interface{}) 1028 1029 keymap["keys"] = keys 1030 1031 jsonKeys, err := json.Marshal(keymap) 1032 if err != nil { 1033 return nil, err 1034 } 1035 1036 //get the number of retries 1037 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1038 1039 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries) 1040 if err != nil { 1041 return nil, err 1042 } 1043 defer closeResponseBody(resp) 1044 1045 if logger.IsEnabledFor(logging.DEBUG) { 1046 dump, _ := httputil.DumpResponse(resp, false) 1047 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1048 logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1049 } 1050 1051 //handle as JSON document 1052 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1053 if err != nil { 1054 return nil, err 1055 } 1056 1057 var jsonResponse = &BatchRetrieveDocMedatadataResponse{} 1058 1059 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1060 if err2 != nil { 1061 return nil, err2 1062 } 1063 1064 revisionDocs := []*DocMetadata{} 1065 1066 for _, row := range jsonResponse.Rows { 1067 revisionDoc := &DocMetadata{ID: row.ID, Rev: row.Doc.Rev, Version: row.Doc.Version} 1068 revisionDocs = append(revisionDocs, revisionDoc) 1069 } 1070 1071 return revisionDocs, nil 1072 1073 } 1074 1075 //BatchUpdateDocuments - batch method to batch update documents 1076 func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*BatchUpdateResponse, error) { 1077 1078 logger.Debugf("Entering BatchUpdateDocuments() documents=%v", documents) 1079 1080 batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 1081 if err != nil { 1082 logger.Errorf("URL parse error: %s", err.Error()) 1083 return nil, err 1084 } 1085 batchURL.Path = dbclient.DBName + "/_bulk_docs" 1086 1087 documentMap := make(map[string]interface{}) 1088 1089 var jsonDocumentMap []interface{} 1090 1091 for _, jsonDocument := range documents { 1092 1093 //create a document map 1094 var document = make(map[string]interface{}) 1095 1096 //unmarshal the JSON component of the CouchDoc into the document 1097 json.Unmarshal(jsonDocument.JSONValue, &document) 1098 1099 //iterate through any attachments 1100 if len(jsonDocument.Attachments) > 0 { 1101 1102 //create a file attachment map 1103 fileAttachment := make(map[string]interface{}) 1104 1105 //for each attachment, create a Base64Attachment, name the attachment, 1106 //add the content type and base64 encode the attachment 1107 for _, attachment := range jsonDocument.Attachments { 1108 fileAttachment[attachment.Name] = Base64Attachment{attachment.ContentType, 1109 base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)} 1110 } 1111 1112 //add attachments to the document 1113 document["_attachments"] = fileAttachment 1114 1115 } 1116 1117 //Append the document to the map of documents 1118 jsonDocumentMap = append(jsonDocumentMap, document) 1119 1120 } 1121 1122 //Add the documents to the "docs" item 1123 documentMap["docs"] = jsonDocumentMap 1124 1125 jsonKeys, err := json.Marshal(documentMap) 1126 1127 if err != nil { 1128 return nil, err 1129 } 1130 1131 //get the number of retries 1132 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1133 1134 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries) 1135 if err != nil { 1136 return nil, err 1137 } 1138 defer closeResponseBody(resp) 1139 1140 if logger.IsEnabledFor(logging.DEBUG) { 1141 dump, _ := httputil.DumpResponse(resp, false) 1142 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1143 logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1144 } 1145 1146 //handle as JSON document 1147 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1148 if err != nil { 1149 return nil, err 1150 } 1151 1152 var jsonResponse = []*BatchUpdateResponse{} 1153 1154 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1155 if err2 != nil { 1156 return nil, err2 1157 } 1158 1159 logger.Debugf("Exiting BatchUpdateDocuments()") 1160 1161 return jsonResponse, nil 1162 1163 } 1164 1165 //handleRequest method is a generic http request handler. 1166 // if it returns an error, it ensures that the response body is closed, else it is the 1167 // callee's responsibility to close response correctly 1168 func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data []byte, rev string, 1169 multipartBoundary string, maxRetries int) (*http.Response, *DBReturn, error) { 1170 1171 logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL) 1172 1173 //create the return objects for couchDB 1174 var resp *http.Response 1175 var errResp error 1176 couchDBReturn := &DBReturn{} 1177 1178 //set initial wait duration for retries 1179 waitDuration := retryWaitTime * time.Millisecond 1180 1181 //attempt the http request for the max number of retries 1182 for attempts := 0; attempts < maxRetries; attempts++ { 1183 1184 //Set up a buffer for the payload data 1185 payloadData := new(bytes.Buffer) 1186 1187 payloadData.ReadFrom(bytes.NewReader(data)) 1188 1189 //Create request based on URL for couchdb operation 1190 req, err := http.NewRequest(method, connectURL, payloadData) 1191 if err != nil { 1192 return nil, nil, err 1193 } 1194 1195 //add content header for PUT 1196 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 1197 1198 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 1199 //to application/json. Else, this is contains an attachment and needs to be multipart 1200 if multipartBoundary == "" { 1201 req.Header.Set("Content-Type", "application/json") 1202 } else { 1203 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 1204 } 1205 1206 //check to see if the revision is set, if so, pass as a header 1207 if rev != "" { 1208 req.Header.Set("If-Match", rev) 1209 } 1210 } 1211 1212 //add content header for PUT 1213 if method == http.MethodPut || method == http.MethodPost { 1214 req.Header.Set("Accept", "application/json") 1215 } 1216 1217 //add content header for GET 1218 if method == http.MethodGet { 1219 req.Header.Set("Accept", "multipart/related") 1220 } 1221 1222 //If username and password are set the use basic auth 1223 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 1224 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 1225 } 1226 1227 if logger.IsEnabledFor(logging.DEBUG) { 1228 dump, _ := httputil.DumpRequestOut(req, false) 1229 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1230 logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1231 } 1232 1233 //Execute http request 1234 resp, errResp = couchInstance.client.Do(req) 1235 1236 //if an error is not detected then drop out of the retry 1237 if errResp == nil && resp != nil && resp.StatusCode < 500 { 1238 break 1239 } 1240 1241 //if this is an error, record the retry error, else this is a 500 error 1242 if errResp != nil { 1243 1244 //Log the error with the retry count and continue 1245 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Error:%v", 1246 waitDuration.String(), attempts+1, errResp.Error()) 1247 1248 } else { 1249 1250 //Read the response body and close it for next attempt 1251 jsonError, err := ioutil.ReadAll(resp.Body) 1252 closeResponseBody(resp) 1253 if err != nil { 1254 return nil, nil, err 1255 } 1256 1257 errorBytes := []byte(jsonError) 1258 1259 //Unmarshal the response 1260 json.Unmarshal(errorBytes, &couchDBReturn) 1261 1262 //Log the 500 error with the retry count and continue 1263 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Couch DB Error:%s, Status Code:%v Reason:%v", 1264 waitDuration.String(), attempts+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason) 1265 1266 } 1267 //sleep for specified sleep time, then retry 1268 time.Sleep(waitDuration) 1269 1270 //backoff, doubling the retry time for next attempt 1271 waitDuration *= 2 1272 1273 } 1274 1275 //if the error present, return the error 1276 if errResp != nil { 1277 return nil, nil, errResp 1278 } 1279 1280 //set the return code for the couchDB request 1281 couchDBReturn.StatusCode = resp.StatusCode 1282 1283 //check to see if the status code is 400 or higher 1284 //response codes 4XX and 500 will be treated as errors 1285 if resp.StatusCode >= 400 { 1286 // close the response before returning error 1287 defer closeResponseBody(resp) 1288 1289 //Read the response body 1290 jsonError, err := ioutil.ReadAll(resp.Body) 1291 if err != nil { 1292 return nil, nil, err 1293 } 1294 1295 errorBytes := []byte(jsonError) 1296 1297 //marshal the response 1298 json.Unmarshal(errorBytes, &couchDBReturn) 1299 1300 logger.Debugf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1301 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1302 1303 return nil, couchDBReturn, fmt.Errorf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1304 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1305 1306 } 1307 1308 logger.Debugf("Exiting handleRequest()") 1309 1310 //If no errors, then return the results 1311 return resp, couchDBReturn, nil 1312 } 1313 1314 //IsJSON tests a string to determine if a valid JSON 1315 func IsJSON(s string) bool { 1316 var js map[string]interface{} 1317 return json.Unmarshal([]byte(s), &js) == nil 1318 } 1319 1320 // encodePathElement uses Golang for encoding and in addition, replaces a '/' by %2F. 1321 // Otherwise, in the regular encoding, a '/' is treated as a path separator in the url 1322 func encodePathElement(str string) string { 1323 u := &url.URL{} 1324 u.Path = str 1325 encodedStr := u.String() 1326 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1327 return encodedStr 1328 } 1329 1330 func encodeForJSON(str string) (string, error) { 1331 buf := &bytes.Buffer{} 1332 encoder := json.NewEncoder(buf) 1333 if err := encoder.Encode(str); err != nil { 1334 return "", err 1335 } 1336 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1337 buffer := buf.Bytes() 1338 return string(buffer[1 : len(buffer)-2]), nil 1339 }