aws-sdk-s3 1.211.0 → 1.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/VERSION +1 -1
- data/lib/aws-sdk-s3/bucket.rb +44 -16
- data/lib/aws-sdk-s3/client.rb +479 -162
- data/lib/aws-sdk-s3/client_api.rb +54 -0
- data/lib/aws-sdk-s3/customizations.rb +8 -2
- data/lib/aws-sdk-s3/directory_download_error.rb +16 -0
- data/lib/aws-sdk-s3/directory_downloader.rb +230 -0
- data/lib/aws-sdk-s3/directory_upload_error.rb +16 -0
- data/lib/aws-sdk-s3/directory_uploader.rb +270 -0
- data/lib/aws-sdk-s3/endpoints.rb +13 -0
- data/lib/aws-sdk-s3/errors.rb +11 -0
- data/lib/aws-sdk-s3/multipart_upload.rb +12 -12
- data/lib/aws-sdk-s3/multipart_upload_part.rb +8 -8
- data/lib/aws-sdk-s3/object.rb +32 -32
- data/lib/aws-sdk-s3/object_acl.rb +4 -4
- data/lib/aws-sdk-s3/object_summary.rb +28 -28
- data/lib/aws-sdk-s3/object_version.rb +16 -16
- data/lib/aws-sdk-s3/resource.rb +28 -0
- data/lib/aws-sdk-s3/transfer_manager.rb +237 -18
- data/lib/aws-sdk-s3/types.rb +350 -121
- data/lib/aws-sdk-s3.rb +1 -1
- data/sig/bucket.rbs +2 -1
- data/sig/client.rbs +24 -1
- data/sig/errors.rbs +2 -0
- data/sig/resource.rbs +2 -1
- data/sig/types.rbs +38 -0
- metadata +7 -3
|
@@ -24,6 +24,7 @@ module Aws::S3
|
|
|
24
24
|
AcceptRanges = Shapes::StringShape.new(name: 'AcceptRanges')
|
|
25
25
|
AccessControlPolicy = Shapes::StructureShape.new(name: 'AccessControlPolicy')
|
|
26
26
|
AccessControlTranslation = Shapes::StructureShape.new(name: 'AccessControlTranslation')
|
|
27
|
+
AccessDenied = Shapes::StructureShape.new(name: 'AccessDenied')
|
|
27
28
|
AccessKeyIdValue = Shapes::StringShape.new(name: 'AccessKeyIdValue')
|
|
28
29
|
AccessPointAlias = Shapes::BooleanShape.new(name: 'AccessPointAlias')
|
|
29
30
|
AccessPointArn = Shapes::StringShape.new(name: 'AccessPointArn')
|
|
@@ -60,6 +61,7 @@ module Aws::S3
|
|
|
60
61
|
BucketLoggingStatus = Shapes::StructureShape.new(name: 'BucketLoggingStatus')
|
|
61
62
|
BucketLogsPermission = Shapes::StringShape.new(name: 'BucketLogsPermission')
|
|
62
63
|
BucketName = Shapes::StringShape.new(name: 'BucketName')
|
|
64
|
+
BucketNamespace = Shapes::StringShape.new(name: 'BucketNamespace')
|
|
63
65
|
BucketRegion = Shapes::StringShape.new(name: 'BucketRegion')
|
|
64
66
|
BucketType = Shapes::StringShape.new(name: 'BucketType')
|
|
65
67
|
BucketVersioningStatus = Shapes::StringShape.new(name: 'BucketVersioningStatus')
|
|
@@ -438,6 +440,7 @@ module Aws::S3
|
|
|
438
440
|
NoSuchBucket = Shapes::StructureShape.new(name: 'NoSuchBucket')
|
|
439
441
|
NoSuchKey = Shapes::StructureShape.new(name: 'NoSuchKey')
|
|
440
442
|
NoSuchUpload = Shapes::StructureShape.new(name: 'NoSuchUpload')
|
|
443
|
+
NonEmptyKmsKeyArnString = Shapes::StringShape.new(name: 'NonEmptyKmsKeyArnString')
|
|
441
444
|
NoncurrentVersionExpiration = Shapes::StructureShape.new(name: 'NoncurrentVersionExpiration')
|
|
442
445
|
NoncurrentVersionTransition = Shapes::StructureShape.new(name: 'NoncurrentVersionTransition')
|
|
443
446
|
NoncurrentVersionTransitionList = Shapes::ListShape.new(name: 'NoncurrentVersionTransitionList', flattened: true)
|
|
@@ -450,6 +453,7 @@ module Aws::S3
|
|
|
450
453
|
ObjectAttributes = Shapes::StringShape.new(name: 'ObjectAttributes')
|
|
451
454
|
ObjectAttributesList = Shapes::ListShape.new(name: 'ObjectAttributesList')
|
|
452
455
|
ObjectCannedACL = Shapes::StringShape.new(name: 'ObjectCannedACL')
|
|
456
|
+
ObjectEncryption = Shapes::UnionShape.new(name: 'ObjectEncryption')
|
|
453
457
|
ObjectIdentifier = Shapes::StructureShape.new(name: 'ObjectIdentifier')
|
|
454
458
|
ObjectIdentifierList = Shapes::ListShape.new(name: 'ObjectIdentifierList', flattened: true)
|
|
455
459
|
ObjectKey = Shapes::StringShape.new(name: 'ObjectKey')
|
|
@@ -616,6 +620,7 @@ module Aws::S3
|
|
|
616
620
|
SSECustomerKey = Shapes::StringShape.new(name: 'SSECustomerKey')
|
|
617
621
|
SSECustomerKeyMD5 = Shapes::StringShape.new(name: 'SSECustomerKeyMD5')
|
|
618
622
|
SSEKMS = Shapes::StructureShape.new(name: 'SSEKMS', locationName: "SSE-KMS")
|
|
623
|
+
SSEKMSEncryption = Shapes::StructureShape.new(name: 'SSEKMSEncryption', locationName: "SSE-KMS")
|
|
619
624
|
SSEKMSEncryptionContext = Shapes::StringShape.new(name: 'SSEKMSEncryptionContext')
|
|
620
625
|
SSEKMSKeyId = Shapes::StringShape.new(name: 'SSEKMSKeyId')
|
|
621
626
|
SSES3 = Shapes::StructureShape.new(name: 'SSES3', locationName: "SSE-S3")
|
|
@@ -678,6 +683,8 @@ module Aws::S3
|
|
|
678
683
|
URI = Shapes::StringShape.new(name: 'URI')
|
|
679
684
|
UpdateBucketMetadataInventoryTableConfigurationRequest = Shapes::StructureShape.new(name: 'UpdateBucketMetadataInventoryTableConfigurationRequest')
|
|
680
685
|
UpdateBucketMetadataJournalTableConfigurationRequest = Shapes::StructureShape.new(name: 'UpdateBucketMetadataJournalTableConfigurationRequest')
|
|
686
|
+
UpdateObjectEncryptionRequest = Shapes::StructureShape.new(name: 'UpdateObjectEncryptionRequest')
|
|
687
|
+
UpdateObjectEncryptionResponse = Shapes::StructureShape.new(name: 'UpdateObjectEncryptionResponse')
|
|
681
688
|
UploadIdMarker = Shapes::StringShape.new(name: 'UploadIdMarker')
|
|
682
689
|
UploadPartCopyOutput = Shapes::StructureShape.new(name: 'UploadPartCopyOutput')
|
|
683
690
|
UploadPartCopyRequest = Shapes::StructureShape.new(name: 'UploadPartCopyRequest')
|
|
@@ -721,6 +728,8 @@ module Aws::S3
|
|
|
721
728
|
AccessControlTranslation.add_member(:owner, Shapes::ShapeRef.new(shape: OwnerOverride, required: true, location_name: "Owner"))
|
|
722
729
|
AccessControlTranslation.struct_class = Types::AccessControlTranslation
|
|
723
730
|
|
|
731
|
+
AccessDenied.struct_class = Types::AccessDenied
|
|
732
|
+
|
|
724
733
|
AllowedHeaders.member = Shapes::ShapeRef.new(shape: AllowedHeader)
|
|
725
734
|
|
|
726
735
|
AllowedMethods.member = Shapes::ShapeRef.new(shape: AllowedMethod)
|
|
@@ -1005,6 +1014,7 @@ module Aws::S3
|
|
|
1005
1014
|
CreateBucketRequest.add_member(:grant_write_acp, Shapes::ShapeRef.new(shape: GrantWriteACP, location: "header", location_name: "x-amz-grant-write-acp"))
|
|
1006
1015
|
CreateBucketRequest.add_member(:object_lock_enabled_for_bucket, Shapes::ShapeRef.new(shape: ObjectLockEnabledForBucket, location: "header", location_name: "x-amz-bucket-object-lock-enabled"))
|
|
1007
1016
|
CreateBucketRequest.add_member(:object_ownership, Shapes::ShapeRef.new(shape: ObjectOwnership, location: "header", location_name: "x-amz-object-ownership"))
|
|
1017
|
+
CreateBucketRequest.add_member(:bucket_namespace, Shapes::ShapeRef.new(shape: BucketNamespace, location: "header", location_name: "x-amz-bucket-namespace"))
|
|
1008
1018
|
CreateBucketRequest.struct_class = Types::CreateBucketRequest
|
|
1009
1019
|
CreateBucketRequest[:payload] = :create_bucket_configuration
|
|
1010
1020
|
CreateBucketRequest[:payload_member] = CreateBucketRequest.member(:create_bucket_configuration)
|
|
@@ -2210,6 +2220,12 @@ module Aws::S3
|
|
|
2210
2220
|
|
|
2211
2221
|
ObjectAttributesList.member = Shapes::ShapeRef.new(shape: ObjectAttributes)
|
|
2212
2222
|
|
|
2223
|
+
ObjectEncryption.add_member(:ssekms, Shapes::ShapeRef.new(shape: SSEKMSEncryption, location_name: "SSE-KMS"))
|
|
2224
|
+
ObjectEncryption.add_member(:unknown, Shapes::ShapeRef.new(shape: nil, location_name: 'unknown'))
|
|
2225
|
+
ObjectEncryption.add_member_subclass(:ssekms, Types::ObjectEncryption::Ssekms)
|
|
2226
|
+
ObjectEncryption.add_member_subclass(:unknown, Types::ObjectEncryption::Unknown)
|
|
2227
|
+
ObjectEncryption.struct_class = Types::ObjectEncryption
|
|
2228
|
+
|
|
2213
2229
|
ObjectIdentifier.add_member(:key, Shapes::ShapeRef.new(shape: ObjectKey, required: true, location_name: "Key"))
|
|
2214
2230
|
ObjectIdentifier.add_member(:version_id, Shapes::ShapeRef.new(shape: ObjectVersionId, location_name: "VersionId"))
|
|
2215
2231
|
ObjectIdentifier.add_member(:etag, Shapes::ShapeRef.new(shape: ETag, location_name: "ETag"))
|
|
@@ -2830,6 +2846,10 @@ module Aws::S3
|
|
|
2830
2846
|
SSEKMS.add_member(:key_id, Shapes::ShapeRef.new(shape: SSEKMSKeyId, required: true, location_name: "KeyId"))
|
|
2831
2847
|
SSEKMS.struct_class = Types::SSEKMS
|
|
2832
2848
|
|
|
2849
|
+
SSEKMSEncryption.add_member(:kms_key_arn, Shapes::ShapeRef.new(shape: NonEmptyKmsKeyArnString, required: true, location_name: "KMSKeyArn"))
|
|
2850
|
+
SSEKMSEncryption.add_member(:bucket_key_enabled, Shapes::ShapeRef.new(shape: BucketKeyEnabled, location_name: "BucketKeyEnabled"))
|
|
2851
|
+
SSEKMSEncryption.struct_class = Types::SSEKMSEncryption
|
|
2852
|
+
|
|
2833
2853
|
SSES3.struct_class = Types::SSES3
|
|
2834
2854
|
|
|
2835
2855
|
ScanRange.add_member(:start, Shapes::ShapeRef.new(shape: Start, location_name: "Start"))
|
|
@@ -2978,6 +2998,21 @@ module Aws::S3
|
|
|
2978
2998
|
UpdateBucketMetadataJournalTableConfigurationRequest[:payload] = :journal_table_configuration
|
|
2979
2999
|
UpdateBucketMetadataJournalTableConfigurationRequest[:payload_member] = UpdateBucketMetadataJournalTableConfigurationRequest.member(:journal_table_configuration)
|
|
2980
3000
|
|
|
3001
|
+
UpdateObjectEncryptionRequest.add_member(:bucket, Shapes::ShapeRef.new(shape: BucketName, required: true, location: "uri", location_name: "Bucket", metadata: {"contextParam" => {"name" => "Bucket"}}))
|
|
3002
|
+
UpdateObjectEncryptionRequest.add_member(:key, Shapes::ShapeRef.new(shape: ObjectKey, required: true, location: "uri", location_name: "Key"))
|
|
3003
|
+
UpdateObjectEncryptionRequest.add_member(:version_id, Shapes::ShapeRef.new(shape: ObjectVersionId, location: "querystring", location_name: "versionId"))
|
|
3004
|
+
UpdateObjectEncryptionRequest.add_member(:object_encryption, Shapes::ShapeRef.new(shape: ObjectEncryption, required: true, location_name: "ObjectEncryption", metadata: {"xmlNamespace" => {"uri" => "http://s3.amazonaws.com/doc/2006-03-01/"}}))
|
|
3005
|
+
UpdateObjectEncryptionRequest.add_member(:request_payer, Shapes::ShapeRef.new(shape: RequestPayer, location: "header", location_name: "x-amz-request-payer"))
|
|
3006
|
+
UpdateObjectEncryptionRequest.add_member(:expected_bucket_owner, Shapes::ShapeRef.new(shape: AccountId, location: "header", location_name: "x-amz-expected-bucket-owner"))
|
|
3007
|
+
UpdateObjectEncryptionRequest.add_member(:content_md5, Shapes::ShapeRef.new(shape: ContentMD5, location: "header", location_name: "Content-MD5"))
|
|
3008
|
+
UpdateObjectEncryptionRequest.add_member(:checksum_algorithm, Shapes::ShapeRef.new(shape: ChecksumAlgorithm, location: "header", location_name: "x-amz-sdk-checksum-algorithm"))
|
|
3009
|
+
UpdateObjectEncryptionRequest.struct_class = Types::UpdateObjectEncryptionRequest
|
|
3010
|
+
UpdateObjectEncryptionRequest[:payload] = :object_encryption
|
|
3011
|
+
UpdateObjectEncryptionRequest[:payload_member] = UpdateObjectEncryptionRequest.member(:object_encryption)
|
|
3012
|
+
|
|
3013
|
+
UpdateObjectEncryptionResponse.add_member(:request_charged, Shapes::ShapeRef.new(shape: RequestCharged, location: "header", location_name: "x-amz-request-charged"))
|
|
3014
|
+
UpdateObjectEncryptionResponse.struct_class = Types::UpdateObjectEncryptionResponse
|
|
3015
|
+
|
|
2981
3016
|
UploadPartCopyOutput.add_member(:copy_source_version_id, Shapes::ShapeRef.new(shape: CopySourceVersionId, location: "header", location_name: "x-amz-copy-source-version-id"))
|
|
2982
3017
|
UploadPartCopyOutput.add_member(:copy_part_result, Shapes::ShapeRef.new(shape: CopyPartResult, location_name: "CopyPartResult"))
|
|
2983
3018
|
UploadPartCopyOutput.add_member(:server_side_encryption, Shapes::ShapeRef.new(shape: ServerSideEncryption, location: "header", location_name: "x-amz-server-side-encryption"))
|
|
@@ -4298,6 +4333,25 @@ module Aws::S3
|
|
|
4298
4333
|
o.output = Shapes::ShapeRef.new(shape: Shapes::StructureShape.new(struct_class: Aws::EmptyStructure))
|
|
4299
4334
|
end)
|
|
4300
4335
|
|
|
4336
|
+
api.add_operation(:update_object_encryption, Seahorse::Model::Operation.new.tap do |o|
|
|
4337
|
+
o.name = "UpdateObjectEncryption"
|
|
4338
|
+
o.http_method = "PUT"
|
|
4339
|
+
o.http_request_uri = "/{Key+}?encryption"
|
|
4340
|
+
o.http_checksum = {
|
|
4341
|
+
"requestAlgorithmMember" => "checksum_algorithm",
|
|
4342
|
+
"requestChecksumRequired" => true,
|
|
4343
|
+
}
|
|
4344
|
+
o.http_checksum = {
|
|
4345
|
+
"requestAlgorithmMember" => "checksum_algorithm",
|
|
4346
|
+
"requestChecksumRequired" => true,
|
|
4347
|
+
}
|
|
4348
|
+
o.input = Shapes::ShapeRef.new(shape: UpdateObjectEncryptionRequest)
|
|
4349
|
+
o.output = Shapes::ShapeRef.new(shape: UpdateObjectEncryptionResponse)
|
|
4350
|
+
o.errors << Shapes::ShapeRef.new(shape: NoSuchKey)
|
|
4351
|
+
o.errors << Shapes::ShapeRef.new(shape: InvalidRequest)
|
|
4352
|
+
o.errors << Shapes::ShapeRef.new(shape: AccessDenied)
|
|
4353
|
+
end)
|
|
4354
|
+
|
|
4301
4355
|
api.add_operation(:upload_part, Seahorse::Model::Operation.new.tap do |o|
|
|
4302
4356
|
o.name = "UploadPart"
|
|
4303
4357
|
o.http_method = "PUT"
|
|
@@ -7,15 +7,21 @@ module Aws
|
|
|
7
7
|
autoload :Encryption, 'aws-sdk-s3/encryption'
|
|
8
8
|
autoload :EncryptionV2, 'aws-sdk-s3/encryption_v2'
|
|
9
9
|
autoload :EncryptionV3, 'aws-sdk-s3/encryption_v3'
|
|
10
|
-
autoload :
|
|
10
|
+
autoload :LegacySigner, 'aws-sdk-s3/legacy_signer'
|
|
11
|
+
|
|
12
|
+
# transfer manager + multipart upload/download utilities
|
|
11
13
|
autoload :DefaultExecutor, 'aws-sdk-s3/default_executor'
|
|
14
|
+
autoload :FilePart, 'aws-sdk-s3/file_part'
|
|
12
15
|
autoload :FileUploader, 'aws-sdk-s3/file_uploader'
|
|
13
16
|
autoload :FileDownloader, 'aws-sdk-s3/file_downloader'
|
|
14
|
-
autoload :LegacySigner, 'aws-sdk-s3/legacy_signer'
|
|
15
17
|
autoload :MultipartDownloadError, 'aws-sdk-s3/multipart_download_error'
|
|
16
18
|
autoload :MultipartFileUploader, 'aws-sdk-s3/multipart_file_uploader'
|
|
17
19
|
autoload :MultipartStreamUploader, 'aws-sdk-s3/multipart_stream_uploader'
|
|
18
20
|
autoload :MultipartUploadError, 'aws-sdk-s3/multipart_upload_error'
|
|
21
|
+
autoload :DirectoryDownloadError, 'aws-sdk-s3/directory_download_error'
|
|
22
|
+
autoload :DirectoryDownloader, 'aws-sdk-s3/directory_downloader'
|
|
23
|
+
autoload :DirectoryUploadError, 'aws-sdk-s3/directory_upload_error'
|
|
24
|
+
autoload :DirectoryUploader, 'aws-sdk-s3/directory_uploader'
|
|
19
25
|
autoload :ObjectCopier, 'aws-sdk-s3/object_copier'
|
|
20
26
|
autoload :ObjectMultipartCopier, 'aws-sdk-s3/object_multipart_copier'
|
|
21
27
|
autoload :PresignedPost, 'aws-sdk-s3/presigned_post'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module S3
|
|
5
|
+
# Raised when DirectoryDownloader fails to download objects from S3 bucket
|
|
6
|
+
class DirectoryDownloadError < StandardError
|
|
7
|
+
def initialize(message, errors = [])
|
|
8
|
+
@errors = errors
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Array<StandardError>] The list of errors encountered when downloading objects
|
|
13
|
+
attr_reader :errors
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module S3
|
|
5
|
+
# @api private
|
|
6
|
+
# This is a one-shot class that downloads objects from a bucket to a local directory.
|
|
7
|
+
# This works as follows:
|
|
8
|
+
# * ObjectProducer runs in a background thread, calling `list_objects_v2` and
|
|
9
|
+
# pushing entries into a SizedQueue (max: 100).
|
|
10
|
+
# * An internal executor pulls from that queue and posts work. Each task uses
|
|
11
|
+
# FileDownloader to download objects then signals completion via `completion_queue`.
|
|
12
|
+
#
|
|
13
|
+
# We track how many tasks we posted, then pop that many times from `completion_queue`
|
|
14
|
+
# to wait for everything to finish.
|
|
15
|
+
#
|
|
16
|
+
# Errors are collected in a mutex-protected array. On failure (unless ignore_failure is set),
|
|
17
|
+
# we call abort which closes the queue - the producer catches ClosedQueueError and exits cleanly.
|
|
18
|
+
class DirectoryDownloader
|
|
19
|
+
def initialize(options = {})
|
|
20
|
+
@client = options[:client] || Client.new
|
|
21
|
+
@executor = options[:executor] || DefaultExecutor.new
|
|
22
|
+
@logger = options[:logger]
|
|
23
|
+
@producer = nil
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :client, :executor
|
|
28
|
+
|
|
29
|
+
def abort
|
|
30
|
+
@producer&.close
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def download(destination, bucket:, **options)
|
|
34
|
+
if File.exist?(destination)
|
|
35
|
+
raise ArgumentError, 'invalid destination, expected a directory' unless File.directory?(destination)
|
|
36
|
+
else
|
|
37
|
+
FileUtils.mkdir_p(destination)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
download_opts = build_download_opts(destination, options)
|
|
41
|
+
@producer = ObjectProducer.new(build_producer_opts(destination, bucket, options))
|
|
42
|
+
downloader = FileDownloader.new(client: @client, executor: @executor)
|
|
43
|
+
downloads, errors = process_download_queue(downloader, download_opts)
|
|
44
|
+
build_result(downloads, errors)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_download_opts(destination, opts)
|
|
50
|
+
{
|
|
51
|
+
destination: destination,
|
|
52
|
+
ignore_failure: opts[:ignore_failure] || false
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_producer_opts(destination, bucket, opts)
|
|
57
|
+
{
|
|
58
|
+
client: @client,
|
|
59
|
+
directory_downloader: self,
|
|
60
|
+
destination: destination,
|
|
61
|
+
bucket: bucket,
|
|
62
|
+
s3_prefix: opts[:s3_prefix],
|
|
63
|
+
filter_callback: opts[:filter_callback],
|
|
64
|
+
request_callback: opts[:request_callback]
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_result(download_count, errors)
|
|
69
|
+
if @producer&.closed?
|
|
70
|
+
msg = "directory download failed: #{errors.map(&:message).join('; ')}"
|
|
71
|
+
raise DirectoryDownloadError.new(msg, errors)
|
|
72
|
+
else
|
|
73
|
+
{
|
|
74
|
+
completed_downloads: [download_count - errors.count, 0].max,
|
|
75
|
+
failed_downloads: errors.count,
|
|
76
|
+
errors: errors.any? ? errors : nil
|
|
77
|
+
}.compact
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def download_object(entry, downloader, errors, opts)
|
|
82
|
+
raise entry.error if entry.error
|
|
83
|
+
|
|
84
|
+
FileUtils.mkdir_p(File.dirname(entry.path)) unless Dir.exist?(File.dirname(entry.path))
|
|
85
|
+
downloader.download(entry.path, entry.params)
|
|
86
|
+
@logger&.debug("Downloaded #{entry.params[:key]} from #{entry.params[:bucket]} to #{entry.path}")
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
@logger&.warn("Failed to download #{entry.params[:key]} from #{entry.params[:bucket]}: #{e.message}")
|
|
89
|
+
@mutex.synchronize { errors << e }
|
|
90
|
+
abort unless opts[:ignore_failure]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def process_download_queue(downloader, opts)
|
|
94
|
+
queue_executor = DefaultExecutor.new(max_threads: 2)
|
|
95
|
+
completion_queue = Queue.new
|
|
96
|
+
posted_count = 0
|
|
97
|
+
errors = []
|
|
98
|
+
begin
|
|
99
|
+
@producer.each do |object|
|
|
100
|
+
queue_executor.post(object) do |o|
|
|
101
|
+
download_object(o, downloader, errors, opts)
|
|
102
|
+
ensure
|
|
103
|
+
completion_queue << :done
|
|
104
|
+
end
|
|
105
|
+
posted_count += 1
|
|
106
|
+
end
|
|
107
|
+
rescue ClosedQueueError
|
|
108
|
+
# abort already requested
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
@mutex.synchronize { errors << e }
|
|
111
|
+
abort
|
|
112
|
+
end
|
|
113
|
+
posted_count.times { completion_queue.pop }
|
|
114
|
+
[posted_count, errors]
|
|
115
|
+
ensure
|
|
116
|
+
queue_executor&.shutdown
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @api private
|
|
120
|
+
class ObjectProducer
|
|
121
|
+
include Enumerable
|
|
122
|
+
|
|
123
|
+
DEFAULT_QUEUE_SIZE = 100
|
|
124
|
+
DONE_MARKER = :done
|
|
125
|
+
|
|
126
|
+
def initialize(opts = {})
|
|
127
|
+
@directory_downloader = opts[:directory_downloader]
|
|
128
|
+
@destination_dir = opts[:destination]
|
|
129
|
+
@bucket = opts[:bucket]
|
|
130
|
+
@client = opts[:client]
|
|
131
|
+
@s3_prefix = opts[:s3_prefix]
|
|
132
|
+
@filter_callback = opts[:filter_callback]
|
|
133
|
+
@request_callback = opts[:request_callback]
|
|
134
|
+
@object_queue = SizedQueue.new(DEFAULT_QUEUE_SIZE)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def closed?
|
|
138
|
+
@object_queue.closed?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def close
|
|
142
|
+
@object_queue.close
|
|
143
|
+
@object_queue.clear
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def each
|
|
147
|
+
producer_thread = Thread.new do
|
|
148
|
+
stream_objects
|
|
149
|
+
@object_queue << DONE_MARKER
|
|
150
|
+
rescue ClosedQueueError
|
|
151
|
+
# abort requested
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
close
|
|
154
|
+
raise e
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
while (object = @object_queue.shift) && object != DONE_MARKER
|
|
158
|
+
yield object
|
|
159
|
+
end
|
|
160
|
+
ensure
|
|
161
|
+
producer_thread.value
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def apply_request_callback(key, params)
|
|
167
|
+
callback_params = @request_callback.call(key, params.dup)
|
|
168
|
+
return params unless callback_params.is_a?(Hash) && callback_params.any?
|
|
169
|
+
|
|
170
|
+
params.merge(callback_params)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def build_object_entry(key)
|
|
174
|
+
params = { bucket: @bucket, key: key }
|
|
175
|
+
params = apply_request_callback(key, params) if @request_callback
|
|
176
|
+
error = validate_key(key)
|
|
177
|
+
return DownloadEntry.new(path: '', params: params, error: error) if error
|
|
178
|
+
|
|
179
|
+
full_path = normalize_path(File.join(@destination_dir, key))
|
|
180
|
+
DownloadEntry.new(path: full_path, params: params, error: error)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def include_object?(obj)
|
|
184
|
+
return true unless @filter_callback
|
|
185
|
+
|
|
186
|
+
@filter_callback.call(obj)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def directory_marker?(obj)
|
|
190
|
+
obj.key.end_with?('/') && obj.size.zero?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def normalize_path(path)
|
|
194
|
+
return path if File::SEPARATOR == '/'
|
|
195
|
+
|
|
196
|
+
path.tr('/', File::SEPARATOR)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def stream_objects(continuation_token: nil)
|
|
200
|
+
resp = @client.list_objects_v2(bucket: @bucket, prefix: @s3_prefix, continuation_token: continuation_token)
|
|
201
|
+
resp.contents&.each do |o|
|
|
202
|
+
next if directory_marker?(o)
|
|
203
|
+
next unless include_object?(o)
|
|
204
|
+
|
|
205
|
+
@object_queue << build_object_entry(o.key)
|
|
206
|
+
end
|
|
207
|
+
stream_objects(continuation_token: resp.next_continuation_token) if resp.next_continuation_token
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def validate_key(key)
|
|
211
|
+
segments = key.split('/')
|
|
212
|
+
return unless segments.any? { |s| %w[. ..].include?(s) }
|
|
213
|
+
|
|
214
|
+
DirectoryDownloadError.new("invalid key '#{key}': contains '.' or '..' path segments")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @api private
|
|
218
|
+
class DownloadEntry
|
|
219
|
+
def initialize(opts = {})
|
|
220
|
+
@path = opts[:path]
|
|
221
|
+
@params = opts[:params]
|
|
222
|
+
@error = opts[:error]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
attr_reader :path, :params, :error
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module S3
|
|
5
|
+
# Raised when DirectoryUploader fails to upload files to S3 bucket
|
|
6
|
+
class DirectoryUploadError < StandardError
|
|
7
|
+
def initialize(message, errors = [])
|
|
8
|
+
@errors = errors
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Array<StandardError>] The list of errors encountered when uploading files
|
|
13
|
+
attr_reader :errors
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|