github.com/inklabsfoundation/inkchain@v0.17.1-0.20181025012015-c3cef8062f19/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/inklabsfoundation/inkchain/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 return nil, "", nil 686 } 687 logger.Debugf("couchDBReturn=%v\n", couchDBReturn) 688 return nil, "", err 689 } 690 defer closeResponseBody(resp) 691 692 //Get the media type from the Content-Type header 693 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 694 if err != nil { 695 log.Fatal(err) 696 } 697 698 //Get the revision from header 699 revision, err := getRevisionHeader(resp) 700 if err != nil { 701 return nil, "", err 702 } 703 704 //check to see if the is multipart, handle as attachment if multipart is detected 705 if strings.HasPrefix(mediaType, "multipart/") { 706 //Set up the multipart reader based on the boundary 707 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 708 for { 709 p, err := multipartReader.NextPart() 710 if err == io.EOF { 711 break // processed all parts 712 } 713 if err != nil { 714 return nil, "", err 715 } 716 717 defer p.Close() 718 719 logger.Debugf("part header=%s", p.Header) 720 switch p.Header.Get("Content-Type") { 721 case "application/json": 722 partdata, err := ioutil.ReadAll(p) 723 if err != nil { 724 return nil, "", err 725 } 726 couchDoc.JSONValue = partdata 727 default: 728 729 //Create an attachment structure and load it 730 attachment := &Attachment{} 731 attachment.ContentType = p.Header.Get("Content-Type") 732 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 733 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 734 switch p.Header.Get("Content-Encoding") { 735 case "gzip": //See if the part is gzip encoded 736 737 var respBody []byte 738 739 gr, err := gzip.NewReader(p) 740 if err != nil { 741 return nil, "", err 742 } 743 respBody, err = ioutil.ReadAll(gr) 744 if err != nil { 745 return nil, "", err 746 } 747 748 logger.Debugf("Retrieved attachment data") 749 attachment.AttachmentBytes = respBody 750 attachment.Name = p.FileName() 751 attachments = append(attachments, attachment) 752 753 default: 754 755 //retrieve the data, this is not gzip 756 partdata, err := ioutil.ReadAll(p) 757 if err != nil { 758 return nil, "", err 759 } 760 logger.Debugf("Retrieved attachment data") 761 attachment.AttachmentBytes = partdata 762 attachment.Name = p.FileName() 763 attachments = append(attachments, attachment) 764 765 } // end content-encoding switch 766 } // end if attachment 767 } // end content-type switch 768 } // for all multiparts 769 770 couchDoc.Attachments = attachments 771 772 return &couchDoc, revision, nil 773 } 774 775 //handle as JSON document 776 couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body) 777 if err != nil { 778 return nil, "", err 779 } 780 781 logger.Debugf("Exiting ReadDoc()") 782 return &couchDoc, revision, nil 783 } 784 785 //ReadDocRange method provides function to a range of documents based on the start and end keys 786 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 787 //This function provides a limit option to specify the max number of entries and is supplied by config. 788 //Skip is reserved for possible future future use. 789 func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) { 790 791 logger.Debugf("Entering ReadDocRange() startKey=%s, endKey=%s", startKey, endKey) 792 793 var results []QueryResult 794 795 rangeURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 796 if err != nil { 797 logger.Errorf("URL parse error: %s", err.Error()) 798 return nil, err 799 } 800 rangeURL.Path = dbclient.DBName + "/_all_docs" 801 802 queryParms := rangeURL.Query() 803 queryParms.Set("limit", strconv.Itoa(limit)) 804 queryParms.Add("skip", strconv.Itoa(skip)) 805 queryParms.Add("include_docs", "true") 806 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 807 808 //Append the startKey if provided 809 810 if startKey != "" { 811 var err error 812 if startKey, err = encodeForJSON(startKey); err != nil { 813 return nil, err 814 } 815 queryParms.Add("startkey", "\""+startKey+"\"") 816 } 817 818 //Append the endKey if provided 819 if endKey != "" { 820 var err error 821 if endKey, err = encodeForJSON(endKey); err != nil { 822 return nil, err 823 } 824 queryParms.Add("endkey", "\""+endKey+"\"") 825 } 826 827 rangeURL.RawQuery = queryParms.Encode() 828 829 //get the number of retries 830 maxRetries := dbclient.CouchInstance.conf.MaxRetries 831 832 resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries, true) 833 if err != nil { 834 return nil, err 835 } 836 defer closeResponseBody(resp) 837 838 if logger.IsEnabledFor(logging.DEBUG) { 839 dump, err2 := httputil.DumpResponse(resp, true) 840 if err2 != nil { 841 log.Fatal(err2) 842 } 843 logger.Debugf("%s", dump) 844 } 845 846 //handle as JSON document 847 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 848 if err != nil { 849 return nil, err 850 } 851 852 var jsonResponse = &RangeQueryResponse{} 853 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 854 if err2 != nil { 855 return nil, err2 856 } 857 858 logger.Debugf("Total Rows: %d", jsonResponse.TotalRows) 859 860 for _, row := range jsonResponse.Rows { 861 862 var jsonDoc = &Doc{} 863 err3 := json.Unmarshal(row.Doc, &jsonDoc) 864 if err3 != nil { 865 return nil, err3 866 } 867 868 if jsonDoc.Attachments != nil { 869 870 logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID) 871 872 couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID) 873 if err != nil { 874 return nil, err 875 } 876 877 var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments} 878 results = append(results, *addDocument) 879 880 } else { 881 882 logger.Debugf("Adding json docment for id: %s", jsonDoc.ID) 883 884 var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil} 885 results = append(results, *addDocument) 886 887 } 888 889 } 890 891 logger.Debugf("Exiting ReadDocRange()") 892 893 return &results, nil 894 895 } 896 897 //DeleteDoc method provides function to delete a document from the database by id 898 func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { 899 900 logger.Debugf("Entering DeleteDoc() id=%s", id) 901 902 deleteURL, err := url.Parse(dbclient.CouchInstance.conf.URL) 903 if err != nil { 904 logger.Errorf("URL parse error: %s", err.Error()) 905 return err 906 } 907 908 deleteURL.Path = dbclient.DBName 909 // id can contain a '/', so encode separately 910 deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)} 911 912 //get the number of retries 913 maxRetries := dbclient.CouchInstance.conf.MaxRetries 914 915 //handle the request for saving document with a retry if there is a revision conflict 916 resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete, 917 *deleteURL, nil, "", "", maxRetries, true) 918 919 if err != nil { 920 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 921 logger.Debug("Document not found (404), returning nil value instead of 404 error") 922 // non-existent document should return nil value instead of a 404 error 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, true) 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, true) 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, true) 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 //handleRequestWithRevisionRetry method is a generic http request handler with 1166 //a retry for document revision conflict errors, 1167 //which may be detected during saves or deletes that timed out from client http perspective, 1168 //but which eventually succeeded in couchdb 1169 func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method string, connectURL url.URL, data []byte, rev string, 1170 multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) { 1171 1172 //Initialize a flag for the revsion conflict 1173 revisionConflictDetected := false 1174 var resp *http.Response 1175 var couchDBReturn *DBReturn 1176 var errResp error 1177 1178 //attempt the http request for the max number of retries 1179 //In this case, the retry is to catch problems where a client timeout may miss a 1180 //successful CouchDB update and cause a document revision conflict on a retry in handleRequest 1181 for attempts := 0; attempts < maxRetries; attempts++ { 1182 1183 //if the revision was not passed in, or if a revision conflict is detected on prior attempt, 1184 //query CouchDB for the document revision 1185 if rev == "" || revisionConflictDetected { 1186 rev = dbclient.getDocumentRevision(id) 1187 } 1188 1189 //handle the request for saving/deleting the couchdb data 1190 resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(method, connectURL.String(), 1191 data, rev, multipartBoundary, maxRetries, keepConnectionOpen) 1192 1193 //If there was a 409 conflict error during the save/delete, log it and retry it. 1194 //Otherwise, break out of the retry loop 1195 if couchDBReturn != nil && couchDBReturn.StatusCode == 409 { 1196 logger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1) 1197 revisionConflictDetected = true 1198 } else { 1199 break 1200 } 1201 } 1202 1203 // return the handleRequest results 1204 return resp, couchDBReturn, errResp 1205 } 1206 1207 //handleRequest method is a generic http request handler. 1208 // If it returns an error, it ensures that the response body is closed, else it is the 1209 // callee's responsibility to close response correctly. 1210 // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned 1211 func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data []byte, rev string, 1212 multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) { 1213 1214 logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL) 1215 1216 //create the return objects for couchDB 1217 var resp *http.Response 1218 var errResp error 1219 couchDBReturn := &DBReturn{} 1220 1221 //set initial wait duration for retries 1222 waitDuration := retryWaitTime * time.Millisecond 1223 1224 if maxRetries < 1 { 1225 return nil, nil, fmt.Errorf("Number of retries must be greater than zero.") 1226 } 1227 1228 //attempt the http request for the max number of retries 1229 for attempts := 0; attempts < maxRetries; attempts++ { 1230 1231 //Set up a buffer for the payload data 1232 payloadData := new(bytes.Buffer) 1233 1234 payloadData.ReadFrom(bytes.NewReader(data)) 1235 1236 //Create request based on URL for couchdb operation 1237 req, err := http.NewRequest(method, connectURL, payloadData) 1238 if err != nil { 1239 return nil, nil, err 1240 } 1241 1242 //set the request to close on completion if shared connections are not allowSharedConnection 1243 //Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused. 1244 //Apache JIRA item for CouchDB https://issues.apache.org/jira/browse/COUCHDB-3394 1245 if !keepConnectionOpen { 1246 req.Close = true 1247 } 1248 1249 //add content header for PUT 1250 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 1251 1252 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 1253 //to application/json. Else, this is contains an attachment and needs to be multipart 1254 if multipartBoundary == "" { 1255 req.Header.Set("Content-Type", "application/json") 1256 } else { 1257 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 1258 } 1259 1260 //check to see if the revision is set, if so, pass as a header 1261 if rev != "" { 1262 req.Header.Set("If-Match", rev) 1263 } 1264 } 1265 1266 //add content header for PUT 1267 if method == http.MethodPut || method == http.MethodPost { 1268 req.Header.Set("Accept", "application/json") 1269 } 1270 1271 //add content header for GET 1272 if method == http.MethodGet { 1273 req.Header.Set("Accept", "multipart/related") 1274 } 1275 1276 //If username and password are set the use basic auth 1277 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 1278 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 1279 } 1280 1281 if logger.IsEnabledFor(logging.DEBUG) { 1282 dump, _ := httputil.DumpRequestOut(req, false) 1283 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1284 logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1285 } 1286 1287 //Execute http request 1288 resp, errResp = couchInstance.client.Do(req) 1289 1290 //check to see if the return from CouchDB is valid 1291 if invalidCouchDBReturn(resp, errResp) { 1292 continue 1293 } 1294 1295 //if there is no golang http error and no CouchDB 500 error, then drop out of the retry 1296 if errResp == nil && resp != nil && resp.StatusCode < 500 { 1297 break 1298 } 1299 1300 //if this is an unexpected golang http error, log the error and retry 1301 if errResp != nil { 1302 1303 //Log the error with the retry count and continue 1304 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Error:%v", 1305 waitDuration.String(), attempts+1, errResp.Error()) 1306 1307 //otherwise this is an unexpected 500 error from CouchDB. Log the error and retry. 1308 } else { 1309 //Read the response body and close it for next attempt 1310 jsonError, err := ioutil.ReadAll(resp.Body) 1311 closeResponseBody(resp) 1312 if err != nil { 1313 return nil, nil, err 1314 } 1315 1316 errorBytes := []byte(jsonError) 1317 1318 //Unmarshal the response 1319 json.Unmarshal(errorBytes, &couchDBReturn) 1320 1321 //Log the 500 error with the retry count and continue 1322 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Couch DB Error:%s, Status Code:%v Reason:%v", 1323 waitDuration.String(), attempts+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason) 1324 1325 } 1326 //sleep for specified sleep time, then retry 1327 time.Sleep(waitDuration) 1328 1329 //backoff, doubling the retry time for next attempt 1330 waitDuration *= 2 1331 1332 } // end retry loop 1333 1334 //if a golang http error is still present after retries are exhausted, return the error 1335 if errResp != nil { 1336 return nil, nil, errResp 1337 } 1338 1339 //This situation should not occur according to the golang spec. 1340 //if this error returned (errResp) from an http call, then the resp should be not nil, 1341 //this is a structure and StatusCode is an int 1342 //This is meant to provide a more graceful error if this should occur 1343 if invalidCouchDBReturn(resp, errResp) { 1344 return nil, nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port.") 1345 } 1346 1347 //set the return code for the couchDB request 1348 couchDBReturn.StatusCode = resp.StatusCode 1349 1350 //check to see if the status code from couchdb is 400 or higher 1351 //response codes 4XX and 500 will be treated as errors - 1352 //golang error will be created from the couchDBReturn contents and both will be returned 1353 if resp.StatusCode >= 400 { 1354 // close the response before returning error 1355 defer closeResponseBody(resp) 1356 1357 //Read the response body 1358 jsonError, err := ioutil.ReadAll(resp.Body) 1359 if err != nil { 1360 return nil, nil, err 1361 } 1362 1363 errorBytes := []byte(jsonError) 1364 1365 //marshal the response 1366 json.Unmarshal(errorBytes, &couchDBReturn) 1367 1368 logger.Debugf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1369 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1370 1371 return nil, couchDBReturn, fmt.Errorf("Couch DB Error:%s, Status Code:%v, Reason:%s", 1372 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1373 1374 } 1375 1376 logger.Debugf("Exiting handleRequest()") 1377 1378 //If no errors, then return the http response and the couchdb return object 1379 return resp, couchDBReturn, nil 1380 } 1381 1382 //invalidCouchDBResponse checks to make sure either a valid response or error is returned 1383 func invalidCouchDBReturn(resp *http.Response, errResp error) bool { 1384 if resp == nil && errResp == nil { 1385 return true 1386 } 1387 return false 1388 } 1389 1390 //IsJSON tests a string to determine if a valid JSON 1391 func IsJSON(s string) bool { 1392 var js map[string]interface{} 1393 return json.Unmarshal([]byte(s), &js) == nil 1394 } 1395 1396 // encodePathElement uses Golang for encoding and in addition, replaces a '/' by %2F. 1397 // Otherwise, in the regular encoding, a '/' is treated as a path separator in the url 1398 func encodePathElement(str string) string { 1399 u := &url.URL{} 1400 u.Path = str 1401 encodedStr := u.String() 1402 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1403 return encodedStr 1404 } 1405 1406 func encodeForJSON(str string) (string, error) { 1407 buf := &bytes.Buffer{} 1408 encoder := json.NewEncoder(buf) 1409 if err := encoder.Encode(str); err != nil { 1410 return "", err 1411 } 1412 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1413 buffer := buf.Bytes() 1414 return string(buffer[1 : len(buffer)-2]), nil 1415 }