github.com/true-sqn/fabric@v2.1.1+incompatible/core/ledger/util/couchdb/couchdb.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package couchdb 8 9 import ( 10 "bytes" 11 "compress/gzip" 12 "context" 13 "encoding/base64" 14 "encoding/json" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "log" 19 "mime" 20 "mime/multipart" 21 "net/http" 22 "net/http/httputil" 23 "net/textproto" 24 "net/url" 25 "regexp" 26 "strconv" 27 "strings" 28 "time" 29 "unicode/utf8" 30 31 "github.com/hyperledger/fabric/common/flogging" 32 "github.com/pkg/errors" 33 "go.uber.org/zap/zapcore" 34 ) 35 36 var logger = flogging.MustGetLogger("couchdb") 37 38 //time between retry attempts in milliseconds 39 const retryWaitTime = 125 40 41 // DBOperationResponse is body for successful database calls. 42 type DBOperationResponse struct { 43 Ok bool 44 } 45 46 // DBInfo is body for database information. 47 type DBInfo struct { 48 DbName string `json:"db_name"` 49 Sizes struct { 50 File int `json:"file"` 51 External int `json:"external"` 52 Active int `json:"active"` 53 } `json:"sizes"` 54 Other struct { 55 DataSize int `json:"data_size"` 56 } `json:"other"` 57 DocDelCount int `json:"doc_del_count"` 58 DocCount int `json:"doc_count"` 59 DiskSize int `json:"disk_size"` 60 DiskFormatVersion int `json:"disk_format_version"` 61 DataSize int `json:"data_size"` 62 CompactRunning bool `json:"compact_running"` 63 InstanceStartTime string `json:"instance_start_time"` 64 } 65 66 //ConnectionInfo is a structure for capturing the database info and version 67 type ConnectionInfo struct { 68 Couchdb string `json:"couchdb"` 69 Version string `json:"version"` 70 Vendor struct { 71 Name string `json:"name"` 72 } `json:"vendor"` 73 } 74 75 //RangeQueryResponse is used for processing REST range query responses from CouchDB 76 type RangeQueryResponse struct { 77 TotalRows int32 `json:"total_rows"` 78 Offset int32 `json:"offset"` 79 Rows []struct { 80 ID string `json:"id"` 81 Key string `json:"key"` 82 Value struct { 83 Rev string `json:"rev"` 84 } `json:"value"` 85 Doc json.RawMessage `json:"doc"` 86 } `json:"rows"` 87 } 88 89 //QueryResponse is used for processing REST query responses from CouchDB 90 type QueryResponse struct { 91 Warning string `json:"warning"` 92 Docs []json.RawMessage `json:"docs"` 93 Bookmark string `json:"bookmark"` 94 } 95 96 // DocMetadata is used for capturing CouchDB document header info, 97 // used to capture id, version, rev and attachments returned in the query from CouchDB 98 type DocMetadata struct { 99 ID string `json:"_id"` 100 Rev string `json:"_rev"` 101 Version string `json:"~version"` 102 AttachmentsInfo map[string]*AttachmentInfo `json:"_attachments"` 103 } 104 105 //DocID is a minimal structure for capturing the ID from a query result 106 type DocID struct { 107 ID string `json:"_id"` 108 } 109 110 //QueryResult is used for returning query results from CouchDB 111 type QueryResult struct { 112 ID string 113 Value []byte 114 Attachments []*AttachmentInfo 115 } 116 117 // Config is a structure used to configure a CouchInstance. 118 type Config struct { 119 // Address is the hostname:port of the CouchDB database instance. 120 Address string 121 // Username is the username used to authenticate with CouchDB. This username 122 // must have read and write access permissions. 123 Username string 124 // Password is the password for Username. 125 Password string 126 // MaxRetries is the maximum number of times to retry CouchDB operations on 127 // failure. 128 MaxRetries int 129 // MaxRetriesOnStartup is the maximum number of times to retry CouchDB operations on 130 // failure when initializing the ledger. 131 MaxRetriesOnStartup int 132 // RequestTimeout is the timeout used for CouchDB operations. 133 RequestTimeout time.Duration 134 // InternalQueryLimit is the maximum number of records to return internally 135 // when querying CouchDB. 136 InternalQueryLimit int 137 // MaxBatchUpdateSize is the maximum number of records to included in CouchDB 138 // bulk update operations. 139 MaxBatchUpdateSize int 140 // WarmIndexesAfterNBlocks is the number of blocks after which to warm any 141 // CouchDB indexes. 142 WarmIndexesAfterNBlocks int 143 // CreateGlobalChangesDB determines whether or not to create the "_global_changes" 144 // system database. 145 CreateGlobalChangesDB bool 146 // RedoLogPath is the directory where the CouchDB redo log files are stored. 147 RedoLogPath string 148 // UserCacheSizeMBs denotes the user specified maximum mega bytes (MB) to be allocated 149 // for the user state cache (i.e., all chaincodes deployed by the user). Note that 150 // UserCacheSizeMBs needs to be a multiple of 32 MB. If it is not a multiple of 32 MB, 151 // the peer would round the size to the next multiple of 32 MB. 152 UserCacheSizeMBs int 153 } 154 155 //CouchInstance represents a CouchDB instance 156 type CouchInstance struct { 157 conf *Config 158 client *http.Client // a client to connect to this instance 159 stats *stats 160 } 161 162 //CouchDatabase represents a database within a CouchDB instance 163 type CouchDatabase struct { 164 CouchInstance *CouchInstance //connection configuration 165 DBName string 166 IndexWarmCounter int 167 } 168 169 //DBReturn contains an error reported by CouchDB 170 type DBReturn struct { 171 StatusCode int `json:"status_code"` 172 Error string `json:"error"` 173 Reason string `json:"reason"` 174 } 175 176 //CreateIndexResponse contains an the index creation response from CouchDB 177 type CreateIndexResponse struct { 178 Result string `json:"result"` 179 ID string `json:"id"` 180 Name string `json:"name"` 181 } 182 183 //AttachmentInfo contains the definition for an attached file for couchdb 184 type AttachmentInfo struct { 185 Name string 186 ContentType string `json:"content_type"` 187 Length uint64 188 AttachmentBytes []byte `json:"data"` 189 } 190 191 //FileDetails defines the structure needed to send an attachment to couchdb 192 type FileDetails struct { 193 Follows bool `json:"follows"` 194 ContentType string `json:"content_type"` 195 Length int `json:"length"` 196 } 197 198 //CouchDoc defines the structure for a JSON document value 199 type CouchDoc struct { 200 JSONValue []byte 201 Attachments []*AttachmentInfo 202 } 203 204 //BatchRetrieveDocMetadataResponse is used for processing REST batch responses from CouchDB 205 type BatchRetrieveDocMetadataResponse struct { 206 Rows []struct { 207 ID string `json:"id"` 208 DocMetadata struct { 209 ID string `json:"_id"` 210 Rev string `json:"_rev"` 211 Version string `json:"~version"` 212 } `json:"doc"` 213 } `json:"rows"` 214 } 215 216 //BatchUpdateResponse defines a structure for batch update response 217 type BatchUpdateResponse struct { 218 ID string `json:"id"` 219 Error string `json:"error"` 220 Reason string `json:"reason"` 221 Ok bool `json:"ok"` 222 Rev string `json:"rev"` 223 } 224 225 //Base64Attachment contains the definition for an attached file for couchdb 226 type Base64Attachment struct { 227 ContentType string `json:"content_type"` 228 AttachmentData string `json:"data"` 229 } 230 231 //IndexResult contains the definition for a couchdb index 232 type IndexResult struct { 233 DesignDocument string `json:"designdoc"` 234 Name string `json:"name"` 235 Definition string `json:"definition"` 236 } 237 238 //DatabaseSecurity contains the definition for CouchDB database security 239 type DatabaseSecurity struct { 240 Admins struct { 241 Names []string `json:"names"` 242 Roles []string `json:"roles"` 243 } `json:"admins"` 244 Members struct { 245 Names []string `json:"names"` 246 Roles []string `json:"roles"` 247 } `json:"members"` 248 } 249 250 // closeResponseBody discards the body and then closes it to enable returning it to 251 // connection pool 252 func closeResponseBody(resp *http.Response) { 253 if resp != nil { 254 io.Copy(ioutil.Discard, resp.Body) // discard whatever is remaining of body 255 resp.Body.Close() 256 } 257 } 258 259 //CreateDatabaseIfNotExist method provides function to create database 260 func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() error { 261 262 logger.Debugf("[%s] Entering CreateDatabaseIfNotExist()", dbclient.DBName) 263 264 dbInfo, couchDBReturn, err := dbclient.GetDatabaseInfo() 265 if err != nil { 266 if couchDBReturn == nil || couchDBReturn.StatusCode != 404 { 267 return err 268 } 269 } 270 271 //If the dbInfo returns populated and status code is 200, then the database exists 272 if dbInfo != nil && couchDBReturn.StatusCode == 200 { 273 274 //Apply database security if needed 275 errSecurity := dbclient.applyDatabasePermissions() 276 if errSecurity != nil { 277 return errSecurity 278 } 279 280 logger.Debugf("[%s] Database already exists", dbclient.DBName) 281 282 logger.Debugf("[%s] Exiting CreateDatabaseIfNotExist()", dbclient.DBName) 283 284 return nil 285 } 286 287 logger.Debugf("[%s] Database does not exist.", dbclient.DBName) 288 289 connectURL, err := url.Parse(dbclient.CouchInstance.URL()) 290 if err != nil { 291 logger.Errorf("URL parse error: %s", err) 292 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 293 } 294 295 //get the number of retries 296 maxRetries := dbclient.CouchInstance.conf.MaxRetries 297 298 //process the URL with a PUT, creates the database 299 resp, _, err := dbclient.handleRequest(http.MethodPut, "CreateDatabaseIfNotExist", connectURL, nil, "", "", maxRetries, true, nil) 300 301 if err != nil { 302 303 // Check to see if the database exists 304 // Even though handleRequest() returned an error, the 305 // database may have been created and a false error 306 // returned due to a timeout or race condition. 307 // Do a final check to see if the database really got created. 308 dbInfo, couchDBReturn, errDbInfo := dbclient.GetDatabaseInfo() 309 //If there is no error, then the database exists, return without an error 310 if errDbInfo == nil && dbInfo != nil && couchDBReturn.StatusCode == 200 { 311 312 errSecurity := dbclient.applyDatabasePermissions() 313 if errSecurity != nil { 314 return errSecurity 315 } 316 317 logger.Infof("[%s] Created state database", dbclient.DBName) 318 logger.Debugf("[%s] Exiting CreateDatabaseIfNotExist()", dbclient.DBName) 319 return nil 320 } 321 322 return err 323 324 } 325 defer closeResponseBody(resp) 326 327 errSecurity := dbclient.applyDatabasePermissions() 328 if errSecurity != nil { 329 return errSecurity 330 } 331 332 logger.Infof("Created state database %s", dbclient.DBName) 333 334 logger.Debugf("[%s] Exiting CreateDatabaseIfNotExist()", dbclient.DBName) 335 336 return nil 337 338 } 339 340 //applyDatabaseSecurity 341 func (dbclient *CouchDatabase) applyDatabasePermissions() error { 342 343 //If the username and password are not set, then skip applying permissions 344 if dbclient.CouchInstance.conf.Username == "" && dbclient.CouchInstance.conf.Password == "" { 345 return nil 346 } 347 348 securityPermissions := &DatabaseSecurity{} 349 350 securityPermissions.Admins.Names = append(securityPermissions.Admins.Names, dbclient.CouchInstance.conf.Username) 351 securityPermissions.Members.Names = append(securityPermissions.Members.Names, dbclient.CouchInstance.conf.Username) 352 353 err := dbclient.ApplyDatabaseSecurity(securityPermissions) 354 if err != nil { 355 return err 356 } 357 358 return nil 359 } 360 361 //GetDatabaseInfo method provides function to retrieve database information 362 func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { 363 364 connectURL, err := url.Parse(dbclient.CouchInstance.URL()) 365 if err != nil { 366 logger.Errorf("URL parse error: %s", err) 367 return nil, nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 368 } 369 370 //get the number of retries 371 maxRetries := dbclient.CouchInstance.conf.MaxRetries 372 373 resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseInfo", connectURL, nil, "", "", maxRetries, true, nil) 374 if err != nil { 375 return nil, couchDBReturn, err 376 } 377 defer closeResponseBody(resp) 378 379 dbResponse := &DBInfo{} 380 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 381 if decodeErr != nil { 382 return nil, nil, errors.Wrap(decodeErr, "error decoding response body") 383 } 384 385 // trace the database info response 386 logger.Debugw("GetDatabaseInfo()", "dbResponseJSON", dbResponse) 387 388 return dbResponse, couchDBReturn, nil 389 390 } 391 392 //VerifyCouchConfig method provides function to verify the connection information 393 func (couchInstance *CouchInstance) VerifyCouchConfig() (*ConnectionInfo, *DBReturn, error) { 394 395 logger.Debugf("Entering VerifyCouchConfig()") 396 defer logger.Debugf("Exiting VerifyCouchConfig()") 397 398 connectURL, err := url.Parse(couchInstance.URL()) 399 if err != nil { 400 logger.Errorf("URL parse error: %s", err) 401 return nil, nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.URL()) 402 } 403 connectURL.Path = "/" 404 405 //get the number of retries for startup 406 maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup 407 408 resp, couchDBReturn, err := couchInstance.handleRequest(context.Background(), http.MethodGet, "", "VerifyCouchConfig", connectURL, nil, 409 "", "", maxRetriesOnStartup, true, nil) 410 411 if err != nil { 412 return nil, couchDBReturn, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port") 413 } 414 defer closeResponseBody(resp) 415 416 dbResponse := &ConnectionInfo{} 417 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 418 if decodeErr != nil { 419 return nil, nil, errors.Wrap(decodeErr, "error decoding response body") 420 } 421 422 // trace the database info response 423 logger.Debugw("VerifyConnection()", "dbResponseJSON", dbResponse) 424 425 //check to see if the system databases exist 426 //Verifying the existence of the system database accomplishes two steps 427 //1. Ensures the system databases are created 428 //2. Verifies the username password provided in the CouchDB config are valid for system admin 429 err = CreateSystemDatabasesIfNotExist(couchInstance) 430 if err != nil { 431 logger.Errorf("Unable to connect to CouchDB, error: %s. Check the admin username and password.", err) 432 return nil, nil, errors.WithMessage(err, "unable to connect to CouchDB. Check the admin username and password") 433 } 434 435 return dbResponse, couchDBReturn, nil 436 } 437 438 // IsEmpty returns false if couchInstance contains any databases 439 // (except couchdb system databases and any database name supplied in the parameter 'databasesToIgnore') 440 func (couchInstance *CouchInstance) IsEmpty(databasesToIgnore []string) (bool, error) { 441 toIgnore := map[string]bool{} 442 for _, s := range databasesToIgnore { 443 toIgnore[s] = true 444 } 445 applicationDBNames, err := couchInstance.RetrieveApplicationDBNames() 446 if err != nil { 447 return false, err 448 } 449 for _, dbName := range applicationDBNames { 450 if !toIgnore[dbName] { 451 return false, nil 452 } 453 } 454 return true, nil 455 } 456 457 // RetrieveApplicationDBNames returns all the applicaiton database names in the couch instance 458 func (couchInstance *CouchInstance) RetrieveApplicationDBNames() ([]string, error) { 459 connectURL, err := url.Parse(couchInstance.URL()) 460 if err != nil { 461 logger.Errorf("URL parse error: %s", err) 462 return nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.URL()) 463 } 464 connectURL.Path = "/_all_dbs" 465 maxRetries := couchInstance.conf.MaxRetries 466 resp, _, err := couchInstance.handleRequest( 467 context.Background(), 468 http.MethodGet, 469 "", 470 "IsEmpty", 471 connectURL, 472 nil, 473 "", 474 "", 475 maxRetries, 476 true, 477 nil, 478 ) 479 480 if err != nil { 481 return nil, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port") 482 } 483 484 var dbNames []string 485 defer closeResponseBody(resp) 486 if err := json.NewDecoder(resp.Body).Decode(&dbNames); err != nil { 487 return nil, errors.Wrap(err, "error decoding response body") 488 } 489 logger.Debugf("dbNames = %s", dbNames) 490 applicationsDBNames := []string{} 491 for _, d := range dbNames { 492 if !isCouchSystemDBName(d) { 493 applicationsDBNames = append(applicationsDBNames, d) 494 } 495 } 496 return applicationsDBNames, nil 497 } 498 499 func isCouchSystemDBName(name string) bool { 500 return strings.HasPrefix(name, "_") 501 } 502 503 // HealthCheck checks if the peer is able to communicate with CouchDB 504 func (couchInstance *CouchInstance) HealthCheck(ctx context.Context) error { 505 connectURL, err := url.Parse(couchInstance.URL()) 506 if err != nil { 507 logger.Errorf("URL parse error: %s", err) 508 return errors.Wrapf(err, "error parsing CouchDB URL: %s", couchInstance.URL()) 509 } 510 _, _, err = couchInstance.handleRequest(ctx, http.MethodHead, "", "HealthCheck", connectURL, nil, "", "", 0, true, nil) 511 if err != nil { 512 return fmt.Errorf("failed to connect to couch db [%s]", err) 513 } 514 return nil 515 } 516 517 // InternalQueryLimit returns the maximum number of records to return internally 518 // when querying CouchDB. 519 func (couchInstance *CouchInstance) InternalQueryLimit() int32 { 520 return int32(couchInstance.conf.InternalQueryLimit) 521 } 522 523 // MaxBatchUpdateSize returns the maximum number of records to include in a 524 // bulk update operation. 525 func (couchInstance *CouchInstance) MaxBatchUpdateSize() int { 526 return couchInstance.conf.MaxBatchUpdateSize 527 } 528 529 // URL returns the URL for the CouchDB instance. 530 func (couchInstance *CouchInstance) URL() string { 531 URL := &url.URL{ 532 Host: couchInstance.conf.Address, 533 Scheme: "http", 534 } 535 return URL.String() 536 } 537 538 //DropDatabase provides method to drop an existing database 539 func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) { 540 dbName := dbclient.DBName 541 542 logger.Debugf("[%s] Entering DropDatabase()", dbName) 543 544 connectURL, err := url.Parse(dbclient.CouchInstance.URL()) 545 if err != nil { 546 logger.Errorf("URL parse error: %s", err) 547 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 548 } 549 550 //get the number of retries 551 maxRetries := dbclient.CouchInstance.conf.MaxRetries 552 553 resp, _, err := dbclient.handleRequest(http.MethodDelete, "DropDatabase", connectURL, nil, "", "", maxRetries, true, nil) 554 if err != nil { 555 return nil, err 556 } 557 defer closeResponseBody(resp) 558 559 dbResponse := &DBOperationResponse{} 560 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 561 if decodeErr != nil { 562 return nil, errors.Wrap(decodeErr, "error decoding response body") 563 } 564 565 if dbResponse.Ok == true { 566 logger.Debugf("[%s] Dropped database", dbclient.DBName) 567 } 568 569 logger.Debugf("[%s] Exiting DropDatabase()", dbclient.DBName) 570 571 if dbResponse.Ok == true { 572 573 return dbResponse, nil 574 575 } 576 577 return dbResponse, errors.New("error dropping database") 578 579 } 580 581 // EnsureFullCommit calls _ensure_full_commit for explicit fsync 582 func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) { 583 dbName := dbclient.DBName 584 585 logger.Debugf("[%s] Entering EnsureFullCommit()", dbName) 586 587 connectURL, err := url.Parse(dbclient.CouchInstance.URL()) 588 if err != nil { 589 logger.Errorf("URL parse error: %s", err) 590 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 591 } 592 593 //get the number of retries 594 maxRetries := dbclient.CouchInstance.conf.MaxRetries 595 596 resp, _, err := dbclient.handleRequest(http.MethodPost, "EnsureFullCommit", connectURL, nil, "", "", maxRetries, true, nil, "_ensure_full_commit") 597 if err != nil { 598 logger.Errorf("Failed to invoke couchdb _ensure_full_commit. Error: %+v", err) 599 return nil, err 600 } 601 defer closeResponseBody(resp) 602 603 dbResponse := &DBOperationResponse{} 604 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 605 if decodeErr != nil { 606 return nil, errors.Wrap(decodeErr, "error decoding response body") 607 } 608 609 // check if we should warm indexes 610 if dbclient.CouchInstance.conf.WarmIndexesAfterNBlocks > 0 { 611 // check to see if the number of blocks committed exceeds the threshold for index warming 612 if dbclient.IndexWarmCounter >= dbclient.CouchInstance.conf.WarmIndexesAfterNBlocks { 613 // use a go routine to launch WarmIndexAllIndexes() 614 go dbclient.runWarmIndexAllIndexes() 615 dbclient.IndexWarmCounter = 0 616 } 617 dbclient.IndexWarmCounter++ 618 } 619 620 logger.Debugf("[%s] Exiting EnsureFullCommit()", dbclient.DBName) 621 622 if dbResponse.Ok == true { 623 624 return dbResponse, nil 625 626 } 627 628 return dbResponse, errors.New("error syncing database") 629 } 630 631 //SaveDoc method provides a function to save a document, id and byte array 632 func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc) (string, error) { 633 dbName := dbclient.DBName 634 635 logger.Debugf("[%s] Entering SaveDoc() id=[%s]", dbName, id) 636 637 if !utf8.ValidString(id) { 638 return "", errors.Errorf("doc id [%x] not a valid utf8 string", id) 639 } 640 641 saveURL, err := url.Parse(dbclient.CouchInstance.URL()) 642 if err != nil { 643 logger.Errorf("URL parse error: %s", err) 644 return "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 645 } 646 647 //Set up a buffer for the data to be pushed to couchdb 648 data := []byte{} 649 650 //Set up a default boundary for use by multipart if sending attachments 651 defaultBoundary := "" 652 653 //Create a flag for shared connections. This is set to false for zero length attachments 654 keepConnectionOpen := true 655 656 //check to see if attachments is nil, if so, then this is a JSON only 657 if couchDoc.Attachments == nil { 658 659 //Test to see if this is a valid JSON 660 if IsJSON(string(couchDoc.JSONValue)) != true { 661 return "", errors.New("JSON format is not valid") 662 } 663 664 // if there are no attachments, then use the bytes passed in as the JSON 665 data = couchDoc.JSONValue 666 667 } else { // there are attachments 668 669 //attachments are included, create the multipart definition 670 multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary) 671 if err3 != nil { 672 return "", err3 673 } 674 675 //If there is a zero length attachment, do not keep the connection open 676 for _, attach := range couchDoc.Attachments { 677 if attach.Length < 1 { 678 keepConnectionOpen = false 679 } 680 } 681 682 //Set the data buffer to the data from the create multi-part data 683 data = multipartData.Bytes() 684 685 //Set the default boundary to the value generated in the multipart creation 686 defaultBoundary = multipartBoundary 687 688 } 689 690 //get the number of retries 691 maxRetries := dbclient.CouchInstance.conf.MaxRetries 692 693 //handle the request for saving document with a retry if there is a revision conflict 694 resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut, dbName, "SaveDoc", saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen, nil) 695 696 if err != nil { 697 return "", err 698 } 699 defer closeResponseBody(resp) 700 701 //get the revision and return 702 revision, err := getRevisionHeader(resp) 703 if err != nil { 704 return "", err 705 } 706 707 logger.Debugf("[%s] Exiting SaveDoc()", dbclient.DBName) 708 709 return revision, nil 710 711 } 712 713 //getDocumentRevision will return the revision if the document exists, otherwise it will return "" 714 func (dbclient *CouchDatabase) getDocumentRevision(id string) string { 715 716 var rev = "" 717 718 //See if the document already exists, we need the rev for saves and deletes 719 _, revdoc, err := dbclient.ReadDoc(id) 720 if err == nil { 721 //set the revision to the rev returned from the document read 722 rev = revdoc 723 } 724 return rev 725 } 726 727 func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) { 728 729 //Create a buffer for writing the result 730 writeBuffer := new(bytes.Buffer) 731 732 // read the attachment and save as an attachment 733 writer := multipart.NewWriter(writeBuffer) 734 735 //retrieve the boundary for the multipart 736 defaultBoundary = writer.Boundary() 737 738 fileAttachments := map[string]FileDetails{} 739 740 for _, attachment := range couchDoc.Attachments { 741 fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)} 742 } 743 744 attachmentJSONMap := map[string]interface{}{ 745 "_attachments": fileAttachments} 746 747 //Add any data uploaded with the files 748 if couchDoc.JSONValue != nil { 749 750 //create a generic map 751 genericMap := make(map[string]interface{}) 752 753 //unmarshal the data into the generic map 754 decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.JSONValue)) 755 decoder.UseNumber() 756 decodeErr := decoder.Decode(&genericMap) 757 if decodeErr != nil { 758 return *writeBuffer, "", errors.Wrap(decodeErr, "error decoding json data") 759 } 760 761 //add all key/values to the attachmentJSONMap 762 for jsonKey, jsonValue := range genericMap { 763 attachmentJSONMap[jsonKey] = jsonValue 764 } 765 766 } 767 768 filesForUpload, err := json.Marshal(attachmentJSONMap) 769 if err != nil { 770 return *writeBuffer, "", errors.Wrap(err, "error marshalling json data") 771 } 772 773 logger.Debugf(string(filesForUpload)) 774 775 //create the header for the JSON 776 header := make(textproto.MIMEHeader) 777 header.Set("Content-Type", "application/json") 778 779 part, err := writer.CreatePart(header) 780 if err != nil { 781 return *writeBuffer, defaultBoundary, errors.Wrap(err, "error creating multipart") 782 } 783 784 part.Write(filesForUpload) 785 786 for _, attachment := range couchDoc.Attachments { 787 788 header := make(textproto.MIMEHeader) 789 part, err2 := writer.CreatePart(header) 790 if err2 != nil { 791 return *writeBuffer, defaultBoundary, errors.Wrap(err2, "error creating multipart") 792 } 793 part.Write(attachment.AttachmentBytes) 794 795 } 796 797 err = writer.Close() 798 if err != nil { 799 return *writeBuffer, defaultBoundary, errors.Wrap(err, "error closing multipart writer") 800 } 801 802 return *writeBuffer, defaultBoundary, nil 803 804 } 805 806 func getRevisionHeader(resp *http.Response) (string, error) { 807 808 if resp == nil { 809 return "", errors.New("no response received from CouchDB") 810 } 811 812 revision := resp.Header.Get("Etag") 813 814 if revision == "" { 815 return "", errors.New("no revision tag detected") 816 } 817 818 reg := regexp.MustCompile(`"([^"]*)"`) 819 revisionNoQuotes := reg.ReplaceAllString(revision, "${1}") 820 return revisionNoQuotes, nil 821 822 } 823 824 //ReadDoc method provides function to retrieve a document and its revision 825 //from the database by id 826 func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) { 827 var couchDoc CouchDoc 828 attachments := []*AttachmentInfo{} 829 dbName := dbclient.DBName 830 831 logger.Debugf("[%s] Entering ReadDoc() id=[%s]", dbName, id) 832 if !utf8.ValidString(id) { 833 return nil, "", errors.Errorf("doc id [%x] not a valid utf8 string", id) 834 } 835 836 readURL, err := url.Parse(dbclient.CouchInstance.URL()) 837 if err != nil { 838 logger.Errorf("URL parse error: %s", err) 839 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 840 } 841 842 query := readURL.Query() 843 query.Add("attachments", "true") 844 845 //get the number of retries 846 maxRetries := dbclient.CouchInstance.conf.MaxRetries 847 848 resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "ReadDoc", readURL, nil, "", "", maxRetries, true, &query, id) 849 if err != nil { 850 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 851 logger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.DBName) 852 // non-existent document should return nil value instead of a 404 error 853 // for details see https://github.com/hyperledger-archives/fabric/issues/936 854 return nil, "", nil 855 } 856 logger.Debugf("[%s] couchDBReturn=%v\n", dbclient.DBName, couchDBReturn) 857 return nil, "", err 858 } 859 defer closeResponseBody(resp) 860 861 //Get the media type from the Content-Type header 862 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 863 if err != nil { 864 log.Fatal(err) 865 } 866 867 //Get the revision from header 868 revision, err := getRevisionHeader(resp) 869 if err != nil { 870 return nil, "", err 871 } 872 873 //check to see if the is multipart, handle as attachment if multipart is detected 874 if strings.HasPrefix(mediaType, "multipart/") { 875 //Set up the multipart reader based on the boundary 876 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 877 for { 878 p, err := multipartReader.NextPart() 879 if err == io.EOF { 880 break // processed all parts 881 } 882 if err != nil { 883 return nil, "", errors.Wrap(err, "error reading next multipart") 884 } 885 886 defer p.Close() 887 888 logger.Debugf("[%s] part header=%s", dbclient.DBName, p.Header) 889 switch p.Header.Get("Content-Type") { 890 case "application/json": 891 partdata, err := ioutil.ReadAll(p) 892 if err != nil { 893 return nil, "", errors.Wrap(err, "error reading multipart data") 894 } 895 couchDoc.JSONValue = partdata 896 default: 897 898 //Create an attachment structure and load it 899 attachment := &AttachmentInfo{} 900 attachment.ContentType = p.Header.Get("Content-Type") 901 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 902 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 903 switch p.Header.Get("Content-Encoding") { 904 case "gzip": //See if the part is gzip encoded 905 906 var respBody []byte 907 908 gr, err := gzip.NewReader(p) 909 if err != nil { 910 return nil, "", errors.Wrap(err, "error creating gzip reader") 911 } 912 respBody, err = ioutil.ReadAll(gr) 913 if err != nil { 914 return nil, "", errors.Wrap(err, "error reading gzip data") 915 } 916 917 logger.Debugf("[%s] Retrieved attachment data", dbclient.DBName) 918 attachment.AttachmentBytes = respBody 919 attachment.Length = uint64(len(attachment.AttachmentBytes)) 920 attachment.Name = p.FileName() 921 attachments = append(attachments, attachment) 922 923 default: 924 925 //retrieve the data, this is not gzip 926 partdata, err := ioutil.ReadAll(p) 927 if err != nil { 928 return nil, "", errors.Wrap(err, "error reading multipart data") 929 } 930 logger.Debugf("[%s] Retrieved attachment data", dbclient.DBName) 931 attachment.AttachmentBytes = partdata 932 attachment.Length = uint64(len(attachment.AttachmentBytes)) 933 attachment.Name = p.FileName() 934 attachments = append(attachments, attachment) 935 936 } // end content-encoding switch 937 } // end if attachment 938 } // end content-type switch 939 } // for all multiparts 940 941 couchDoc.Attachments = attachments 942 943 return &couchDoc, revision, nil 944 } 945 946 //handle as JSON document 947 couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body) 948 if err != nil { 949 return nil, "", errors.Wrap(err, "error reading response body") 950 } 951 952 logger.Debugf("[%s] Exiting ReadDoc()", dbclient.DBName) 953 return &couchDoc, revision, nil 954 } 955 956 //ReadDocRange method provides function to a range of documents based on the start and end keys 957 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 958 //This function provides a limit option to specify the max number of entries and is supplied by config. 959 //Skip is reserved for possible future future use. 960 func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit int32) ([]*QueryResult, string, error) { 961 dbName := dbclient.DBName 962 logger.Debugf("[%s] Entering ReadDocRange() startKey=%s, endKey=%s", dbName, startKey, endKey) 963 964 var results []*QueryResult 965 966 rangeURL, err := url.Parse(dbclient.CouchInstance.URL()) 967 if err != nil { 968 logger.Errorf("URL parse error: %s", err) 969 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 970 } 971 972 queryParms := rangeURL.Query() 973 //Increment the limit by 1 to see if there are more qualifying records 974 queryParms.Set("limit", strconv.FormatInt(int64(limit+1), 10)) 975 queryParms.Add("include_docs", "true") 976 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 977 queryParms.Add("attachments", "true") // get the attachments as well 978 979 //Append the startKey if provided 980 if startKey != "" { 981 if startKey, err = encodeForJSON(startKey); err != nil { 982 return nil, "", err 983 } 984 queryParms.Add("startkey", "\""+startKey+"\"") 985 } 986 987 //Append the endKey if provided 988 if endKey != "" { 989 var err error 990 if endKey, err = encodeForJSON(endKey); err != nil { 991 return nil, "", err 992 } 993 queryParms.Add("endkey", "\""+endKey+"\"") 994 } 995 996 //get the number of retries 997 maxRetries := dbclient.CouchInstance.conf.MaxRetries 998 999 resp, _, err := dbclient.handleRequest(http.MethodGet, "RangeDocRange", rangeURL, nil, "", "", maxRetries, true, &queryParms, "_all_docs") 1000 if err != nil { 1001 return nil, "", err 1002 } 1003 defer closeResponseBody(resp) 1004 1005 if logger.IsEnabledFor(zapcore.DebugLevel) { 1006 dump, err2 := httputil.DumpResponse(resp, true) 1007 if err2 != nil { 1008 log.Fatal(err2) 1009 } 1010 logger.Debugf("[%s] %s", dbclient.DBName, dump) 1011 } 1012 1013 //handle as JSON document 1014 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1015 if err != nil { 1016 return nil, "", errors.Wrap(err, "error reading response body") 1017 } 1018 1019 var jsonResponse = &RangeQueryResponse{} 1020 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1021 if err2 != nil { 1022 return nil, "", errors.Wrap(err2, "error unmarshalling json data") 1023 } 1024 1025 //if an additional record is found, then reduce the count by 1 1026 //and populate the nextStartKey 1027 if jsonResponse.TotalRows > limit { 1028 jsonResponse.TotalRows = limit 1029 } 1030 1031 logger.Debugf("[%s] Total Rows: %d", dbclient.DBName, jsonResponse.TotalRows) 1032 1033 //Use the next endKey as the starting default for the nextStartKey 1034 nextStartKey := endKey 1035 1036 for index, row := range jsonResponse.Rows { 1037 1038 var docMetadata = &DocMetadata{} 1039 err3 := json.Unmarshal(row.Doc, &docMetadata) 1040 if err3 != nil { 1041 return nil, "", errors.Wrap(err3, "error unmarshalling json data") 1042 } 1043 1044 //if there is an extra row for the nextStartKey, then do not add the row to the result set 1045 //and populate the nextStartKey variable 1046 if int32(index) >= jsonResponse.TotalRows { 1047 nextStartKey = docMetadata.ID 1048 continue 1049 } 1050 1051 if docMetadata.AttachmentsInfo != nil { 1052 1053 logger.Debugf("[%s] Adding JSON document and attachments for id: %s", dbclient.DBName, docMetadata.ID) 1054 1055 attachments := []*AttachmentInfo{} 1056 for attachmentName, attachment := range docMetadata.AttachmentsInfo { 1057 attachment.Name = attachmentName 1058 1059 attachments = append(attachments, attachment) 1060 } 1061 1062 var addDocument = &QueryResult{docMetadata.ID, row.Doc, attachments} 1063 results = append(results, addDocument) 1064 1065 } else { 1066 1067 logger.Debugf("[%s] Adding json docment for id: %s", dbclient.DBName, docMetadata.ID) 1068 1069 var addDocument = &QueryResult{docMetadata.ID, row.Doc, nil} 1070 results = append(results, addDocument) 1071 1072 } 1073 1074 } 1075 1076 logger.Debugf("[%s] Exiting ReadDocRange()", dbclient.DBName) 1077 1078 return results, nextStartKey, nil 1079 1080 } 1081 1082 //DeleteDoc method provides function to delete a document from the database by id 1083 func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { 1084 dbName := dbclient.DBName 1085 1086 logger.Debugf("[%s] Entering DeleteDoc() id=%s", dbName, id) 1087 1088 deleteURL, err := url.Parse(dbclient.CouchInstance.URL()) 1089 if err != nil { 1090 logger.Errorf("URL parse error: %s", err) 1091 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1092 } 1093 1094 //get the number of retries 1095 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1096 1097 //handle the request for saving document with a retry if there is a revision conflict 1098 resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete, dbName, "DeleteDoc", 1099 deleteURL, nil, "", "", maxRetries, true, nil) 1100 1101 if err != nil { 1102 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 1103 logger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.DBName) 1104 // non-existent document should return nil value instead of a 404 error 1105 // for details see https://github.com/hyperledger-archives/fabric/issues/936 1106 return nil 1107 } 1108 return err 1109 } 1110 defer closeResponseBody(resp) 1111 1112 logger.Debugf("[%s] Exiting DeleteDoc()", dbclient.DBName) 1113 1114 return nil 1115 1116 } 1117 1118 //QueryDocuments method provides function for processing a query 1119 func (dbclient *CouchDatabase) QueryDocuments(query string) ([]*QueryResult, string, error) { 1120 dbName := dbclient.DBName 1121 1122 logger.Debugf("[%s] Entering QueryDocuments() query=%s", dbName, query) 1123 1124 var results []*QueryResult 1125 1126 queryURL, err := url.Parse(dbclient.CouchInstance.URL()) 1127 if err != nil { 1128 logger.Errorf("URL parse error: %s", err) 1129 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1130 } 1131 1132 //get the number of retries 1133 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1134 1135 resp, _, err := dbclient.handleRequest(http.MethodPost, "QueryDocuments", queryURL, []byte(query), "", "", maxRetries, true, nil, "_find") 1136 if err != nil { 1137 return nil, "", err 1138 } 1139 defer closeResponseBody(resp) 1140 1141 if logger.IsEnabledFor(zapcore.DebugLevel) { 1142 dump, err2 := httputil.DumpResponse(resp, true) 1143 if err2 != nil { 1144 log.Fatal(err2) 1145 } 1146 logger.Debugf("[%s] %s", dbclient.DBName, dump) 1147 } 1148 1149 //handle as JSON document 1150 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1151 if err != nil { 1152 return nil, "", errors.Wrap(err, "error reading response body") 1153 } 1154 1155 var jsonResponse = &QueryResponse{} 1156 1157 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1158 if err2 != nil { 1159 return nil, "", errors.Wrap(err2, "error unmarshalling json data") 1160 } 1161 1162 if jsonResponse.Warning != "" { 1163 logger.Warnf("The query [%s] caused the following warning: [%s]", query, jsonResponse.Warning) 1164 } 1165 1166 for _, row := range jsonResponse.Docs { 1167 1168 var docMetadata = &DocMetadata{} 1169 err3 := json.Unmarshal(row, &docMetadata) 1170 if err3 != nil { 1171 return nil, "", errors.Wrap(err3, "error unmarshalling json data") 1172 } 1173 1174 // JSON Query results never have attachments 1175 // The If block below will never be executed 1176 if docMetadata.AttachmentsInfo != nil { 1177 1178 logger.Debugf("[%s] Adding JSON docment and attachments for id: %s", dbclient.DBName, docMetadata.ID) 1179 1180 couchDoc, _, err := dbclient.ReadDoc(docMetadata.ID) 1181 if err != nil { 1182 return nil, "", err 1183 } 1184 var addDocument = &QueryResult{ID: docMetadata.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments} 1185 results = append(results, addDocument) 1186 1187 } else { 1188 logger.Debugf("[%s] Adding json docment for id: %s", dbclient.DBName, docMetadata.ID) 1189 var addDocument = &QueryResult{ID: docMetadata.ID, Value: row, Attachments: nil} 1190 1191 results = append(results, addDocument) 1192 1193 } 1194 } 1195 1196 logger.Debugf("[%s] Exiting QueryDocuments()", dbclient.DBName) 1197 1198 return results, jsonResponse.Bookmark, nil 1199 1200 } 1201 1202 // ListIndex method lists the defined indexes for a database 1203 func (dbclient *CouchDatabase) ListIndex() ([]*IndexResult, error) { 1204 1205 //IndexDefinition contains the definition for a couchdb index 1206 type indexDefinition struct { 1207 DesignDocument string `json:"ddoc"` 1208 Name string `json:"name"` 1209 Type string `json:"type"` 1210 Definition json.RawMessage `json:"def"` 1211 } 1212 1213 //ListIndexResponse contains the definition for listing couchdb indexes 1214 type listIndexResponse struct { 1215 TotalRows int `json:"total_rows"` 1216 Indexes []indexDefinition `json:"indexes"` 1217 } 1218 1219 dbName := dbclient.DBName 1220 logger.Debug("[%s] Entering ListIndex()", dbName) 1221 1222 indexURL, err := url.Parse(dbclient.CouchInstance.URL()) 1223 if err != nil { 1224 logger.Errorf("URL parse error: %s", err) 1225 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1226 } 1227 1228 //get the number of retries 1229 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1230 1231 resp, _, err := dbclient.handleRequest(http.MethodGet, "ListIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index") 1232 if err != nil { 1233 return nil, err 1234 } 1235 defer closeResponseBody(resp) 1236 1237 //handle as JSON document 1238 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1239 if err != nil { 1240 return nil, errors.Wrap(err, "error reading response body") 1241 } 1242 1243 var jsonResponse = &listIndexResponse{} 1244 1245 err2 := json.Unmarshal(jsonResponseRaw, jsonResponse) 1246 if err2 != nil { 1247 return nil, errors.Wrap(err2, "error unmarshalling json data") 1248 } 1249 1250 var results []*IndexResult 1251 1252 for _, row := range jsonResponse.Indexes { 1253 1254 //if the DesignDocument does not begin with "_design/", then this is a system 1255 //level index and is not meaningful and cannot be edited or deleted 1256 designDoc := row.DesignDocument 1257 s := strings.SplitAfterN(designDoc, "_design/", 2) 1258 if len(s) > 1 { 1259 designDoc = s[1] 1260 1261 //Add the index definition to the results 1262 var addIndexResult = &IndexResult{DesignDocument: designDoc, Name: row.Name, Definition: fmt.Sprintf("%s", row.Definition)} 1263 results = append(results, addIndexResult) 1264 } 1265 1266 } 1267 1268 logger.Debugf("[%s] Exiting ListIndex()", dbclient.DBName) 1269 1270 return results, nil 1271 1272 } 1273 1274 // CreateIndex method provides a function creating an index 1275 func (dbclient *CouchDatabase) CreateIndex(indexdefinition string) (*CreateIndexResponse, error) { 1276 dbName := dbclient.DBName 1277 1278 logger.Debugf("[%s] Entering CreateIndex() indexdefinition=%s", dbName, indexdefinition) 1279 1280 //Test to see if this is a valid JSON 1281 if IsJSON(indexdefinition) != true { 1282 return nil, errors.New("JSON format is not valid") 1283 } 1284 1285 indexURL, err := url.Parse(dbclient.CouchInstance.URL()) 1286 if err != nil { 1287 logger.Errorf("URL parse error: %s", err) 1288 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1289 } 1290 1291 //get the number of retries 1292 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1293 1294 resp, _, err := dbclient.handleRequest(http.MethodPost, "CreateIndex", indexURL, []byte(indexdefinition), "", "", maxRetries, true, nil, "_index") 1295 if err != nil { 1296 return nil, err 1297 } 1298 defer closeResponseBody(resp) 1299 1300 if resp == nil { 1301 return nil, errors.New("invalid response received from CouchDB") 1302 } 1303 1304 //Read the response body 1305 respBody, err := ioutil.ReadAll(resp.Body) 1306 if err != nil { 1307 return nil, errors.Wrap(err, "error reading response body") 1308 } 1309 1310 couchDBReturn := &CreateIndexResponse{} 1311 1312 jsonBytes := []byte(respBody) 1313 1314 //unmarshal the response 1315 err = json.Unmarshal(jsonBytes, &couchDBReturn) 1316 if err != nil { 1317 return nil, errors.Wrap(err, "error unmarshalling json data") 1318 } 1319 1320 if couchDBReturn.Result == "created" { 1321 1322 logger.Infof("Created CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.DBName, couchDBReturn.ID) 1323 1324 return couchDBReturn, nil 1325 1326 } 1327 1328 logger.Infof("Updated CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.DBName, couchDBReturn.ID) 1329 1330 return couchDBReturn, nil 1331 } 1332 1333 // DeleteIndex method provides a function deleting an index 1334 func (dbclient *CouchDatabase) DeleteIndex(designdoc, indexname string) error { 1335 dbName := dbclient.DBName 1336 1337 logger.Debugf("[%s] Entering DeleteIndex() designdoc=%s indexname=%s", dbName, designdoc, indexname) 1338 1339 indexURL, err := url.Parse(dbclient.CouchInstance.URL()) 1340 if err != nil { 1341 logger.Errorf("URL parse error: %s", err) 1342 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1343 } 1344 1345 //get the number of retries 1346 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1347 1348 resp, _, err := dbclient.handleRequest(http.MethodDelete, "DeleteIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index", designdoc, "json", indexname) 1349 if err != nil { 1350 return err 1351 } 1352 defer closeResponseBody(resp) 1353 1354 return nil 1355 1356 } 1357 1358 //WarmIndex method provides a function for warming a single index 1359 func (dbclient *CouchDatabase) WarmIndex(designdoc, indexname string) error { 1360 dbName := dbclient.DBName 1361 1362 logger.Debugf("[%s] Entering WarmIndex() designdoc=%s indexname=%s", dbName, designdoc, indexname) 1363 1364 indexURL, err := url.Parse(dbclient.CouchInstance.URL()) 1365 if err != nil { 1366 logger.Errorf("URL parse error: %s", err) 1367 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1368 } 1369 1370 queryParms := indexURL.Query() 1371 //Query parameter that allows the execution of the URL to return immediately 1372 //The update_after will cause the index update to run after the URL returns 1373 queryParms.Add("stale", "update_after") 1374 1375 //get the number of retries 1376 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1377 1378 resp, _, err := dbclient.handleRequest(http.MethodGet, "WarmIndex", indexURL, nil, "", "", maxRetries, true, &queryParms, "_design", designdoc, "_view", indexname) 1379 if err != nil { 1380 return err 1381 } 1382 defer closeResponseBody(resp) 1383 1384 return nil 1385 1386 } 1387 1388 //runWarmIndexAllIndexes is a wrapper for WarmIndexAllIndexes to catch and report any errors 1389 func (dbclient *CouchDatabase) runWarmIndexAllIndexes() { 1390 1391 err := dbclient.WarmIndexAllIndexes() 1392 if err != nil { 1393 logger.Errorf("Error detected during WarmIndexAllIndexes(): %+v", err) 1394 } 1395 1396 } 1397 1398 //WarmIndexAllIndexes method provides a function for warming all indexes for a database 1399 func (dbclient *CouchDatabase) WarmIndexAllIndexes() error { 1400 1401 logger.Debugf("[%s] Entering WarmIndexAllIndexes()", dbclient.DBName) 1402 1403 //Retrieve all indexes 1404 listResult, err := dbclient.ListIndex() 1405 if err != nil { 1406 return err 1407 } 1408 1409 //For each index definition, execute an index refresh 1410 for _, elem := range listResult { 1411 1412 err := dbclient.WarmIndex(elem.DesignDocument, elem.Name) 1413 if err != nil { 1414 return err 1415 } 1416 1417 } 1418 1419 logger.Debugf("[%s] Exiting WarmIndexAllIndexes()", dbclient.DBName) 1420 1421 return nil 1422 1423 } 1424 1425 //GetDatabaseSecurity method provides function to retrieve the security config for a database 1426 func (dbclient *CouchDatabase) GetDatabaseSecurity() (*DatabaseSecurity, error) { 1427 dbName := dbclient.DBName 1428 1429 logger.Debugf("[%s] Entering GetDatabaseSecurity()", dbName) 1430 1431 securityURL, err := url.Parse(dbclient.CouchInstance.URL()) 1432 if err != nil { 1433 logger.Errorf("URL parse error: %s", err) 1434 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1435 } 1436 1437 //get the number of retries 1438 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1439 1440 resp, _, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseSecurity", securityURL, nil, "", "", maxRetries, true, nil, "_security") 1441 1442 if err != nil { 1443 return nil, err 1444 } 1445 defer closeResponseBody(resp) 1446 1447 //handle as JSON document 1448 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1449 if err != nil { 1450 return nil, errors.Wrap(err, "error reading response body") 1451 } 1452 1453 var jsonResponse = &DatabaseSecurity{} 1454 1455 err2 := json.Unmarshal(jsonResponseRaw, jsonResponse) 1456 if err2 != nil { 1457 return nil, errors.Wrap(err2, "error unmarshalling json data") 1458 } 1459 1460 logger.Debugf("[%s] Exiting GetDatabaseSecurity()", dbclient.DBName) 1461 1462 return jsonResponse, nil 1463 1464 } 1465 1466 //ApplyDatabaseSecurity method provides function to update the security config for a database 1467 func (dbclient *CouchDatabase) ApplyDatabaseSecurity(databaseSecurity *DatabaseSecurity) error { 1468 dbName := dbclient.DBName 1469 1470 logger.Debugf("[%s] Entering ApplyDatabaseSecurity()", dbName) 1471 1472 securityURL, err := url.Parse(dbclient.CouchInstance.URL()) 1473 if err != nil { 1474 logger.Errorf("URL parse error: %s", err) 1475 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1476 } 1477 1478 //Ensure all of the arrays are initialized to empty arrays instead of nil 1479 if databaseSecurity.Admins.Names == nil { 1480 databaseSecurity.Admins.Names = make([]string, 0) 1481 } 1482 if databaseSecurity.Admins.Roles == nil { 1483 databaseSecurity.Admins.Roles = make([]string, 0) 1484 } 1485 if databaseSecurity.Members.Names == nil { 1486 databaseSecurity.Members.Names = make([]string, 0) 1487 } 1488 if databaseSecurity.Members.Roles == nil { 1489 databaseSecurity.Members.Roles = make([]string, 0) 1490 } 1491 1492 //get the number of retries 1493 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1494 1495 databaseSecurityJSON, err := json.Marshal(databaseSecurity) 1496 if err != nil { 1497 return errors.Wrap(err, "error unmarshalling json data") 1498 } 1499 1500 logger.Debugf("[%s] Applying security to database: %s", dbclient.DBName, string(databaseSecurityJSON)) 1501 1502 resp, _, err := dbclient.handleRequest(http.MethodPut, "ApplyDatabaseSecurity", securityURL, databaseSecurityJSON, "", "", maxRetries, true, nil, "_security") 1503 1504 if err != nil { 1505 return err 1506 } 1507 defer closeResponseBody(resp) 1508 1509 logger.Debugf("[%s] Exiting ApplyDatabaseSecurity()", dbclient.DBName) 1510 1511 return nil 1512 1513 } 1514 1515 //BatchRetrieveDocumentMetadata - batch method to retrieve document metadata for a set of keys, 1516 // including ID, couchdb revision number, and ledger version 1517 func (dbclient *CouchDatabase) BatchRetrieveDocumentMetadata(keys []string) ([]*DocMetadata, error) { 1518 1519 logger.Debugf("[%s] Entering BatchRetrieveDocumentMetadata() keys=%s", dbclient.DBName, keys) 1520 1521 batchRetrieveURL, err := url.Parse(dbclient.CouchInstance.URL()) 1522 if err != nil { 1523 logger.Errorf("URL parse error: %s", err) 1524 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1525 } 1526 1527 queryParms := batchRetrieveURL.Query() 1528 1529 // While BatchRetrieveDocumentMetadata() does not return the entire document, 1530 // for reads/writes, we do need to get document so that we can get the ledger version of the key. 1531 // TODO For blind writes we do not need to get the version, therefore when we bulk get 1532 // the revision numbers for the write keys that were not represented in read set 1533 // (the second time BatchRetrieveDocumentMetadata is called during block processing), 1534 // we could set include_docs to false to optimize the response. 1535 queryParms.Add("include_docs", "true") 1536 1537 keymap := make(map[string]interface{}) 1538 1539 keymap["keys"] = keys 1540 1541 jsonKeys, err := json.Marshal(keymap) 1542 if err != nil { 1543 return nil, errors.Wrap(err, "error marshalling json data") 1544 } 1545 1546 //get the number of retries 1547 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1548 1549 resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchRetrieveDocumentMetadata", batchRetrieveURL, jsonKeys, "", "", maxRetries, true, &queryParms, "_all_docs") 1550 if err != nil { 1551 return nil, err 1552 } 1553 defer closeResponseBody(resp) 1554 1555 if logger.IsEnabledFor(zapcore.DebugLevel) { 1556 dump, _ := httputil.DumpResponse(resp, false) 1557 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1558 logger.Debugf("[%s] HTTP Response: %s", dbclient.DBName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1559 } 1560 1561 //handle as JSON document 1562 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1563 if err != nil { 1564 return nil, errors.Wrap(err, "error reading response body") 1565 } 1566 1567 var jsonResponse = &BatchRetrieveDocMetadataResponse{} 1568 1569 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1570 if err2 != nil { 1571 return nil, errors.Wrap(err2, "error unmarshalling json data") 1572 } 1573 1574 docMetadataArray := []*DocMetadata{} 1575 1576 for _, row := range jsonResponse.Rows { 1577 docMetadata := &DocMetadata{ID: row.ID, Rev: row.DocMetadata.Rev, Version: row.DocMetadata.Version} 1578 docMetadataArray = append(docMetadataArray, docMetadata) 1579 } 1580 1581 logger.Debugf("[%s] Exiting BatchRetrieveDocumentMetadata()", dbclient.DBName) 1582 1583 return docMetadataArray, nil 1584 1585 } 1586 1587 //BatchUpdateDocuments - batch method to batch update documents 1588 func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*BatchUpdateResponse, error) { 1589 dbName := dbclient.DBName 1590 1591 if logger.IsEnabledFor(zapcore.DebugLevel) { 1592 documentIdsString, err := printDocumentIds(documents) 1593 if err == nil { 1594 logger.Debugf("[%s] Entering BatchUpdateDocuments() document ids=[%s]", dbName, documentIdsString) 1595 } else { 1596 logger.Debugf("[%s] Entering BatchUpdateDocuments() Could not print document ids due to error: %+v", dbName, err) 1597 } 1598 } 1599 1600 batchUpdateURL, err := url.Parse(dbclient.CouchInstance.URL()) 1601 if err != nil { 1602 logger.Errorf("URL parse error: %s", err) 1603 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.URL()) 1604 } 1605 1606 documentMap := make(map[string]interface{}) 1607 1608 var jsonDocumentMap []interface{} 1609 1610 for _, jsonDocument := range documents { 1611 1612 //create a document map 1613 var document = make(map[string]interface{}) 1614 1615 //unmarshal the JSON component of the CouchDoc into the document 1616 err = json.Unmarshal(jsonDocument.JSONValue, &document) 1617 if err != nil { 1618 return nil, errors.Wrap(err, "error unmarshalling json data") 1619 } 1620 1621 //iterate through any attachments 1622 if len(jsonDocument.Attachments) > 0 { 1623 1624 //create a file attachment map 1625 fileAttachment := make(map[string]interface{}) 1626 1627 //for each attachment, create a Base64Attachment, name the attachment, 1628 //add the content type and base64 encode the attachment 1629 for _, attachment := range jsonDocument.Attachments { 1630 fileAttachment[attachment.Name] = Base64Attachment{attachment.ContentType, 1631 base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)} 1632 } 1633 1634 //add attachments to the document 1635 document["_attachments"] = fileAttachment 1636 1637 } 1638 1639 //Append the document to the map of documents 1640 jsonDocumentMap = append(jsonDocumentMap, document) 1641 1642 } 1643 1644 //Add the documents to the "docs" item 1645 documentMap["docs"] = jsonDocumentMap 1646 1647 bulkDocsJSON, err := json.Marshal(documentMap) 1648 if err != nil { 1649 return nil, errors.Wrap(err, "error marshalling json data") 1650 } 1651 1652 //get the number of retries 1653 maxRetries := dbclient.CouchInstance.conf.MaxRetries 1654 1655 resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchUpdateDocuments", batchUpdateURL, bulkDocsJSON, "", "", maxRetries, true, nil, "_bulk_docs") 1656 if err != nil { 1657 return nil, err 1658 } 1659 defer closeResponseBody(resp) 1660 1661 if logger.IsEnabledFor(zapcore.DebugLevel) { 1662 dump, _ := httputil.DumpResponse(resp, false) 1663 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1664 logger.Debugf("[%s] HTTP Response: %s", dbclient.DBName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1665 } 1666 1667 //handle as JSON document 1668 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1669 if err != nil { 1670 return nil, errors.Wrap(err, "error reading response body") 1671 } 1672 1673 var jsonResponse = []*BatchUpdateResponse{} 1674 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1675 if err2 != nil { 1676 return nil, errors.Wrap(err2, "error unmarshalling json data") 1677 } 1678 1679 logger.Debugf("[%s] Exiting BatchUpdateDocuments() _bulk_docs response=[%s]", dbclient.DBName, string(jsonResponseRaw)) 1680 1681 return jsonResponse, nil 1682 1683 } 1684 1685 //handleRequestWithRevisionRetry method is a generic http request handler with 1686 //a retry for document revision conflict errors, 1687 //which may be detected during saves or deletes that timed out from client http perspective, 1688 //but which eventually succeeded in couchdb 1689 func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string, 1690 multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values) (*http.Response, *DBReturn, error) { 1691 1692 //Initialize a flag for the revision conflict 1693 revisionConflictDetected := false 1694 var resp *http.Response 1695 var couchDBReturn *DBReturn 1696 var errResp error 1697 1698 //attempt the http request for the max number of retries 1699 //In this case, the retry is to catch problems where a client timeout may miss a 1700 //successful CouchDB update and cause a document revision conflict on a retry in handleRequest 1701 for attempts := 0; attempts <= maxRetries; attempts++ { 1702 1703 //if the revision was not passed in, or if a revision conflict is detected on prior attempt, 1704 //query CouchDB for the document revision 1705 if rev == "" || revisionConflictDetected { 1706 rev = dbclient.getDocumentRevision(id) 1707 } 1708 1709 //handle the request for saving/deleting the couchdb data 1710 resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(context.Background(), method, dbName, functionName, connectURL, 1711 data, rev, multipartBoundary, maxRetries, keepConnectionOpen, queryParms, id) 1712 1713 //If there was a 409 conflict error during the save/delete, log it and retry it. 1714 //Otherwise, break out of the retry loop 1715 if couchDBReturn != nil && couchDBReturn.StatusCode == 409 { 1716 logger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1) 1717 revisionConflictDetected = true 1718 } else { 1719 break 1720 } 1721 } 1722 1723 // return the handleRequest results 1724 return resp, couchDBReturn, errResp 1725 } 1726 1727 func (dbclient *CouchDatabase) handleRequest(method, functionName string, connectURL *url.URL, data []byte, rev, multipartBoundary string, 1728 maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *DBReturn, error) { 1729 1730 return dbclient.CouchInstance.handleRequest(context.Background(), 1731 method, dbclient.DBName, functionName, connectURL, data, rev, multipartBoundary, 1732 maxRetries, keepConnectionOpen, queryParms, pathElements..., 1733 ) 1734 } 1735 1736 //handleRequest method is a generic http request handler. 1737 // If it returns an error, it ensures that the response body is closed, else it is the 1738 // callee's responsibility to close response correctly. 1739 // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned 1740 func (couchInstance *CouchInstance) handleRequest(ctx context.Context, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string, 1741 multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *DBReturn, error) { 1742 1743 logger.Debugf("Entering handleRequest() method=%s url=%v dbName=%s", method, connectURL, dbName) 1744 1745 //create the return objects for couchDB 1746 var resp *http.Response 1747 var errResp error 1748 couchDBReturn := &DBReturn{} 1749 defer couchInstance.recordMetric(time.Now(), dbName, functionName, couchDBReturn) 1750 1751 //set initial wait duration for retries 1752 waitDuration := retryWaitTime * time.Millisecond 1753 1754 if maxRetries < 0 { 1755 return nil, nil, errors.New("number of retries must be zero or greater") 1756 } 1757 1758 requestURL := constructCouchDBUrl(connectURL, dbName, pathElements...) 1759 1760 if queryParms != nil { 1761 requestURL.RawQuery = queryParms.Encode() 1762 } 1763 1764 logger.Debugf("Request URL: %s", requestURL) 1765 1766 //attempt the http request for the max number of retries 1767 // if maxRetries is 0, the database creation will be attempted once and will 1768 // return an error if unsuccessful 1769 // if maxRetries is 3 (default), a maximum of 4 attempts (one attempt with 3 retries) 1770 // will be made with warning entries for unsuccessful attempts 1771 for attempts := 0; attempts <= maxRetries; attempts++ { 1772 1773 //Set up a buffer for the payload data 1774 payloadData := new(bytes.Buffer) 1775 1776 payloadData.ReadFrom(bytes.NewReader(data)) 1777 1778 //Create request based on URL for couchdb operation 1779 req, err := http.NewRequest(method, requestURL.String(), payloadData) 1780 if err != nil { 1781 return nil, nil, errors.Wrap(err, "error creating http request") 1782 } 1783 req.WithContext(ctx) 1784 1785 //set the request to close on completion if shared connections are not allowSharedConnection 1786 //Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused. 1787 //Apache JIRA item for CouchDB https://issues.apache.org/jira/browse/COUCHDB-3394 1788 if !keepConnectionOpen { 1789 req.Close = true 1790 } 1791 1792 //add content header for PUT 1793 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 1794 1795 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 1796 //to application/json. Else, this is contains an attachment and needs to be multipart 1797 if multipartBoundary == "" { 1798 req.Header.Set("Content-Type", "application/json") 1799 } else { 1800 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 1801 } 1802 1803 //check to see if the revision is set, if so, pass as a header 1804 if rev != "" { 1805 req.Header.Set("If-Match", rev) 1806 } 1807 } 1808 1809 //add content header for PUT 1810 if method == http.MethodPut || method == http.MethodPost { 1811 req.Header.Set("Accept", "application/json") 1812 } 1813 1814 //add content header for GET 1815 if method == http.MethodGet { 1816 req.Header.Set("Accept", "multipart/related") 1817 } 1818 1819 //If username and password are set the use basic auth 1820 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 1821 //req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW5w") 1822 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 1823 } 1824 1825 //Execute http request 1826 resp, errResp = couchInstance.client.Do(req) 1827 1828 //check to see if the return from CouchDB is valid 1829 if invalidCouchDBReturn(resp, errResp) { 1830 continue 1831 } 1832 1833 //if there is no golang http error and no CouchDB 500 error, then drop out of the retry 1834 if errResp == nil && resp != nil && resp.StatusCode < 500 { 1835 // if this is an error, then populate the couchDBReturn 1836 if resp.StatusCode >= 400 { 1837 //Read the response body and close it for next attempt 1838 jsonError, err := ioutil.ReadAll(resp.Body) 1839 if err != nil { 1840 return nil, nil, errors.Wrap(err, "error reading response body") 1841 } 1842 defer closeResponseBody(resp) 1843 1844 errorBytes := []byte(jsonError) 1845 //Unmarshal the response 1846 err = json.Unmarshal(errorBytes, &couchDBReturn) 1847 if err != nil { 1848 return nil, nil, errors.Wrap(err, "error unmarshalling json data") 1849 } 1850 } 1851 1852 break 1853 } 1854 1855 // If the maxRetries is greater than 0, then log the retry info 1856 if maxRetries > 0 { 1857 1858 //if this is an unexpected golang http error, log the error and retry 1859 if errResp != nil { 1860 1861 //Log the error with the retry count and continue 1862 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Error:%v", 1863 waitDuration.String(), attempts+1, errResp.Error()) 1864 1865 //otherwise this is an unexpected 500 error from CouchDB. Log the error and retry. 1866 } else { 1867 //Read the response body and close it for next attempt 1868 jsonError, err := ioutil.ReadAll(resp.Body) 1869 defer closeResponseBody(resp) 1870 if err != nil { 1871 return nil, nil, errors.Wrap(err, "error reading response body") 1872 } 1873 1874 errorBytes := []byte(jsonError) 1875 //Unmarshal the response 1876 err = json.Unmarshal(errorBytes, &couchDBReturn) 1877 if err != nil { 1878 return nil, nil, errors.Wrap(err, "error unmarshalling json data") 1879 } 1880 1881 //Log the 500 error with the retry count and continue 1882 logger.Warningf("Retrying couchdb request in %s. Attempt:%v Couch DB Error:%s, Status Code:%v Reason:%v", 1883 waitDuration.String(), attempts+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason) 1884 1885 } 1886 //sleep for specified sleep time, then retry 1887 time.Sleep(waitDuration) 1888 1889 //backoff, doubling the retry time for next attempt 1890 waitDuration *= 2 1891 1892 } 1893 1894 } // end retry loop 1895 1896 //if a golang http error is still present after retries are exhausted, return the error 1897 if errResp != nil { 1898 return nil, couchDBReturn, errors.Wrap(errResp, "http error calling couchdb") 1899 } 1900 1901 //This situation should not occur according to the golang spec. 1902 //if this error returned (errResp) from an http call, then the resp should be not nil, 1903 //this is a structure and StatusCode is an int 1904 //This is meant to provide a more graceful error if this should occur 1905 if invalidCouchDBReturn(resp, errResp) { 1906 return nil, nil, errors.New("unable to connect to CouchDB, check the hostname and port.") 1907 } 1908 1909 //set the return code for the couchDB request 1910 couchDBReturn.StatusCode = resp.StatusCode 1911 1912 // check to see if the status code from couchdb is 400 or higher 1913 // response codes 4XX and 500 will be treated as errors - 1914 // golang error will be created from the couchDBReturn contents and both will be returned 1915 if resp.StatusCode >= 400 { 1916 1917 // if the status code is 400 or greater, log and return an error 1918 logger.Debugf("Error handling CouchDB request. Error:%s, Status Code:%v, Reason:%s", 1919 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1920 1921 return nil, couchDBReturn, errors.Errorf("error handling CouchDB request. Error:%s, Status Code:%v, Reason:%s", 1922 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1923 1924 } 1925 1926 logger.Debugf("Exiting handleRequest()") 1927 1928 //If no errors, then return the http response and the couchdb return object 1929 return resp, couchDBReturn, nil 1930 } 1931 1932 func (ci *CouchInstance) recordMetric(startTime time.Time, dbName, api string, couchDBReturn *DBReturn) { 1933 ci.stats.observeProcessingTime(startTime, dbName, api, strconv.Itoa(couchDBReturn.StatusCode)) 1934 } 1935 1936 //invalidCouchDBResponse checks to make sure either a valid response or error is returned 1937 func invalidCouchDBReturn(resp *http.Response, errResp error) bool { 1938 if resp == nil && errResp == nil { 1939 return true 1940 } 1941 return false 1942 } 1943 1944 //IsJSON tests a string to determine if a valid JSON 1945 func IsJSON(s string) bool { 1946 var js map[string]interface{} 1947 return json.Unmarshal([]byte(s), &js) == nil 1948 } 1949 1950 // encodePathElement uses Golang for url path encoding, additionally: 1951 // '/' is replaced by %2F, otherwise path encoding will treat as path separator and ignore it 1952 // '+' is replaced by %2B, otherwise path encoding will ignore it, while CouchDB will unencode the plus as a space 1953 // Note that all other URL special characters have been tested successfully without need for special handling 1954 func encodePathElement(str string) string { 1955 1956 u := &url.URL{} 1957 u.Path = str 1958 encodedStr := u.EscapedPath() // url encode using golang url path encoding rules 1959 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1960 encodedStr = strings.Replace(encodedStr, "+", "%2B", -1) 1961 1962 return encodedStr 1963 } 1964 1965 func encodeForJSON(str string) (string, error) { 1966 buf := &bytes.Buffer{} 1967 encoder := json.NewEncoder(buf) 1968 if err := encoder.Encode(str); err != nil { 1969 return "", errors.Wrap(err, "error encoding json data") 1970 } 1971 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1972 buffer := buf.Bytes() 1973 return string(buffer[1 : len(buffer)-2]), nil 1974 } 1975 1976 // printDocumentIds is a convenience method to print readable log entries for arrays of pointers 1977 // to couch document IDs 1978 func printDocumentIds(documentPointers []*CouchDoc) (string, error) { 1979 1980 documentIds := []string{} 1981 1982 for _, documentPointer := range documentPointers { 1983 docMetadata := &DocMetadata{} 1984 err := json.Unmarshal(documentPointer.JSONValue, &docMetadata) 1985 if err != nil { 1986 return "", errors.Wrap(err, "error unmarshalling json data") 1987 } 1988 documentIds = append(documentIds, docMetadata.ID) 1989 } 1990 return strings.Join(documentIds, ","), nil 1991 }