github.com/Jeffail/benthos/v3@v3.65.0/lib/input/reader/amazon_s3.go (about) 1 package reader 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/url" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/Jeffail/benthos/v3/lib/log" 16 "github.com/Jeffail/benthos/v3/lib/message" 17 "github.com/Jeffail/benthos/v3/lib/metrics" 18 "github.com/Jeffail/benthos/v3/lib/types" 19 sess "github.com/Jeffail/benthos/v3/lib/util/aws/session" 20 "github.com/Jeffail/gabs/v2" 21 "github.com/aws/aws-sdk-go/aws" 22 "github.com/aws/aws-sdk-go/aws/session" 23 "github.com/aws/aws-sdk-go/service/s3" 24 "github.com/aws/aws-sdk-go/service/s3/s3manager" 25 "github.com/aws/aws-sdk-go/service/sqs" 26 ) 27 28 //------------------------------------------------------------------------------ 29 30 // S3DownloadManagerConfig is a config struct containing fields for an S3 31 // download manager. 32 type S3DownloadManagerConfig struct { 33 Enabled bool `json:"enabled" yaml:"enabled"` 34 } 35 36 // AmazonS3Config contains configuration values for the AmazonS3 input type. 37 type AmazonS3Config struct { 38 sess.Config `json:",inline" yaml:",inline"` 39 Bucket string `json:"bucket" yaml:"bucket"` 40 Prefix string `json:"prefix" yaml:"prefix"` 41 Retries int `json:"retries" yaml:"retries"` 42 ForcePathStyleURLs bool `json:"force_path_style_urls" yaml:"force_path_style_urls"` 43 DownloadManager S3DownloadManagerConfig `json:"download_manager" yaml:"download_manager"` 44 DeleteObjects bool `json:"delete_objects" yaml:"delete_objects"` 45 SQSURL string `json:"sqs_url" yaml:"sqs_url"` 46 SQSEndpoint string `json:"sqs_endpoint" yaml:"sqs_endpoint"` 47 SQSBodyPath string `json:"sqs_body_path" yaml:"sqs_body_path"` 48 SQSBucketPath string `json:"sqs_bucket_path" yaml:"sqs_bucket_path"` 49 SQSEnvelopePath string `json:"sqs_envelope_path" yaml:"sqs_envelope_path"` 50 SQSMaxMessages int64 `json:"sqs_max_messages" yaml:"sqs_max_messages"` 51 MaxBatchCount int `json:"max_batch_count" yaml:"max_batch_count"` 52 Timeout string `json:"timeout" yaml:"timeout"` 53 } 54 55 // NewAmazonS3Config creates a new AmazonS3Config with default values. 56 func NewAmazonS3Config() AmazonS3Config { 57 return AmazonS3Config{ 58 Config: sess.NewConfig(), 59 Bucket: "", 60 Prefix: "", 61 Retries: 3, 62 ForcePathStyleURLs: false, 63 DownloadManager: S3DownloadManagerConfig{ 64 Enabled: true, 65 }, 66 DeleteObjects: false, 67 SQSURL: "", 68 SQSEndpoint: "", 69 SQSBodyPath: "Records.*.s3.object.key", 70 SQSBucketPath: "", 71 SQSEnvelopePath: "", 72 SQSMaxMessages: 10, 73 MaxBatchCount: 1, 74 Timeout: "5s", 75 } 76 } 77 78 //------------------------------------------------------------------------------ 79 80 type objKey struct { 81 s3Key string 82 s3Bucket string 83 attempts int 84 sqsHandle *sqs.DeleteMessageBatchRequestEntry 85 } 86 87 // AmazonS3 is a benthos reader.Type implementation that reads messages from an 88 // Amazon S3 bucket. 89 type AmazonS3 struct { 90 conf AmazonS3Config 91 92 sqsBodyPath string 93 sqsEnvPath string 94 sqsBucketPath string 95 96 readKeys []objKey 97 targetKeys []objKey 98 targetKeysMut sync.Mutex 99 100 readMethod func() (types.Part, objKey, error) 101 102 session *session.Session 103 s3 *s3.S3 104 downloader *s3manager.Downloader 105 sqs *sqs.SQS 106 timeout time.Duration 107 108 log log.Modular 109 stats metrics.Type 110 } 111 112 // NewAmazonS3 creates a new Amazon S3 bucket reader.Type. 113 func NewAmazonS3( 114 conf AmazonS3Config, 115 log log.Modular, 116 stats metrics.Type, 117 ) (*AmazonS3, error) { 118 if len(conf.SQSURL) > 0 && conf.SQSBodyPath == "Records.s3.object.key" { 119 log.Warnf("It looks like a deprecated SQS Body path is configured: 'Records.s3.object.key', you might not receive S3 items unless you update to the new syntax 'Records.*.s3.object.key'") 120 } 121 122 if conf.Bucket == "" { 123 return nil, errors.New("a bucket must be specified (even with an SQS bucket path configured)") 124 } 125 126 var timeout time.Duration 127 if tout := conf.Timeout; len(tout) > 0 { 128 var err error 129 if timeout, err = time.ParseDuration(tout); err != nil { 130 return nil, fmt.Errorf("failed to parse timeout string: %v", err) 131 } 132 } 133 if conf.MaxBatchCount < 1 { 134 return nil, fmt.Errorf("max_batch_count '%v' must be > 0", conf.MaxBatchCount) 135 } 136 s := &AmazonS3{ 137 conf: conf, 138 sqsBodyPath: conf.SQSBodyPath, 139 sqsEnvPath: conf.SQSEnvelopePath, 140 sqsBucketPath: conf.SQSBucketPath, 141 log: log, 142 stats: stats, 143 timeout: timeout, 144 } 145 if conf.DownloadManager.Enabled { 146 s.readMethod = s.readFromMgr 147 } else { 148 s.readMethod = s.read 149 } 150 return s, nil 151 } 152 153 // Connect attempts to establish a connection to the target S3 bucket and any 154 // relevant queues used to traverse the objects (SQS, etc). 155 func (a *AmazonS3) Connect() error { 156 return a.ConnectWithContext(context.Background()) 157 } 158 159 // ConnectWithContext attempts to establish a connection to the target S3 bucket 160 // and any relevant queues used to traverse the objects (SQS, etc). 161 func (a *AmazonS3) ConnectWithContext(ctx context.Context) error { 162 a.targetKeysMut.Lock() 163 defer a.targetKeysMut.Unlock() 164 165 if a.session != nil { 166 return nil 167 } 168 169 sess, err := a.conf.GetSession(func(c *aws.Config) { 170 c.S3ForcePathStyle = aws.Bool(a.conf.ForcePathStyleURLs) 171 }) 172 if err != nil { 173 return err 174 } 175 176 sThree := s3.New(sess) 177 dler := s3manager.NewDownloader(sess) 178 179 if a.conf.SQSURL == "" { 180 listInput := &s3.ListObjectsInput{ 181 Bucket: aws.String(a.conf.Bucket), 182 } 183 if len(a.conf.Prefix) > 0 { 184 listInput.Prefix = aws.String(a.conf.Prefix) 185 } 186 err := sThree.ListObjectsPagesWithContext(ctx, listInput, 187 func(page *s3.ListObjectsOutput, isLastPage bool) bool { 188 for _, obj := range page.Contents { 189 a.targetKeys = append(a.targetKeys, objKey{ 190 s3Key: *obj.Key, 191 attempts: a.conf.Retries, 192 }) 193 } 194 return true 195 }, 196 ) 197 if err != nil { 198 return fmt.Errorf("failed to list objects: %v", err) 199 } 200 } else { 201 sqsSess := sess.Copy() 202 if len(a.conf.SQSEndpoint) > 0 { 203 sqsSess.Config.Endpoint = &a.conf.SQSEndpoint 204 } 205 a.sqs = sqs.New(sqsSess) 206 } 207 208 a.log.Infof("Receiving Amazon S3 objects from bucket: %s\n", a.conf.Bucket) 209 210 a.session = sess 211 a.downloader = dler 212 a.s3 = sThree 213 return nil 214 } 215 216 func digStrsFromSlices(slice []interface{}) []string { 217 var strs []string 218 for _, v := range slice { 219 switch t := v.(type) { 220 case []interface{}: 221 strs = append(strs, digStrsFromSlices(t)...) 222 case string: 223 strs = append(strs, t) 224 } 225 } 226 return strs 227 } 228 229 type objTarget struct { 230 key string 231 bucket string 232 } 233 234 func (a *AmazonS3) parseItemPaths(sqsMsg *string) ([]objTarget, error) { 235 gObj, err := gabs.ParseJSON([]byte(*sqsMsg)) 236 if err != nil { 237 return nil, fmt.Errorf("failed to parse SQS message: %v", err) 238 } 239 240 if len(a.sqsEnvPath) > 0 { 241 switch t := gObj.Path(a.sqsEnvPath).Data().(type) { 242 case string: 243 if gObj, err = gabs.ParseJSON([]byte(t)); err != nil { 244 return nil, fmt.Errorf("failed to parse SQS message envelope: %v", err) 245 } 246 case []interface{}: 247 docs := []interface{}{} 248 strs := digStrsFromSlices(t) 249 for _, v := range strs { 250 var gObj2 interface{} 251 if err2 := json.Unmarshal([]byte(v), &gObj2); err2 == nil { 252 docs = append(docs, gObj2) 253 } 254 } 255 if len(docs) == 0 { 256 return nil, errors.New("couldn't locate S3 items from SQS message") 257 } 258 gObj = gabs.Wrap(docs) 259 default: 260 return nil, fmt.Errorf("unexpected envelope value: %v", t) 261 } 262 } 263 264 var buckets []string 265 switch t := gObj.Path(a.sqsBucketPath).Data().(type) { 266 case string: 267 buckets = []string{t} 268 case []interface{}: 269 buckets = digStrsFromSlices(t) 270 } 271 272 items := []objTarget{} 273 274 switch t := gObj.Path(a.sqsBodyPath).Data().(type) { 275 case string: 276 if strings.HasPrefix(t, a.conf.Prefix) { 277 bucket := "" 278 if len(buckets) > 0 { 279 bucket = buckets[0] 280 } 281 items = append(items, objTarget{ 282 key: t, 283 bucket: bucket, 284 }) 285 } 286 case []interface{}: 287 newTargets := []string{} 288 strs := digStrsFromSlices(t) 289 for _, p := range strs { 290 if strings.HasPrefix(p, a.conf.Prefix) { 291 newTargets = append(newTargets, p) 292 } 293 } 294 if len(newTargets) > 0 { 295 for i, target := range newTargets { 296 bucket := "" 297 if len(buckets) > i { 298 bucket = buckets[i] 299 } 300 decodedTarget, err := url.QueryUnescape(target) 301 if err != nil { 302 return nil, fmt.Errorf("failed to decode S3 path: %v", err) 303 } 304 items = append(items, objTarget{ 305 key: decodedTarget, 306 bucket: bucket, 307 }) 308 } 309 } else { 310 return nil, errors.New("no items found in SQS message at specified path") 311 } 312 default: 313 return nil, errors.New("no items found in SQS message at specified path") 314 } 315 return items, nil 316 } 317 318 func (a *AmazonS3) rejectObjects(keys []objKey) { 319 ctx, done := context.WithTimeout(context.Background(), a.timeout) 320 defer done() 321 322 var failedMessageHandles []*sqs.ChangeMessageVisibilityBatchRequestEntry 323 for _, key := range keys { 324 failedMessageHandles = append(failedMessageHandles, &sqs.ChangeMessageVisibilityBatchRequestEntry{ 325 Id: key.sqsHandle.Id, 326 ReceiptHandle: key.sqsHandle.ReceiptHandle, 327 VisibilityTimeout: aws.Int64(0), 328 }) 329 } 330 for len(failedMessageHandles) > 0 { 331 input := sqs.ChangeMessageVisibilityBatchInput{ 332 QueueUrl: aws.String(a.conf.SQSURL), 333 Entries: failedMessageHandles, 334 } 335 336 // trim input entries to max size 337 if len(failedMessageHandles) > 10 { 338 input.Entries, failedMessageHandles = failedMessageHandles[:10], failedMessageHandles[10:] 339 } else { 340 failedMessageHandles = nil 341 } 342 if _, err := a.sqs.ChangeMessageVisibilityBatchWithContext(ctx, &input); err != nil { 343 a.log.Errorf("Failed to reject SQS message: %v\n", err) 344 } 345 } 346 } 347 348 func (a *AmazonS3) deleteObjects(keys []objKey) { 349 ctx, done := context.WithTimeout(context.Background(), a.timeout) 350 defer done() 351 352 deleteHandles := []*sqs.DeleteMessageBatchRequestEntry{} 353 for _, key := range keys { 354 if a.conf.DeleteObjects { 355 bucket := a.conf.Bucket 356 if len(key.s3Bucket) > 0 { 357 bucket = key.s3Bucket 358 } 359 if _, serr := a.s3.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ 360 Bucket: aws.String(bucket), 361 Key: aws.String(key.s3Key), 362 }); serr != nil { 363 a.log.Errorf("Failed to delete consumed object: %v\n", serr) 364 } 365 } 366 if key.sqsHandle != nil { 367 deleteHandles = append(deleteHandles, key.sqsHandle) 368 } 369 } 370 for len(deleteHandles) > 0 { 371 input := sqs.DeleteMessageBatchInput{ 372 QueueUrl: aws.String(a.conf.SQSURL), 373 Entries: deleteHandles, 374 } 375 376 // trim input entries to max size 377 if len(deleteHandles) > 10 { 378 input.Entries, deleteHandles = deleteHandles[:10], deleteHandles[10:] 379 } else { 380 deleteHandles = nil 381 } 382 383 if res, serr := a.sqs.DeleteMessageBatchWithContext(ctx, &input); serr != nil { 384 a.log.Errorf("Failed to delete consumed SQS messages: %v\n", serr) 385 } else { 386 for _, fail := range res.Failed { 387 a.log.Errorf("Failed to delete consumed SQS message '%v', response code: %v\n", *fail.Id, *fail.Code) 388 } 389 } 390 } 391 } 392 393 func (a *AmazonS3) readSQSEvents() error { 394 var dudMessageHandles []*sqs.ChangeMessageVisibilityBatchRequestEntry 395 addDudFn := func(m *sqs.Message) { 396 dudMessageHandles = append(dudMessageHandles, &sqs.ChangeMessageVisibilityBatchRequestEntry{ 397 Id: m.MessageId, 398 ReceiptHandle: m.ReceiptHandle, 399 VisibilityTimeout: aws.Int64(0), 400 }) 401 } 402 403 output, err := a.sqs.ReceiveMessage(&sqs.ReceiveMessageInput{ 404 QueueUrl: aws.String(a.conf.SQSURL), 405 MaxNumberOfMessages: aws.Int64(a.conf.SQSMaxMessages), 406 WaitTimeSeconds: aws.Int64(int64(a.timeout.Seconds())), 407 }) 408 if err != nil { 409 return err 410 } 411 412 for _, sqsMsg := range output.Messages { 413 msgHandle := &sqs.DeleteMessageBatchRequestEntry{ 414 Id: sqsMsg.MessageId, 415 ReceiptHandle: sqsMsg.ReceiptHandle, 416 } 417 418 if sqsMsg.Body == nil { 419 addDudFn(sqsMsg) 420 a.log.Errorln("Received empty SQS message") 421 continue 422 } 423 424 items, err := a.parseItemPaths(sqsMsg.Body) 425 if err != nil { 426 addDudFn(sqsMsg) 427 a.log.Errorf("SQS error: %v\n", err) 428 continue 429 } 430 431 for _, item := range items { 432 a.targetKeys = append(a.targetKeys, objKey{ 433 s3Key: item.key, 434 s3Bucket: item.bucket, 435 attempts: a.conf.Retries, 436 }) 437 } 438 a.targetKeys[len(a.targetKeys)-1].sqsHandle = msgHandle 439 } 440 441 // Discard any SQS messages not associated with a target file. 442 for len(dudMessageHandles) > 0 { 443 input := sqs.ChangeMessageVisibilityBatchInput{ 444 QueueUrl: aws.String(a.conf.SQSURL), 445 Entries: dudMessageHandles, 446 } 447 448 // trim input entries to max size 449 if len(dudMessageHandles) > 10 { 450 input.Entries, dudMessageHandles = dudMessageHandles[:10], dudMessageHandles[10:] 451 } else { 452 dudMessageHandles = nil 453 } 454 a.sqs.ChangeMessageVisibilityBatch(&input) 455 } 456 457 if len(a.targetKeys) == 0 { 458 return types.ErrTimeout 459 } 460 return nil 461 } 462 463 func (a *AmazonS3) pushReadKey(key objKey) { 464 a.readKeys = append(a.readKeys, key) 465 } 466 467 func (a *AmazonS3) popTargetKey() { 468 if len(a.targetKeys) == 0 { 469 return 470 } 471 if len(a.targetKeys) > 1 { 472 a.targetKeys = a.targetKeys[1:] 473 } else { 474 a.targetKeys = nil 475 } 476 } 477 478 // ReadWithContext attempts to read a new message from the target S3 bucket. 479 func (a *AmazonS3) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) { 480 a.targetKeysMut.Lock() 481 defer a.targetKeysMut.Unlock() 482 483 if a.session == nil { 484 return nil, nil, types.ErrNotConnected 485 } 486 487 if len(a.targetKeys) == 0 { 488 if a.sqs != nil { 489 if err := a.readSQSEvents(); err != nil { 490 return nil, nil, err 491 } 492 } else { 493 // If we aren't using SQS but exhausted our targets we are done. 494 return nil, nil, types.ErrTypeClosed 495 } 496 } 497 if len(a.targetKeys) == 0 { 498 return nil, nil, types.ErrTimeout 499 } 500 501 msg := message.New(nil) 502 503 part, obj, err := a.readMethod() 504 if err != nil { 505 return nil, nil, err 506 } 507 508 msg.Append(part) 509 return msg, func(rctx context.Context, res types.Response) error { 510 if res.Error() == nil { 511 a.deleteObjects([]objKey{obj}) 512 } else { 513 if a.conf.SQSURL == "" { 514 a.targetKeysMut.Lock() 515 // nolint:gocritic // Ignore appendAssign: append result not assigned to the same slice 516 a.targetKeys = append(a.readKeys, obj) 517 a.targetKeysMut.Unlock() 518 } else { 519 a.rejectObjects([]objKey{obj}) 520 } 521 } 522 return nil 523 }, nil 524 } 525 526 // Read attempts to read a new message from the target S3 bucket. 527 func (a *AmazonS3) Read() (types.Message, error) { 528 a.targetKeysMut.Lock() 529 defer a.targetKeysMut.Unlock() 530 531 if a.session == nil { 532 return nil, types.ErrNotConnected 533 } 534 535 timeoutAt := time.Now().Add(a.timeout) 536 537 if len(a.targetKeys) == 0 { 538 if a.sqs != nil { 539 if err := a.readSQSEvents(); err != nil { 540 return nil, err 541 } 542 } else { 543 // If we aren't using SQS but exhausted our targets we are done. 544 return nil, types.ErrTypeClosed 545 } 546 } 547 if len(a.targetKeys) == 0 { 548 return nil, types.ErrTimeout 549 } 550 551 msg := message.New(nil) 552 553 for len(a.targetKeys) > 0 && msg.Len() < a.conf.MaxBatchCount && time.Until(timeoutAt) > 0 { 554 part, objKey, err := a.readMethod() 555 if err != nil { 556 if err == types.ErrTimeout { 557 break 558 } 559 a.log.Errorf("Error: %v\n", err) 560 if msg.Len() == 0 { 561 return nil, err 562 } 563 } else { 564 msg.Append(part) 565 a.pushReadKey(objKey) 566 } 567 } 568 if msg.Len() == 0 { 569 return nil, types.ErrTimeout 570 } 571 572 return msg, nil 573 } 574 575 func addS3Metadata(p types.Part, obj *s3.GetObjectOutput) { 576 meta := p.Metadata() 577 if obj.LastModified != nil { 578 meta.Set("s3_last_modified", obj.LastModified.Format(time.RFC3339)) 579 meta.Set("s3_last_modified_unix", strconv.FormatInt(obj.LastModified.Unix(), 10)) 580 } 581 if obj.ContentType != nil { 582 meta.Set("s3_content_type", *obj.ContentType) 583 } 584 if obj.ContentEncoding != nil { 585 meta.Set("s3_content_encoding", *obj.ContentEncoding) 586 } 587 } 588 589 // read attempts to read a new message from the target S3 bucket. 590 func (a *AmazonS3) read() (types.Part, objKey, error) { 591 target := a.targetKeys[0] 592 593 bucket := a.conf.Bucket 594 if len(target.s3Bucket) > 0 { 595 bucket = target.s3Bucket 596 } 597 obj, err := a.s3.GetObject(&s3.GetObjectInput{ 598 Bucket: aws.String(bucket), 599 Key: aws.String(target.s3Key), 600 }) 601 if err != nil { 602 target.attempts-- 603 if target.attempts == 0 { 604 // Remove the target file from our list. 605 a.popTargetKey() 606 a.log.Errorf("Failed to download file '%s' from bucket '%s' after '%v' attempts: %v\n", target.s3Key, bucket, a.conf.Retries, err) 607 } else { 608 a.targetKeys[0] = target 609 return nil, objKey{}, fmt.Errorf("failed to download file '%s' from bucket '%s': %v", target.s3Key, bucket, err) 610 } 611 return nil, objKey{}, types.ErrTimeout 612 } 613 614 bytes, err := io.ReadAll(obj.Body) 615 obj.Body.Close() 616 if err != nil { 617 a.popTargetKey() 618 return nil, objKey{}, fmt.Errorf("failed to download file '%s' from bucket '%s': %v", target.s3Key, bucket, err) 619 } 620 621 part := message.NewPart(bytes) 622 meta := part.Metadata() 623 for k, v := range obj.Metadata { 624 meta.Set(k, *v) 625 } 626 meta.Set("s3_key", target.s3Key) 627 meta.Set("s3_bucket", bucket) 628 addS3Metadata(part, obj) 629 630 a.popTargetKey() 631 return part, target, nil 632 } 633 634 // readFromMgr attempts to read a new message from the target S3 bucket using a 635 // download manager. 636 func (a *AmazonS3) readFromMgr() (types.Part, objKey, error) { 637 target := a.targetKeys[0] 638 639 buff := &aws.WriteAtBuffer{} 640 641 bucket := a.conf.Bucket 642 if len(target.s3Bucket) > 0 { 643 bucket = target.s3Bucket 644 } 645 646 // Write the contents of S3 Object to the file 647 if _, err := a.downloader.Download(buff, &s3.GetObjectInput{ 648 Bucket: aws.String(bucket), 649 Key: aws.String(target.s3Key), 650 }); err != nil { 651 target.attempts-- 652 if target.attempts == 0 { 653 // Remove the target file from our list. 654 a.popTargetKey() 655 a.log.Errorf("Failed to download file '%s' from bucket '%s' after '%v' attempts: %v\n", target.s3Key, bucket, a.conf.Retries, err) 656 } else { 657 a.targetKeys[0] = target 658 return nil, objKey{}, fmt.Errorf("failed to download file '%s' from bucket '%s': %v", target.s3Key, bucket, err) 659 } 660 return nil, objKey{}, types.ErrTimeout 661 } 662 663 part := message.NewPart(buff.Bytes()) 664 part.Metadata(). 665 Set("s3_key", target.s3Key). 666 Set("s3_bucket", bucket) 667 668 a.popTargetKey() 669 return part, target, nil 670 } 671 672 // Acknowledge confirms whether or not our unacknowledged messages have been 673 // successfully propagated or not. 674 func (a *AmazonS3) Acknowledge(err error) error { 675 if err == nil { 676 a.deleteObjects(a.readKeys) 677 } else { 678 if a.sqs == nil { 679 a.targetKeysMut.Lock() 680 a.targetKeys = append(a.readKeys, a.targetKeys...) 681 a.targetKeysMut.Unlock() 682 } else { 683 a.rejectObjects(a.readKeys) 684 } 685 } 686 a.readKeys = nil 687 return nil 688 } 689 690 // CloseAsync begins cleaning up resources used by this reader asynchronously. 691 func (a *AmazonS3) CloseAsync() { 692 go func() { 693 a.targetKeysMut.Lock() 694 a.rejectObjects(a.targetKeys) 695 a.targetKeys = nil 696 a.targetKeysMut.Unlock() 697 }() 698 } 699 700 // WaitForClose will block until either the reader is closed or a specified 701 // timeout occurs. 702 func (a *AmazonS3) WaitForClose(time.Duration) error { 703 return nil 704 } 705 706 //------------------------------------------------------------------------------