aws-sdk-s3 1.213.0 → 1.216.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 +17 -0
- data/VERSION +1 -1
- data/lib/aws-sdk-s3/bucket.rb +28 -0
- data/lib/aws-sdk-s3/client.rb +103 -56
- data/lib/aws-sdk-s3/client_api.rb +2 -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/resource.rb +28 -0
- data/lib/aws-sdk-s3/transfer_manager.rb +237 -18
- data/lib/aws-sdk-s3/types.rb +43 -7
- data/lib/aws-sdk-s3.rb +1 -1
- data/sig/bucket.rbs +2 -1
- data/sig/client.rbs +2 -1
- data/sig/resource.rbs +2 -1
- data/sig/types.rbs +1 -0
- metadata +7 -3
|
@@ -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
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module S3
|
|
7
|
+
# @api private
|
|
8
|
+
# This is a one-shot class that uploads files from a local directory to a bucket.
|
|
9
|
+
# This works as follows:
|
|
10
|
+
# * FileProducer runs in a background thread, scanning the directory and
|
|
11
|
+
# pushing entries into a SizedQueue (max: 100).
|
|
12
|
+
# * An internal executor pulls from that queue and posts work. Each task uses
|
|
13
|
+
# FileUploader to upload files then signals completion via `completion_queue`.
|
|
14
|
+
#
|
|
15
|
+
# We track how many tasks we posted, then pop that many times from `completion_queue`
|
|
16
|
+
# to wait for everything to finish.
|
|
17
|
+
#
|
|
18
|
+
# Errors are collected in a mutex-protected array. On failure (unless ignore_failure is set),
|
|
19
|
+
# we call abort which closes the queue - the producer catches ClosedQueueError and exits cleanly.
|
|
20
|
+
class DirectoryUploader
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
@client = options[:client] || Client.new
|
|
23
|
+
@executor = options[:executor] || DefaultExecutor.new
|
|
24
|
+
@logger = options[:logger]
|
|
25
|
+
@producer = nil
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :client, :executor
|
|
30
|
+
|
|
31
|
+
def abort
|
|
32
|
+
@producer&.close
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def upload(source_directory, bucket, **opts)
|
|
36
|
+
raise ArgumentError, 'Invalid directory' unless Dir.exist?(source_directory)
|
|
37
|
+
|
|
38
|
+
uploader = FileUploader.new(
|
|
39
|
+
multipart_threshold: opts.delete(:multipart_threshold),
|
|
40
|
+
http_chunk_size: opts.delete(:http_chunk_size),
|
|
41
|
+
client: @client,
|
|
42
|
+
executor: @executor
|
|
43
|
+
)
|
|
44
|
+
upload_opts = build_upload_opts(opts)
|
|
45
|
+
@producer = FileProducer.new(build_producer_opts(source_directory, bucket, opts))
|
|
46
|
+
uploads, errors = process_upload_queue(uploader, upload_opts)
|
|
47
|
+
build_result(uploads, errors)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_upload_opts(opts)
|
|
53
|
+
{ ignore_failure: opts[:ignore_failure] || false }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_producer_opts(source_directory, bucket, opts)
|
|
57
|
+
{
|
|
58
|
+
directory_uploader: self,
|
|
59
|
+
source_dir: source_directory,
|
|
60
|
+
bucket: bucket,
|
|
61
|
+
s3_prefix: opts[:s3_prefix],
|
|
62
|
+
recursive: opts[:recursive] || false,
|
|
63
|
+
follow_symlinks: opts[:follow_symlinks] || false,
|
|
64
|
+
filter_callback: opts[:filter_callback],
|
|
65
|
+
request_callback: opts[:request_callback]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_result(upload_count, errors)
|
|
70
|
+
if @producer&.closed?
|
|
71
|
+
msg = "directory upload failed: #{errors.map(&:message).join('; ')}"
|
|
72
|
+
raise DirectoryUploadError.new(msg, errors)
|
|
73
|
+
else
|
|
74
|
+
{
|
|
75
|
+
completed_uploads: [upload_count - errors.count, 0].max,
|
|
76
|
+
failed_uploads: errors.count,
|
|
77
|
+
errors: errors.any? ? errors : nil
|
|
78
|
+
}.compact
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def process_upload_queue(uploader, opts)
|
|
83
|
+
queue_executor = DefaultExecutor.new(max_threads: 2)
|
|
84
|
+
completion_queue = Queue.new
|
|
85
|
+
posted_count = 0
|
|
86
|
+
errors = []
|
|
87
|
+
begin
|
|
88
|
+
@producer.each do |file|
|
|
89
|
+
queue_executor.post(file) do |f|
|
|
90
|
+
upload_file(f, uploader, errors, opts)
|
|
91
|
+
ensure
|
|
92
|
+
completion_queue << :done
|
|
93
|
+
end
|
|
94
|
+
posted_count += 1
|
|
95
|
+
end
|
|
96
|
+
rescue ClosedQueueError
|
|
97
|
+
# abort already requested
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
@mutex.synchronize { errors << e }
|
|
100
|
+
abort
|
|
101
|
+
end
|
|
102
|
+
posted_count.times { completion_queue.pop }
|
|
103
|
+
[posted_count, errors]
|
|
104
|
+
ensure
|
|
105
|
+
queue_executor&.shutdown
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def upload_file(entry, uploader, errors, opts)
|
|
109
|
+
uploader.upload(entry.path, entry.params)
|
|
110
|
+
@logger&.debug("Uploaded #{entry.path} to #{entry.params[:bucket]} as #{entry.params[:key]}")
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
@logger&.warn("Failed to upload #{entry.path} to #{entry.params[:bucket]}: #{e.message}")
|
|
113
|
+
@mutex.synchronize { errors << e }
|
|
114
|
+
abort unless opts[:ignore_failure]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
class FileProducer
|
|
119
|
+
include Enumerable
|
|
120
|
+
|
|
121
|
+
DEFAULT_QUEUE_SIZE = 100
|
|
122
|
+
DONE_MARKER = :done
|
|
123
|
+
|
|
124
|
+
def initialize(opts = {})
|
|
125
|
+
@directory_uploader = opts[:directory_uploader]
|
|
126
|
+
@source_dir = opts[:source_dir]
|
|
127
|
+
@bucket = opts[:bucket]
|
|
128
|
+
@s3_prefix = opts[:s3_prefix]
|
|
129
|
+
@recursive = opts[:recursive]
|
|
130
|
+
@follow_symlinks = opts[:follow_symlinks]
|
|
131
|
+
@filter_callback = opts[:filter_callback]
|
|
132
|
+
@request_callback = opts[:request_callback]
|
|
133
|
+
@file_queue = SizedQueue.new(DEFAULT_QUEUE_SIZE)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def closed?
|
|
137
|
+
@file_queue.closed?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def close
|
|
141
|
+
@file_queue.close
|
|
142
|
+
@file_queue.clear
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def each
|
|
146
|
+
producer_thread = Thread.new do
|
|
147
|
+
if @recursive
|
|
148
|
+
find_recursively
|
|
149
|
+
else
|
|
150
|
+
find_directly
|
|
151
|
+
end
|
|
152
|
+
@file_queue << DONE_MARKER
|
|
153
|
+
rescue ClosedQueueError
|
|
154
|
+
# abort requested
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
# encountered a traversal error, we must abort immediately
|
|
157
|
+
close
|
|
158
|
+
raise DirectoryUploadError, "Directory traversal failed for '#{@source_dir}': #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
while (file = @file_queue.shift) && file != DONE_MARKER
|
|
162
|
+
yield file
|
|
163
|
+
end
|
|
164
|
+
ensure
|
|
165
|
+
producer_thread.value
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def apply_request_callback(file_path, params)
|
|
171
|
+
callback_params = @request_callback.call(file_path, params.dup)
|
|
172
|
+
return params unless callback_params.is_a?(Hash) && callback_params.any?
|
|
173
|
+
|
|
174
|
+
params.merge(callback_params)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def build_upload_entry(file_path, key)
|
|
178
|
+
params = { bucket: @bucket, key: @s3_prefix ? File.join(@s3_prefix, key) : key }
|
|
179
|
+
params = apply_request_callback(file_path, params) if @request_callback
|
|
180
|
+
UploadEntry.new(path: file_path, params: params)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def find_directly
|
|
184
|
+
Dir.each_child(@source_dir) do |entry|
|
|
185
|
+
entry_path = File.join(@source_dir, entry)
|
|
186
|
+
stat = nil
|
|
187
|
+
|
|
188
|
+
if @follow_symlinks
|
|
189
|
+
stat = File.stat(entry_path)
|
|
190
|
+
next if stat.directory?
|
|
191
|
+
else
|
|
192
|
+
stat = File.lstat(entry_path)
|
|
193
|
+
next if stat.symlink? || stat.directory?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
next unless stat.file?
|
|
197
|
+
next unless include_file?(entry_path, entry)
|
|
198
|
+
|
|
199
|
+
@file_queue << build_upload_entry(entry_path, entry)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def find_recursively
|
|
204
|
+
if @follow_symlinks
|
|
205
|
+
ancestors = Set.new
|
|
206
|
+
ancestors << File.stat(@source_dir).ino
|
|
207
|
+
scan_directory(@source_dir, ancestors: ancestors)
|
|
208
|
+
else
|
|
209
|
+
scan_directory(@source_dir)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def include_file?(file_path, file_name)
|
|
214
|
+
return true unless @filter_callback
|
|
215
|
+
|
|
216
|
+
@filter_callback.call(file_path, file_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def scan_directory(dir_path, key_prefix: '', ancestors: nil)
|
|
220
|
+
Dir.each_child(dir_path) do |entry|
|
|
221
|
+
full_path = File.join(dir_path, entry)
|
|
222
|
+
next unless include_file?(full_path, entry)
|
|
223
|
+
|
|
224
|
+
stat = get_file_stat(full_path)
|
|
225
|
+
next unless stat
|
|
226
|
+
|
|
227
|
+
if stat.directory?
|
|
228
|
+
handle_directory(full_path, entry, key_prefix, ancestors)
|
|
229
|
+
elsif stat.file? # skip non-file types
|
|
230
|
+
key = key_prefix.empty? ? entry : File.join(key_prefix, entry)
|
|
231
|
+
@file_queue << build_upload_entry(full_path, key)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def get_file_stat(full_path)
|
|
237
|
+
return File.stat(full_path) if @follow_symlinks
|
|
238
|
+
|
|
239
|
+
lstat = File.lstat(full_path)
|
|
240
|
+
return if lstat.symlink?
|
|
241
|
+
|
|
242
|
+
lstat
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def handle_directory(dir_path, dir_name, key_prefix, ancestors)
|
|
246
|
+
ino = nil
|
|
247
|
+
if @follow_symlinks && ancestors
|
|
248
|
+
ino = File.stat(dir_path).ino
|
|
249
|
+
return if ancestors.include?(ino) # cycle detected - skip
|
|
250
|
+
|
|
251
|
+
ancestors.add(ino)
|
|
252
|
+
end
|
|
253
|
+
new_prefix = key_prefix.empty? ? dir_name : File.join(key_prefix, dir_name)
|
|
254
|
+
scan_directory(dir_path, key_prefix: new_prefix, ancestors: ancestors)
|
|
255
|
+
ancestors.delete(ino) if @follow_symlinks && ancestors
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @api private
|
|
259
|
+
class UploadEntry
|
|
260
|
+
def initialize(opts = {})
|
|
261
|
+
@path = opts[:path]
|
|
262
|
+
@params = opts[:params]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
attr_reader :path, :params
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
data/lib/aws-sdk-s3/resource.rb
CHANGED
|
@@ -64,6 +64,7 @@ module Aws::S3
|
|
|
64
64
|
# grant_write_acp: "GrantWriteACP",
|
|
65
65
|
# object_lock_enabled_for_bucket: false,
|
|
66
66
|
# object_ownership: "BucketOwnerPreferred", # accepts BucketOwnerPreferred, ObjectWriter, BucketOwnerEnforced
|
|
67
|
+
# bucket_namespace: "account-regional", # accepts account-regional, global
|
|
67
68
|
# })
|
|
68
69
|
# @param [Hash] options ({})
|
|
69
70
|
# @option options [String] :acl
|
|
@@ -171,6 +172,33 @@ module Aws::S3
|
|
|
171
172
|
#
|
|
172
173
|
#
|
|
173
174
|
# [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html
|
|
175
|
+
# @option options [String] :bucket_namespace
|
|
176
|
+
# Specifies the namespace where you want to create your general purpose
|
|
177
|
+
# bucket. When you create a general purpose bucket, you can choose to
|
|
178
|
+
# create a bucket in the shared global namespace or you can choose to
|
|
179
|
+
# create a bucket in your account regional namespace. Your account
|
|
180
|
+
# regional namespace is a subdivision of the global namespace that only
|
|
181
|
+
# your account can create buckets in. For more information on bucket
|
|
182
|
+
# namespaces, see [Namespaces for general purpose buckets][1].
|
|
183
|
+
#
|
|
184
|
+
# General purpose buckets in your account regional namespace must follow
|
|
185
|
+
# a specific naming convention. These buckets consist of a bucket name
|
|
186
|
+
# prefix that you create, and a suffix that contains your 12-digit
|
|
187
|
+
# Amazon Web Services Account ID, the Amazon Web Services Region code,
|
|
188
|
+
# and ends with `-an`. Bucket names must follow the format
|
|
189
|
+
# `bucket-name-prefix-accountId-region-an` (for example,
|
|
190
|
+
# `amzn-s3-demo-bucket-111122223333-us-west-2-an`). For information
|
|
191
|
+
# about bucket naming restrictions, see [Account regional namespace
|
|
192
|
+
# naming rules][2] in the *Amazon S3 User Guide*.
|
|
193
|
+
#
|
|
194
|
+
# <note markdown="1"> This functionality is not supported for directory buckets.
|
|
195
|
+
#
|
|
196
|
+
# </note>
|
|
197
|
+
#
|
|
198
|
+
#
|
|
199
|
+
#
|
|
200
|
+
# [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html
|
|
201
|
+
# [2]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html#account-regional-naming-rules
|
|
174
202
|
# @return [Bucket]
|
|
175
203
|
def create_bucket(options = {})
|
|
176
204
|
Aws::Plugins::UserAgent.metric('RESOURCE_MODEL') do
|