github.com/darrenli6/fabric-sdk-example@v0.0.0-20220109053535-94b13b56df8c/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, true) 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, true) 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, true) 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, true) 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, true) 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 469 if !utf8.ValidString(id) { 470 return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 471 } 472 473 saveURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 474 if err != nil { 475 logger.Errorf("URL parse error: %s", err.Error()) 476 return "", err 477 } 478 479 saveURL.Path = dbclient.DBName 480 // id can contain a '/', so encode separately 481 saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)} 482 483 logger.Debugf(" rev=%s", rev) 484 485 //Set up a buffer for the data to be pushed to couchdb 486 data := []byte{} 487 488 //Set up a default boundary for use by multipart if sending attachments 489 defaultBoundary := "" 490 491 //Create a flag for shared connections. This is set to false for zero length attachments 492 keepConnectionOpen := true 493 494 //check to see if attachments is nil, if so, then this is a JSON only 495 if couchDoc.Attachments == nil { 496 497 //Test to see if this is a valid JSON 498 if IsJSON(string(couchDoc.JSONValue)) != true { 499 return "", fmt.Errorf("JSON format is not valid") 500 } 501 502 // if there are no attachments, then use the bytes passed in as the JSON 503 data = couchDoc.JSONValue 504 505 } else { // there are attachments 506 507 //attachments are included, create the multipart definition 508 multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary) 509 if err3 != nil { 510 return "", err3 511 } 512 513 //If there is a zero length attachment, do not keep the connection open 514 for _, attach := range couchDoc.Attachments { 515 if attach.Length < 1 { 516 keepConnectionOpen = false 517 } 518 } 519 520 //Set the data buffer to the data from the create multi-part data 521 data = multipartData.Bytes() 522 523 //Set the default boundary to the value generated in the multipart creation 524 defaultBoundary = multipartBoundary 525 526 } 527 528 //get the number of retries 529 maxRetries := dbclient.CouchInstance.conf.MaxRetries 530 531 //handle the request for saving document with a retry if there is a revision conflict 532 resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut, 533 *saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen) 534 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 //getDocumentRevision will return the revision if the document exists, otherwise it will return "" 553 func (dbclient *CouchDatabase) getDocumentRevision(id string) string { 554 555 var rev = "" 556 557 //See if the document already exists, we need the rev for saves and deletes 558 _, revdoc, err := dbclient.ReadDoc(id) 559 if err == nil { 560 //set the revision to the rev returned from the document read 561 rev = revdoc 562 } 563 return rev 564 } 565 566 func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) { 567 568 //Create a buffer for writing the result 569 writeBuffer := new(bytes.Buffer) 570 571 // read the attachment and save as an attachment 572 writer := multipart.NewWriter(writeBuffer) 573 574 //retrieve the boundary for the multipart 575 defaultBoundary = writer.Boundary() 576 577 fileAttachments := map[string]FileDetails{} 578 579 for _, attachment := range couchDoc.Attachments { 580 fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)} 581 } 582 583 attachmentJSONMap := map[string]interface{}{ 584 "_attachments": fileAttachments} 585 586 //Add any data uploaded with the files 587 if couchDoc.JSONValue != nil { 588 589 //create a generic map 590 genericMap := make(map[string]interface{}) 591 592 //unmarshal the data into the generic map 593 decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.JSONValue)) 594 decoder.UseNumber() 595 decoder.Decode(&genericMap) 596 597 //add all key/values to the attachmentJSONMap 598 for jsonKey, jsonValue := range genericMap { 599 attachmentJSONMap[jsonKey] = jsonValue 600 } 601 602 } 603 604 filesForUpload, _ := json.Marshal(attachmentJSONMap) 605 logger.Debugf(string(filesForUpload)) 606 607 //create the header for the JSON 608 header := make(textproto.MIMEHeader) 609 header.Set("Content-Type", "application/json") 610 611 part, err := writer.CreatePart(header) 612 if err != nil { 613 return *writeBuffer, defaultBoundary, err 614 } 615 616 part.Write(filesForUpload) 617 618 for _, attachment := range couchDoc.Attachments { 619 620 header := make(textproto.MIMEHeader) 621 part, err2 := writer.CreatePart(header) 622 if err2 != nil { 623 return *writeBuffer, defaultBoundary, err2 624 } 625 part.Write(attachment.AttachmentBytes) 626 627 } 628 629 err = writer.Close() 630 if err != nil { 631 return *writeBuffer, defaultBoundary, err 632 } 633 634 return *writeBuffer, defaultBoundary, nil 635 636 } 637 638 func getRevisionHeader(resp *http.Response) (string, error) { 639 640 revision := resp.Header.Get("Etag") 641 642 if revision == "" { 643 return "", fmt.Errorf("No revision tag detected") 644 } 645 646 reg := regexp.MustCompile(`"([^"]*)"`) 647 revisionNoQuotes := reg.ReplaceAllString(revision, "${1}") 648 return revisionNoQuotes, nil 649 650 } 651 652 //ReadDoc method provides function to retrieve a document and its revision 653 //from the database by id 654 func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) { 655 var couchDoc CouchDoc 656 attachments := []*Attachment{} 657 658 logger.Debugf("Entering ReadDoc() id=[%s]", id) 659 if !utf8.ValidString(id) { 660 return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id) 661 } 662 663 readURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 664 if err != nil { 665 logger.Errorf("URL parse error: %s", err.Error()) 666 return nil, "", err 667 } 668 readURL.Path = dbclient.DBName 669 // id can contain a '/', so encode separately 670 readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)} 671 672 query := readURL.Query() 673 query.Add("attachments", "true") 674 675 readURL.RawQuery = query.Encode() 676 677 //get the number of retries 678 maxRetries := dbclient.CouchInstance.conf.MaxRetries 679 680 resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "", maxRetries, true) 681 if err != nil { 682 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 683 logger.Debug("Document not found (404), returning nil value instead of 404 error") 684 // non-existent document should return nil value instead of a 404 error 685 // for details see https://github.com/hyperledger-archives/fabric/issues/936 686 return nil, "", nil 687 } 688 logger.Debugf("couchDBReturn=%v\n", couchDBReturn) 689 return nil, "", err 690 } 691 defer closeResponseBody(resp) 692 693 //Get the media type from the Content-Type header 694 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 695 if err != nil { 696 log.Fatal(err) 697 } 698 699 //Get the revision from header 700 revision, err := getRevisionHeader(resp) 701 if err != nil { 702 return nil, "", err 703 } 704 705 //check to see if the is multipart, handle as attachment if multipart is detected 706 if strings.HasPrefix(mediaType, "multipart/") { 707 //Set up the multipart reader based on the boundary 708 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 709 for { 710 p, err := multipartReader.NextPart() 711 if err == io.EOF { 712 break // processed all parts 713 } 714 if err != nil { 715 return nil, "", err 716 } 717 718 defer p.Close() 719 720 logger.Debugf("part header=%s", p.Header) 721 switch p.Header.Get("Content-Type") { 722 case "application/json": 723 partdata, err := ioutil.ReadAll(p) 724 if err != nil { 725 return nil, "", err 726 } 727 couchDoc.JSONValue = partdata 728 default: 729 730 //Create an attachment structure and load it 731 attachment := &Attachment{} 732 attachment.ContentType = p.Header.Get("Content-Type") 733 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 734 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 735 switch p.Header.Get("Content-Encoding") { 736 case "gzip": //See if the part is gzip encoded 737 738 var respBody []byte 739 740 gr, err := gzip.NewReader(p) 741 if err != nil { 742 return nil, "", err 743 } 744 respBody, err = ioutil.ReadAll(gr) 745 if err != nil { 746 return nil, "", err 747 } 748 749 logger.Debugf("Retrieved attachment data") 750 attachment.AttachmentBytes = respBody 751 attachment.Name = p.FileName() 752 attachments = append(attachments, attachment) 753 754 default: 755 756 //retrieve the data, this is not gzip 757 partdata, err := ioutil.ReadAll(p) 758 if err != nil { 759 return nil, "", err 760 } 761 logger.Debugf("Retrieved attachment data") 762 attachment.AttachmentBytes = partdata 763 attachment.Name = p.FileName() 764 attachments = append(attachments, attachment) 765 766 } // end content-encoding switch 767 } // end if attachment 768 } // end content-type switch 769 } // for all multiparts 770 771 couchDoc.Attachments = attachments 772 773 return &couchDoc, revision, nil 774 } 775 776 //handle as JSON document 777 couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body) 778 if err != nil { 779 return nil, "", err 780 } 781 782 logger.Debugf("Exiting ReadDoc()") 783 return &couchDoc, revision, nil 784 } 785 786 //ReadDocRange method provides function to a range of documents based on the start and end keys 787 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 788 //This function provides a limit option to specify the max number of entries and is supplied by config. 789 //Skip is reserved for possible future future use. 790 func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) { 791 792 logger.Debugf("Entering ReadDocRange() startKey=%s, endKey=%s", startKey, endKey) 793 794 var results []QueryResult 795 796 rangeURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 797 if err != nil { 798 logger.Errorf("URL parse error: %s", err.Error()) 799 return nil, err 800 } 801 rangeURL.Path = dbclient.DBName + "/_all_docs" 802 803 queryParms := rangeURL.Query() 804 queryParms.Set("limit", strconv.Itoa(limit)) 805 queryParms.Add("skip", strconv.Itoa(skip)) 806 queryParms.Add("include_docs", "true") 807 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 808 809 //Append the startKey if provided 810 811 if startKey != "" { 812 var err error 813 if startKey, err = encodeForJSON(startKey); err != nil { 814 return nil, err 815 } 816 queryParms.Add("startkey", "\""+startKey+"\"") 817 } 818 819 //Append the endKey if provided 820 if endKey != "" { 821 var err error 822 if endKey, err = encodeForJSON(endKey); err != nil { 823 return nil, err 824 } 825 queryParms.Add("endkey", "\""+endKey+"\"") 826 } 827 828 rangeURL.RawQuery = queryParms.Encode() 829 830 //get the number of retries 831 maxRetries := dbclient.CouchInstance.conf.MaxRetries 832 833 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries, true) 834 if err != nil { 835 return nil, err 836 } 837 defer closeResponseBody(resp) 838 839 if logger.IsEnabledFor(logging.DEBUG) { 840 dump, err2 := httputil.DumpResponse(resp, true) 841 if err2 != nil { 842 log.Fatal(err2) 843 } 844 logger.Debugf("%s", dump) 845 } 846 847 //handle as JSON document 848 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 849 if err != nil { 850 return nil, err 851 } 852 853 var jsonResponse = &RangeQueryResponse{} 854 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 855 if err2 != nil { 856 return nil, err2 857 } 858 859 logger.Debugf("Total Rows: %d", jsonResponse.TotalRows) 860 861 for _, row := range jsonResponse.Rows { 862 863 var jsonDoc = &Doc{} 864 err3 := json.Unmarshal(row.Doc, &jsonDoc) 865 if err3 != nil { 866 return nil, err3 867 } 868 869 if jsonDoc.Attachments != nil { 870 871 logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID) 872 873 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 874 if err != nil { 875 return nil, err 876 } 877 878 var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments} 879 results = append(results, *addDocument) 880 881 } else { 882 883 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 884 885 var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil} 886 results = append(results, *addDocument) 887 888 } 889 890 } 891 892 logger.Debugf("Exiting ReadDocRange()") 893 894 return &results, nil 895 896 } 897 898 //DeleteDoc method provides function to delete a document from the database by id 899 func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { 900 901 logger.Debugf("Entering DeleteDoc() id=%s", id) 902 903 deleteURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 904 if err != nil { 905 logger.Errorf("URL parse error: %s", err.Error()) 906 return err 907 } 908 909 deleteURL.Path = dbclient.DBName 910 // id can contain a '/', so encode separately 911 deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)} 912 913 //get the number of retries 914 maxRetries := dbclient.CouchInstance.conf.MaxRetries 915 916 //handle the request for saving document with a retry if there is a revision conflict 917 resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete, 918 *deleteURL, nil, "", "", maxRetries, true) 919 920 if err != nil { 921 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 922 logger.Debug("Document not found (404), returning nil value instead of 404 error") 923 // non-existent document should return nil value instead of a 404 error 924 // for details see https://github.com/hyperledger-archives/fabric/issues/936 925 return nil 926 } 927 return err 928 } 929 defer closeResponseBody(resp) 930 931 logger.Debugf("Exiting DeleteDoc()") 932 933 return nil 934 935 } 936 937 //QueryDocuments method provides function for processing a query 938 func (dbclient *CouchDatabase) QueryDocuments(query string) (*[]QueryResult, error) { 939 940 logger.Debugf("Entering QueryDocuments() query=%s", query) 941 942 var results []QueryResult 943 944 queryURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 945 if err != nil { 946 logger.Errorf("URL parse error: %s", err.Error()) 947 return nil, err 948 } 949 950 queryURL.Path = dbclient.DBName + "/_find" 951 952 //get the number of retries 953 maxRetries := dbclient.CouchInstance.conf.MaxRetries 954 955 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, queryURL.String(), []byte(query), "", "", maxRetries, true) 956 if err != nil { 957 return nil, err 958 } 959 defer closeResponseBody(resp) 960 961 if logger.IsEnabledFor(logging.DEBUG) { 962 dump, err2 := httputil.DumpResponse(resp, true) 963 if err2 != nil { 964 log.Fatal(err2) 965 } 966 logger.Debugf("%s", dump) 967 } 968 969 //handle as JSON document 970 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 971 if err != nil { 972 return nil, err 973 } 974 975 var jsonResponse = &QueryResponse{} 976 977 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 978 if err2 != nil { 979 return nil, err2 980 } 981 982 for _, row := range jsonResponse.Docs { 983 984 var jsonDoc = &Doc{} 985 err3 := json.Unmarshal(row, &jsonDoc) 986 if err3 != nil { 987 return nil, err3 988 } 989 990 if jsonDoc.Attachments != nil { 991 992 logger.Debugf("Adding JSON docment and attachments for id: %s", jsonDoc.ID) 993 994 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 995 if err != nil { 996 return nil, err 997 } 998 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments} 999 results = append(results, *addDocument) 1000 1001 } else { 1002 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 1003 var addDocument = &QueryResult{ID: jsonDoc.ID, Value: row, Attachments: nil} 1004 1005 results = append(results, *addDocument) 1006 1007 } 1008 } 1009 logger.Debugf("Exiting QueryDocuments()") 1010 1011 return &results, nil 1012 1013 } 1014 1015 //BatchRetrieveIDRevision - batch method to retrieve IDs and revisions 1016 func (dbclient *CouchDatabase) BatchRetrieveIDRevision(keys []string) ([]*DocMetadata, error) { 1017 1018 batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 1019 if err != nil { 1020 logger.Errorf("URL parse error: %s", err.Error()) 1021 return nil, err 1022 } 1023 batchURL.Path = dbclient.DBName + "/_all_docs" 1024 1025 queryParms := batchURL.Query() 1026 queryParms.Add("include_docs", "true") 1027 batchURL.RawQuery = queryParms.Encode() 1028 1029 keymap := make(map[string]interface{}) 1030 1031 keymap["keys"] = keys 1032 1033 jsonKeys, err := json.Marshal(keymap) 1034 if err != nil { 1035 return nil, err 1036 } 1037 1038 //get the number of retries 1039 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1040 1041 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true) 1042 if err != nil { 1043 return nil, err 1044 } 1045 defer closeResponseBody(resp) 1046 1047 if logger.IsEnabledFor(logging.DEBUG) { 1048 dump, _ := httputil.DumpResponse(resp, false) 1049 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1050 logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1051 } 1052 1053 //handle as JSON document 1054 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1055 if err != nil { 1056 return nil, err 1057 } 1058 1059 var jsonResponse = &BatchRetrieveDocMedatadataResponse{} 1060 1061 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1062 if err2 != nil { 1063 return nil, err2 1064 } 1065 1066 revisionDocs := []*DocMetadata{} 1067 1068 for _, row := range jsonResponse.Rows { 1069 revisionDoc := &DocMetadata{ID: row.ID, Rev: row.Doc.Rev, Version: row.Doc.Version} 1070 revisionDocs = append(revisionDocs, revisionDoc) 1071 } 1072 1073 return revisionDocs, nil 1074 1075 } 1076 1077 //BatchUpdateDocuments - batch method to batch update documents 1078 func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*BatchUpdateResponse, error) { 1079 1080 logger.Debugf("Entering BatchUpdateDocuments() documents=%v", documents) 1081 1082 batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 1083 if err != nil { 1084 logger.Errorf("URL parse error: %s", err.Error()) 1085 return nil, err 1086 } 1087 batchURL.Path = dbclient.DBName + "/_bulk_docs" 1088 1089 documentMap := make(map[string]interface{}) 1090 1091 var jsonDocumentMap []interface{} 1092 1093 for _, jsonDocument := range documents { 1094 1095 //create a document map 1096 var document = make(map[string]interface{}) 1097 1098 //unmarshal the JSON component of the CouchDoc into the document 1099 json.Unmarshal(jsonDocument.JSONValue, &document) 1100 1101 //iterate through any attachments 1102 if len(jsonDocument.Attachments) > 0 { 1103 1104 //create a file attachment map 1105 fileAttachment := make(map[string]interface{}) 1106 1107 //for each attachment, create a Base64Attachment, name the attachment, 1108 //add the content type and base64 encode the attachment 1109 for _, attachment := range jsonDocument.Attachments { 1110 fileAttachment[attachment.Name] = Base64Attachment{attachment.ContentType, 1111 base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)} 1112 } 1113 1114 //add attachments to the document 1115 document["_attachments"] = fileAttachment 1116 1117 } 1118 1119 //Append the document to the map of documents 1120 jsonDocumentMap = append(jsonDocumentMap, document) 1121 1122 } 1123 1124 //Add the documents to the "docs" item 1125 documentMap["docs"] = jsonDocumentMap 1126 1127 jsonKeys, err := json.Marshal(documentMap) 1128 1129 if err != nil { 1130 return nil, err 1131 } 1132 1133 //get the number of retries 1134 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1135 1136 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true) 1137 if err != nil { 1138 return nil, err 1139 } 1140 defer closeResponseBody(resp) 1141 1142 if logger.IsEnabledFor(logging.DEBUG) { 1143 dump, _ := httputil.DumpResponse(resp, false) 1144 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1145 logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1146 } 1147 1148 //handle as JSON document 1149 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1150 if err != nil { 1151 return nil, err 1152 } 1153 1154 var jsonResponse = []*BatchUpdateResponse{} 1155 1156 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1157 if err2 != nil { 1158 return nil, err2 1159 } 1160 1161 logger.Debugf("Exiting BatchUpdateDocuments()") 1162 1163 return jsonResponse, nil 1164 1165 } 1166 1167 //handleRequestWithRevisionRetry method is a generic http request handler with 1168 //a retry for document revision conflict errors, 1169 //which may be detected during saves or deletes that timed out from client http perspective, 1170 //but which eventually succeeded in couchdb 1171 func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method string, connectURL url.URL, data []byte, rev string, 1172 multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) { 1173 1174 //Initialize a flag for the revsion conflict 1175 revisionConflictDetected := false 1176 var resp *http.Response 1177 var couchDBReturn *DBReturn 1178 var errResp error 1179 1180 //attempt the http request for the max number of retries 1181 //In this case, the retry is to catch problems where a client timeout may miss a 1182 //successful CouchDB update and cause a document revision conflict on a retry in handleRequest 1183 for attempts := 0; attempts < maxRetries; attempts++ { 1184 1185 //if the revision was not passed in, or if a revision conflict is detected on prior attempt, 1186 //query CouchDB for the document revision 1187 if rev == "" || revisionConflictDetected { 1188 rev = dbclient.getDocumentRevision(id) 1189 } 1190 1191 //handle the request for saving/deleting the couchdb data 1192 resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(method, connectURL.String(), 1193 data, rev, multipartBoundary, maxRetries, keepConnectionOpen) 1194 1195 //If there was a 409 conflict error during the save/delete, log it and retry it. 1196 //Otherwise, break out of the retry loop 1197 if couchDBReturn != nil && couchDBReturn.StatusCode == 409 { 1198 logger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1) 1199 revisionConflictDetected = true 1200 } else { 1201 break 1202 } 1203 } 1204 1205 // return the handleRequest results 1206 return resp, couchDBReturn, errResp 1207 } 1208 1209 //handleRequest method is a generic http request handler. 1210 // If it returns an error, it ensures that the response body is closed, else it is the 1211 // callee's responsibility to close response correctly. 1212 // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned 1213 func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data []byte, rev string, 1214 multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) { 1215 1216 logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL) 1217 1218 //create the return objects for couchDB 1219 var resp *http.Response 1220 var errResp error 1221 couchDBReturn := &DBReturn{} 1222 1223 //set initial wait duration for retries 1224 waitDuration := retryWaitTime * time.Millisecond 1225 1226 if maxRetries < 1 { 1227 return nil, nil, fmt.Errorf("Number of retries must be greater than zero.") 1228 } 1229 1230 //attempt the http request for the max number of retries 1231 for attempts := 0; attempts < maxRetries; attempts++ { 1232 1233 //Set up a buffer for the payload data 1234 payloadData := new(bytes.Buffer) 1235 1236 payloadData.ReadFrom(bytes.NewReader(data)) 1237 1238 //Create request based on URL for couchdb operation 1239 req, err := http.NewRequest(method, connectURL, payloadData) 1240 if err != nil { 1241 return nil, nil, err 1242 } 1243 1244 //set the request to close on completion if shared connections are not allowSharedConnection 1245 //Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused. 1246 //Apache JIRA item for CouchDB https://issues.apache.org/jira/browse/COUCHDB-3394 1247 if !keepConnectionOpen { 1248 req.Close = true 1249 } 1250 1251 //add content header for PUT 1252 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 1253 1254 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 1255 //to application/json. Else, this is contains an attachment and needs to be multipart 1256 if multipartBoundary == "" { 1257 req.Header.Set("Content-Type", "application/json") 1258 } else { 1259 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 1260 } 1261 1262 //check to see if the revision is set, if so, pass as a header 1263 if rev != "" { 1264 req.Header.Set("If-Match", rev) 1265 } 1266 } 1267 1268 //add content header for PUT 1269 if method == http.MethodPut || method == http.MethodPost { 1270 req.Header.Set("Accept", "application/json") 1271 } 1272 1273 //add content header for GET 1274 if method == http.MethodGet { 1275 req.Header.Set("Accept", "multipart/related") 1276 } 1277 1278 //If username and password are set the use basic auth 1279 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 1280 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 1281 } 1282 1283 if logger.IsEnabledFor(logging.DEBUG) { 1284 dump, _ := httputil.DumpRequestOut(req, false) 1285 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1286 logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1287 } 1288 1289 //Execute http request 1290 resp, errResp = couchInstance.client.Do(req) 1291 1292 //check to see if the return from CouchDB is valid 1293 if invalidCouchDBReturn(resp, errResp) { 1294 continue 1295 } 1296 1297 //if there is no golang http error and no CouchDB 500 error, then drop out of the retry 1298 if errResp == nil && resp != nil && resp.StatusCode < 500 { 1299 break 1300 } 1301 1302 //if this is an unexpected golang http error, log the error and retry 1303 if errResp != nil { 1304 1305 //Log the error with the retry count and continue 1306 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Error:%v", 1307 waitDuration.String(), attempts+1, errResp.Error()) 1308 1309 //otherwise this is an unexpected 500 error from CouchDB. Log the error and retry. 1310 } else { 1311 //Read the response body and close it for next attempt 1312 jsonError, err := ioutil.ReadAll(resp.Body) 1313 closeResponseBody(resp) 1314 if err != nil { 1315 return nil, nil, err 1316 } 1317 1318 errorBytes := []byte(jsonError) 1319 1320 //Unmarshal the response 1321 json.Unmarshal(errorBytes, &couchDBReturn) 1322 1323 //Log the 500 error with the retry count and continue 1324 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Couch DB Error:%s, Status Code:%v Reason:%v", 1325 waitDuration.String(), attempts+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason) 1326 1327 } 1328 //sleep for specified sleep time, then retry 1329 time.Sleep(waitDuration) 1330 1331 //backoff, doubling the retry time for next attempt 1332 waitDuration *= 2 1333 1334 } // end retry loop 1335 1336 //if a golang http error is still present after retries are exhausted, return the error 1337 if errResp != nil { 1338 return nil, nil, errResp 1339 } 1340 1341 //This situation should not occur according to the golang spec. 1342 //if this error returned (errResp) from an http call, then the resp should be not nil, 1343 //this is a structure and StatusCode is an int 1344 //This is meant to provide a more graceful error if this should occur 1345 if invalidCouchDBReturn(resp, errResp) { 1346 return nil, nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port.") 1347 } 1348 1349 //set the return code for the couchDB request 1350 couchDBReturn.StatusCode = resp.StatusCode 1351 1352 //check to see if the status code from couchdb is 400 or higher 1353 //response codes 4XX and 500 will be treated as errors - 1354 //golang error will be created from the couchDBReturn contents and both will be returned 1355 if resp.StatusCode >= 400 { 1356 // close the response before returning error 1357 defer closeResponseBody(resp) 1358 1359 //Read the response body 1360 jsonError, err := ioutil.ReadAll(resp.Body) 1361 if err != nil { 1362 return nil, nil, err 1363 } 1364 1365 errorBytes := []byte(jsonError) 1366 1367 //marshal the response 1368 json.Unmarshal(errorBytes, &couchDBReturn) 1369 1370 logger.Debugf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1371 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1372 1373 return nil, couchDBReturn, fmt.Errorf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1374 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1375 1376 } 1377 1378 logger.Debugf("Exiting handleRequest()") 1379 1380 //If no errors, then return the http response and the couchdb return object 1381 return resp, couchDBReturn, nil 1382 } 1383 1384 //invalidCouchDBResponse checks to make sure either a valid response or error is returned 1385 func invalidCouchDBReturn(resp *http.Response, errResp error) bool { 1386 if resp == nil && errResp == nil { 1387 return true 1388 } 1389 return false 1390 } 1391 1392 //IsJSON tests a string to determine if a valid JSON 1393 func IsJSON(s string) bool { 1394 var js map[string]interface{} 1395 return json.Unmarshal([]byte(s), &js) == nil 1396 } 1397 1398 // encodePathElement uses Golang for url path encoding, additionally: 1399 // '/' is replaced by %2F, otherwise path encoding will treat as path separator and ignore it 1400 // '+' is replaced by %2B, otherwise path encoding will ignore it, while CouchDB will unencode the plus as a space 1401 // Note that all other URL special characters have been tested successfully without need for special handling 1402 func encodePathElement(str string) string { 1403 1404 logger.Debugf("Entering encodePathElement() string=%s", str) 1405 1406 u := &url.URL{} 1407 u.Path = str 1408 encodedStr := u.EscapedPath() // url encode using golang url path encoding rules 1409 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1410 encodedStr = strings.Replace(encodedStr, "+", "%2B", -1) 1411 1412 logger.Debugf("Exiting encodePathElement() encodedStr=%s", encodedStr) 1413 1414 return encodedStr 1415 } 1416 1417 func encodeForJSON(str string) (string, error) { 1418 buf := &bytes.Buffer{} 1419 encoder := json.NewEncoder(buf) 1420 if err := encoder.Encode(str); err != nil { 1421 return "", err 1422 } 1423 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1424 buffer := buf.Bytes() 1425 return string(buffer[1 : len(buffer)-2]), nil 1426 }