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.
@@ -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 :FilePart, 'aws-sdk-s3/file_part'
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