vagrant-s3-multidownloader 0.3.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 +7 -0
- data/lib/vagrant-s3-multidownloader/downloader.rb +488 -0
- data/lib/vagrant-s3-multidownloader/metadata_handler.rb +187 -0
- data/lib/vagrant-s3-multidownloader/middleware/handle_metadata_versions.rb +87 -0
- data/lib/vagrant-s3-multidownloader/middleware/handle_s3_urls.rb +106 -0
- data/lib/vagrant-s3-multidownloader/plugin.rb +40 -0
- data/lib/vagrant-s3-multidownloader.rb +31 -0
- data/vagrant-s3-multidownloader.gemspec +12 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c4319db5427dee9a2ddaeb79f3494e2847bf9cdb261985a82a74fe86c462abc8
|
4
|
+
data.tar.gz: f33293ee80907e491d1d8c3c61c60984e0e7df3d9cea370d01a916e42d78da14
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ef026874b3e549060199c63ff83f88e3dbe8af4d348b6084f95c626500145ebecf71eb7070a610b24b9f9965a2cd0f17879edfa24fa7b268fe09797d0f8ffaba
|
7
|
+
data.tar.gz: c400a6303e0d084b984c0ac4f14617bf22aa639a0282fedd9246e4af4e665e2e27aafa4f08a874a7cb7ca0303f57e3509a6df01e7cfeab35b2679eedda1dad94
|
@@ -0,0 +1,488 @@
|
|
1
|
+
require 'vagrant'
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
require 'uri'
|
4
|
+
require 'yaml'
|
5
|
+
require 'thread'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'json'
|
8
|
+
require 'tmpdir'
|
9
|
+
require_relative 'metadata_handler'
|
10
|
+
|
11
|
+
module VagrantPlugins
|
12
|
+
module S3MultiDownloader
|
13
|
+
class Downloader
|
14
|
+
|
15
|
+
def self.patch_vagrant_downloader
|
16
|
+
::Vagrant::Util::Downloader.class_eval do
|
17
|
+
alias_method :original_download!, :download! unless method_defined?(:original_download!)
|
18
|
+
|
19
|
+
def download!
|
20
|
+
if @source.to_s.start_with?("s3://")
|
21
|
+
logger = Log4r::Logger.new("vagrant::downloaders::s3")
|
22
|
+
logger.info("Detected S3 URL: #{@source}")
|
23
|
+
|
24
|
+
# Use our custom S3 downloader
|
25
|
+
downloader = VagrantPlugins::S3MultiD ownloader::Downloader.new(
|
26
|
+
@source, @destination, @ui, continue: @continue, headers: @headers
|
27
|
+
)
|
28
|
+
return downloader.download!
|
29
|
+
end
|
30
|
+
original_download!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Automatically patch the downloader when this module is loaded
|
36
|
+
self.patch_vagrant_downloader
|
37
|
+
|
38
|
+
def initialize(source, destination, ui, options = {})
|
39
|
+
@source = source
|
40
|
+
@destination = destination
|
41
|
+
@ui = ui
|
42
|
+
@options = options
|
43
|
+
@env = options[:env]
|
44
|
+
@logger = Log4r::Logger.new("vagrant::s3-multidownloader")
|
45
|
+
@metadata_handler = MetadataHandler.new(ui)
|
46
|
+
|
47
|
+
@logger.info("S3MultiDownloader initialized")
|
48
|
+
@logger.info("Source: #{@source}")
|
49
|
+
@logger.info("Destination: #{@destination}")
|
50
|
+
end
|
51
|
+
|
52
|
+
def download!
|
53
|
+
# Parse the S3 URL
|
54
|
+
uri, bucket, key = parse_s3_url(@source)
|
55
|
+
|
56
|
+
# Log download information
|
57
|
+
log_download_info(bucket, key)
|
58
|
+
|
59
|
+
# Create S3 client
|
60
|
+
client = create_s3_client
|
61
|
+
|
62
|
+
begin
|
63
|
+
# Check for requested version from Vagrantfile
|
64
|
+
requested_version = @metadata_handler.get_requested_version(@env)
|
65
|
+
log_message(:info, "Box version requested: #{requested_version || 'latest'}") if requested_version
|
66
|
+
|
67
|
+
# Check if this is a metadata.json file
|
68
|
+
is_metadata = key.end_with?("metadata.json")
|
69
|
+
log_message(:detail, "File type: #{is_metadata ? 'metadata.json' : 'box file'}")
|
70
|
+
|
71
|
+
if is_metadata
|
72
|
+
# Handle metadata.json specially
|
73
|
+
handle_metadata_file(client, bucket, key, requested_version)
|
74
|
+
else
|
75
|
+
# Direct box download
|
76
|
+
download_s3_object(client, bucket, key)
|
77
|
+
log_message(:success, "Successfully downloaded #{@source} to #{@destination}")
|
78
|
+
end
|
79
|
+
rescue Aws::S3::Errors::Forbidden => e
|
80
|
+
handle_forbidden_error(e)
|
81
|
+
rescue Aws::S3::Errors::NotFound => e
|
82
|
+
handle_not_found_error(e, bucket, key)
|
83
|
+
rescue Aws::S3::Errors::ServiceError => e
|
84
|
+
handle_service_error(e)
|
85
|
+
rescue StandardError => e
|
86
|
+
handle_standard_error(e)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def parse_s3_url(url)
|
93
|
+
uri = URI.parse(url)
|
94
|
+
bucket = uri.host
|
95
|
+
key = uri.path[1..-1]
|
96
|
+
return uri, bucket, key
|
97
|
+
end
|
98
|
+
|
99
|
+
def s3_url_is_metadata?(url)
|
100
|
+
return false unless url
|
101
|
+
uri = URI.parse(url)
|
102
|
+
path = uri.path
|
103
|
+
path.end_with?('metadata.json')
|
104
|
+
end
|
105
|
+
|
106
|
+
def log_download_info(bucket, key)
|
107
|
+
log_message(:info, "Starting download from S3: #{@source}")
|
108
|
+
log_message(:detail, "Bucket: #{bucket}")
|
109
|
+
log_message(:detail, "Key: #{key}")
|
110
|
+
|
111
|
+
# Log headers if available for debugging
|
112
|
+
if @options[:headers]
|
113
|
+
@logger.debug("Request headers: #{@options[:headers].inspect}")
|
114
|
+
if @options[:headers].is_a?(Hash) && @options[:headers]["User-Agent"]
|
115
|
+
log_message(:detail, "User agent: #{@options[:headers]["User-Agent"]}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_s3_client
|
121
|
+
# Load AWS config (credentials, region)
|
122
|
+
aws_config = load_aws_config
|
123
|
+
profile = aws_config[:profile]
|
124
|
+
log_message(:detail, "AWS config loaded with region: #{aws_config[:region]}")
|
125
|
+
log_message(:detail, "Credentials available: #{!aws_config[:access_key_id].nil?}")
|
126
|
+
|
127
|
+
# Make sure the destination directory exists
|
128
|
+
FileUtils.mkdir_p(File.dirname(@destination))
|
129
|
+
|
130
|
+
# Initialize client options
|
131
|
+
client_options = {
|
132
|
+
force_path_style: true, # Required for most S3-compatible storage systems
|
133
|
+
ssl_verify_peer: false # Don't verify SSL certificates
|
134
|
+
}
|
135
|
+
|
136
|
+
# Get endpoint URL from AWS config file
|
137
|
+
endpoint_url = find_endpoint_url(profile)
|
138
|
+
|
139
|
+
# If not found in config file, check environment as fallback
|
140
|
+
if endpoint_url.nil? && ENV['AWS_ENDPOINT_URL']
|
141
|
+
endpoint_url = ENV['AWS_ENDPOINT_URL']
|
142
|
+
log_message(:detail, "Using S3 endpoint from environment: #{endpoint_url}")
|
143
|
+
end
|
144
|
+
|
145
|
+
if endpoint_url
|
146
|
+
log_message(:info, "Using S3 endpoint URL: #{endpoint_url}")
|
147
|
+
# Add endpoint URL to client options
|
148
|
+
client_options[:endpoint] = endpoint_url
|
149
|
+
else
|
150
|
+
log_message(:detail, "No endpoint URL configured, will use AWS default endpoints")
|
151
|
+
end
|
152
|
+
|
153
|
+
log_message(:info, "Attempting to download actual box from S3")
|
154
|
+
|
155
|
+
# Setup credentials from config files first, not from environment
|
156
|
+
if aws_config[:access_key_id] && aws_config[:secret_access_key]
|
157
|
+
# Use explicit credentials from config files
|
158
|
+
client_options[:credentials] = Aws::Credentials.new(
|
159
|
+
aws_config[:access_key_id],
|
160
|
+
aws_config[:secret_access_key]
|
161
|
+
)
|
162
|
+
log_message(:detail, "Using credentials from config files")
|
163
|
+
elsif profile && !profile.empty?
|
164
|
+
# Try to use profile directly through SharedCredentials
|
165
|
+
begin
|
166
|
+
credentials = Aws::SharedCredentials.new(profile_name: profile)
|
167
|
+
client_options[:credentials] = credentials
|
168
|
+
log_message(:detail, "Using AWS shared credentials from profile: #{profile}")
|
169
|
+
rescue Aws::Errors::NoSuchProfileError => e
|
170
|
+
log_message(:warn, "AWS profile '#{profile}' not found in credentials file: #{e.message}")
|
171
|
+
log_message(:warn, "Falling back to default credential provider chain")
|
172
|
+
end
|
173
|
+
else
|
174
|
+
log_message(:detail, "No specific credentials provided, falling back to default credential provider chain")
|
175
|
+
# Let the AWS SDK use its default credential provider chain
|
176
|
+
end
|
177
|
+
|
178
|
+
# Ensure we have a region set (required by AWS SDK)
|
179
|
+
client_options[:region] = aws_config[:region] || 'us-east-1'
|
180
|
+
|
181
|
+
log_message(:detail, "Creating S3 client with profile credentials and options: #{client_options.inspect}")
|
182
|
+
Aws::S3::Client.new(client_options)
|
183
|
+
end
|
184
|
+
|
185
|
+
def download_s3_object(client, bucket, key, destination = nil)
|
186
|
+
dest = destination || @destination
|
187
|
+
log_message(:info, "Downloading #{bucket}/#{key} using single thread")
|
188
|
+
|
189
|
+
begin
|
190
|
+
# Make sure the destination directory exists
|
191
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
192
|
+
|
193
|
+
# Get the entire object
|
194
|
+
response = client.get_object(bucket: bucket, key: key)
|
195
|
+
|
196
|
+
# Write the file to destination
|
197
|
+
File.open(dest, 'wb') do |file|
|
198
|
+
file.write(response.body.read)
|
199
|
+
end
|
200
|
+
|
201
|
+
log_message(:detail, "Download completed")
|
202
|
+
rescue StandardError => e
|
203
|
+
log_message(:error, "Download error: #{e.message}")
|
204
|
+
raise e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def handle_metadata_file(client, bucket, key, requested_version = nil)
|
209
|
+
log_message(:info, "Processing metadata.json file for versioning")
|
210
|
+
|
211
|
+
# First download the metadata file
|
212
|
+
download_s3_object(client, bucket, key)
|
213
|
+
|
214
|
+
# Process the metadata file
|
215
|
+
if File.exist?(@destination)
|
216
|
+
metadata = @metadata_handler.process_metadata_file(@destination)
|
217
|
+
|
218
|
+
if metadata
|
219
|
+
# Find the requested or latest version
|
220
|
+
version_data = @metadata_handler.find_version_in_metadata(metadata, requested_version)
|
221
|
+
|
222
|
+
if version_data
|
223
|
+
# Find the provider URL
|
224
|
+
provider_name = @env&.dig(:machine)&.provider&.name&.to_s
|
225
|
+
provider_url = @metadata_handler.find_provider_url(version_data, provider_name)
|
226
|
+
|
227
|
+
if provider_url && provider_url.start_with?('s3://')
|
228
|
+
log_message(:info, "Found box URL for version #{version_data['version']}: #{provider_url}")
|
229
|
+
|
230
|
+
# Create a temp path for the box file
|
231
|
+
box_temp_path = File.join(Dir.tmpdir, "vagrant_box_#{Time.now.to_i}")
|
232
|
+
|
233
|
+
# Download the actual box
|
234
|
+
box_uri, box_bucket, box_key = parse_s3_url(provider_url)
|
235
|
+
|
236
|
+
log_message(:info, "Downloading box from: #{provider_url}")
|
237
|
+
download_s3_object(client, box_bucket, box_key, box_temp_path)
|
238
|
+
|
239
|
+
# Copy the downloaded box to the destination
|
240
|
+
FileUtils.cp(box_temp_path, @destination)
|
241
|
+
FileUtils.rm(box_temp_path) if File.exist?(box_temp_path)
|
242
|
+
|
243
|
+
log_message(:success, "Successfully downloaded box version #{version_data['version']} to #{@destination}")
|
244
|
+
else
|
245
|
+
log_message(:warn, "No S3 URL found for provider #{provider_name || 'any'} in version #{version_data['version']}")
|
246
|
+
end
|
247
|
+
else
|
248
|
+
log_message(:warn, "No suitable version found in metadata.json")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
else
|
252
|
+
log_message(:error, "Failed to download metadata.json file")
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def process_metadata_file(metadata_path)
|
257
|
+
@metadata_handler.process_metadata_file(metadata_path)
|
258
|
+
end
|
259
|
+
|
260
|
+
def handle_forbidden_error(error)
|
261
|
+
log_message(:error, "S3 access forbidden. Check your AWS credentials.")
|
262
|
+
log_message(:error, "Error details: #{error.message}")
|
263
|
+
raise Vagrant::Errors::DownloaderError.new(message: "S3 access forbidden: #{error.message}. Please check your AWS credentials.")
|
264
|
+
end
|
265
|
+
|
266
|
+
def handle_not_found_error(error, bucket, key)
|
267
|
+
log_message(:error, "S3 file not found: #{error.message}")
|
268
|
+
raise Vagrant::Errors::DownloaderError.new(message: "S3 file not found: #{bucket}/#{key}. Please check the S3 URL.")
|
269
|
+
end
|
270
|
+
|
271
|
+
def handle_service_error(error)
|
272
|
+
log_message(:error, "S3 error: #{error.message}")
|
273
|
+
raise Vagrant::Errors::DownloaderError.new(message: "S3 error: #{error.message}")
|
274
|
+
end
|
275
|
+
|
276
|
+
def handle_standard_error(error)
|
277
|
+
log_message(:error, "Error: #{error.class.name} - #{error.message}")
|
278
|
+
raise Vagrant::Errors::DownloaderError.new(message: "Download error: #{error.message}")
|
279
|
+
end
|
280
|
+
|
281
|
+
# Helper method to log messages to UI or logger
|
282
|
+
def log_message(level, message)
|
283
|
+
if @ui.respond_to?(level)
|
284
|
+
@ui.send(level, message)
|
285
|
+
else
|
286
|
+
# Default to using the logger with an appropriate log level
|
287
|
+
case level
|
288
|
+
when :detail
|
289
|
+
@logger.info(message)
|
290
|
+
when :success
|
291
|
+
@logger.info("SUCCESS: #{message}")
|
292
|
+
when :warn
|
293
|
+
@logger.warn(message)
|
294
|
+
when :error
|
295
|
+
@logger.error(message)
|
296
|
+
else
|
297
|
+
@logger.send(level, message)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def load_aws_config
|
303
|
+
aws_config_file = File.expand_path("~/.aws/config")
|
304
|
+
aws_credentials_file = File.expand_path("~/.aws/credentials")
|
305
|
+
|
306
|
+
# Initialize with empty config
|
307
|
+
config = { region: "default" }
|
308
|
+
|
309
|
+
# Get profile from environment variable or use default
|
310
|
+
profile = ENV['AWS_PROFILE'] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
|
311
|
+
config[:profile] = profile
|
312
|
+
|
313
|
+
log_message(:info, "Loading AWS credentials from profile: #{profile}")
|
314
|
+
|
315
|
+
# Try to load credentials from AWS credentials file first
|
316
|
+
if File.exist?(aws_credentials_file)
|
317
|
+
begin
|
318
|
+
# Parse AWS credentials file (INI format)
|
319
|
+
credentials_content = File.read(aws_credentials_file)
|
320
|
+
section = 'default'
|
321
|
+
credentials = {}
|
322
|
+
|
323
|
+
credentials_content.each_line do |line|
|
324
|
+
line = line.strip
|
325
|
+
next if line.empty? || line.start_with?('#')
|
326
|
+
|
327
|
+
if line =~ /^\[([^\]]+)\]/
|
328
|
+
section = $1
|
329
|
+
credentials[section] ||= {}
|
330
|
+
elsif line =~ /^([^=]+)=(.*)$/
|
331
|
+
key = $1.strip
|
332
|
+
value = $2.strip
|
333
|
+
credentials[section] ||= {}
|
334
|
+
credentials[section][key] = value
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# First try to load the specified profile, then fall back to default
|
339
|
+
if credentials[profile]
|
340
|
+
log_message(:detail, "Found credentials for profile: #{profile}")
|
341
|
+
config[:access_key_id] = credentials[profile]['aws_access_key_id']
|
342
|
+
config[:secret_access_key] = credentials[profile]['aws_secret_access_key']
|
343
|
+
elsif credentials['default'] && profile != 'default'
|
344
|
+
log_message(:detail, "Profile #{profile} not found, using default profile")
|
345
|
+
config[:access_key_id] = credentials['default']['aws_access_key_id']
|
346
|
+
config[:secret_access_key] = credentials['default']['aws_secret_access_key']
|
347
|
+
end
|
348
|
+
rescue => e
|
349
|
+
# Log the error but continue
|
350
|
+
log_message(:warn, "Error reading AWS credentials file: #{e.message}")
|
351
|
+
end
|
352
|
+
else
|
353
|
+
log_message(:detail, "AWS credentials file not found at #{aws_credentials_file}")
|
354
|
+
end
|
355
|
+
|
356
|
+
# Fall back to environment variables if no credentials found in file
|
357
|
+
if config[:access_key_id].nil? && ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY']
|
358
|
+
log_message(:info, "Using AWS credentials from environment variables")
|
359
|
+
config[:access_key_id] = ENV['AWS_ACCESS_KEY_ID']
|
360
|
+
config[:secret_access_key] = ENV['AWS_SECRET_ACCESS_KEY']
|
361
|
+
end
|
362
|
+
|
363
|
+
# Load region from config file first
|
364
|
+
if File.exist?(aws_config_file)
|
365
|
+
begin
|
366
|
+
# Parse AWS config file (INI format)
|
367
|
+
config_content = File.read(aws_config_file)
|
368
|
+
section = 'default'
|
369
|
+
aws_configs = {}
|
370
|
+
|
371
|
+
config_content.each_line do |line|
|
372
|
+
line = line.strip
|
373
|
+
next if line.empty? || line.start_with?('#')
|
374
|
+
|
375
|
+
if line =~ /^\[([^\]]+)\]/
|
376
|
+
section = $1
|
377
|
+
# Handle "profile" prefix in config file
|
378
|
+
if section.start_with?('profile ')
|
379
|
+
section = section.sub('profile ', '')
|
380
|
+
end
|
381
|
+
aws_configs[section] ||= {}
|
382
|
+
elsif line =~ /^([^=]+)=(.*)$/
|
383
|
+
key = $1.strip
|
384
|
+
value = $2.strip
|
385
|
+
aws_configs[section] ||= {}
|
386
|
+
aws_configs[section][key] = value
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Try to get region from the specified profile first
|
391
|
+
if aws_configs[profile] && aws_configs[profile]['region']
|
392
|
+
config[:region] = aws_configs[profile]['region']
|
393
|
+
log_message(:detail, "Using region '#{config[:region]}' from profile '#{profile}'")
|
394
|
+
# Then try the default profile
|
395
|
+
elsif aws_configs['default'] && aws_configs['default']['region']
|
396
|
+
config[:region] = aws_configs['default']['region']
|
397
|
+
log_message(:detail, "Using region '#{config[:region]}' from default profile")
|
398
|
+
end
|
399
|
+
rescue => e
|
400
|
+
# Log the error but continue
|
401
|
+
log_message(:warn, "Error reading AWS config file: #{e.message}")
|
402
|
+
end
|
403
|
+
else
|
404
|
+
log_message(:detail, "AWS config file not found at #{aws_config_file}")
|
405
|
+
end
|
406
|
+
|
407
|
+
# Fall back to environment variable if no region in config file
|
408
|
+
if config[:region].nil? && ENV['AWS_REGION']
|
409
|
+
config[:region] = ENV['AWS_REGION']
|
410
|
+
log_message(:detail, "Using region from environment: #{config[:region]}")
|
411
|
+
end
|
412
|
+
|
413
|
+
# Default to us-east-1 if no region specified
|
414
|
+
config[:region] ||= "us-east-1"
|
415
|
+
log_message(:info, "Using AWS region: #{config[:region]}")
|
416
|
+
|
417
|
+
config
|
418
|
+
end
|
419
|
+
|
420
|
+
def find_endpoint_url(profile)
|
421
|
+
aws_config_file = File.expand_path("~/.aws/config")
|
422
|
+
|
423
|
+
if File.exist?(aws_config_file)
|
424
|
+
begin
|
425
|
+
# Parse AWS config file (INI format)
|
426
|
+
config_content = File.read(aws_config_file)
|
427
|
+
section = 'default'
|
428
|
+
aws_configs = {}
|
429
|
+
|
430
|
+
config_content.each_line do |line|
|
431
|
+
line = line.strip
|
432
|
+
next if line.empty? || line.start_with?('#')
|
433
|
+
|
434
|
+
if line =~ /^\[([^\]]+)\]/
|
435
|
+
section = $1
|
436
|
+
# Handle "profile" prefix in config file
|
437
|
+
if section.start_with?('profile ')
|
438
|
+
section = section.sub('profile ', '')
|
439
|
+
end
|
440
|
+
aws_configs[section] ||= {}
|
441
|
+
elsif line =~ /^([^=]+)=(.*)$/
|
442
|
+
key = $1.strip
|
443
|
+
value = $2.strip
|
444
|
+
aws_configs[section] ||= {}
|
445
|
+
aws_configs[section][key] = value
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Check for endpoint_url or endpoint in the specified profile
|
450
|
+
if aws_configs[profile]
|
451
|
+
endpoint = aws_configs[profile]['endpoint_url'] ||
|
452
|
+
aws_configs[profile]['endpoint'] ||
|
453
|
+
aws_configs[profile]['s3_endpoint_url'] ||
|
454
|
+
aws_configs[profile]['s3_endpoint']
|
455
|
+
|
456
|
+
if endpoint
|
457
|
+
log_message(:detail, "Found endpoint URL in profile '#{profile}': #{endpoint}")
|
458
|
+
return endpoint
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Check default profile if no endpoint found in specified profile
|
463
|
+
if aws_configs['default'] && profile != 'default'
|
464
|
+
endpoint = aws_configs['default']['endpoint_url'] ||
|
465
|
+
aws_configs['default']['endpoint'] ||
|
466
|
+
aws_configs['default']['s3_endpoint_url'] ||
|
467
|
+
aws_configs['default']['s3_endpoint']
|
468
|
+
|
469
|
+
if endpoint
|
470
|
+
log_message(:detail, "Found endpoint URL in default profile: #{endpoint}")
|
471
|
+
return endpoint
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
log_message(:detail, "No endpoint URL found in AWS config profiles")
|
476
|
+
return nil
|
477
|
+
rescue => e
|
478
|
+
log_message(:warn, "Error reading AWS config file for endpoint: #{e.message}")
|
479
|
+
return nil
|
480
|
+
end
|
481
|
+
else
|
482
|
+
log_message(:detail, "AWS config file not found at #{aws_config_file}")
|
483
|
+
return nil
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'vagrant/util/downloader'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module VagrantPlugins
|
6
|
+
module S3MultiDownloader
|
7
|
+
class MetadataHandler
|
8
|
+
attr_reader :ui, :logger
|
9
|
+
|
10
|
+
def initialize(ui)
|
11
|
+
@ui = ui
|
12
|
+
@logger = Log4r::Logger.new("vagrant::s3-multidownloader::metadata")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Extract version from the environment if available
|
16
|
+
def get_requested_version(env)
|
17
|
+
return nil if !env || !env[:machine] || !env[:machine].config || !env[:machine].config.vm
|
18
|
+
|
19
|
+
if env[:machine].config.vm.box_version
|
20
|
+
version = env[:machine].config.vm.box_version
|
21
|
+
@ui.detail("Found box_version in Vagrantfile: #{version}")
|
22
|
+
return version
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check if the URL points to a metadata.json file
|
29
|
+
def metadata_url?(url)
|
30
|
+
return false unless url
|
31
|
+
url.to_s.end_with?('metadata.json')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Download and parse a metadata.json file
|
35
|
+
def download_metadata(url, destination, client, bucket, key)
|
36
|
+
@ui.info("Downloading metadata from S3: #{url}")
|
37
|
+
|
38
|
+
# Use the client directly to download the metadata file
|
39
|
+
client.get_object(
|
40
|
+
response_target: destination,
|
41
|
+
bucket: bucket,
|
42
|
+
key: key
|
43
|
+
)
|
44
|
+
|
45
|
+
# Parse the metadata
|
46
|
+
begin
|
47
|
+
metadata = JSON.parse(File.read(destination))
|
48
|
+
return metadata
|
49
|
+
rescue JSON::ParserError => e
|
50
|
+
@ui.error("Failed to parse metadata.json: #{e.message}")
|
51
|
+
return nil
|
52
|
+
rescue StandardError => e
|
53
|
+
@ui.error("Error processing metadata.json: #{e.message}")
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Find a specific version in the metadata
|
59
|
+
def find_version_in_metadata(metadata, version = nil)
|
60
|
+
return nil unless metadata && metadata.key?('versions') && metadata['versions'].is_a?(Array)
|
61
|
+
|
62
|
+
available_versions = metadata['versions']
|
63
|
+
@ui.detail("Available versions: #{available_versions.map { |v| v['version'] }.join(', ')}")
|
64
|
+
|
65
|
+
if version
|
66
|
+
# This handles both exact version number and version constraints
|
67
|
+
if version.include?('>') || version.include?('<') || version.include?('~>')
|
68
|
+
@ui.detail("Detected version constraint: #{version}")
|
69
|
+
# Parse the constraint and find the highest satisfying version
|
70
|
+
begin
|
71
|
+
require 'vagrant/util/template_renderer'
|
72
|
+
version_constraint = Gem::Requirement.new(version)
|
73
|
+
|
74
|
+
# Find all versions that satisfy the constraint
|
75
|
+
satisfying_versions = available_versions.select do |v|
|
76
|
+
begin
|
77
|
+
version_number = Gem::Version.new(v['version'])
|
78
|
+
version_constraint.satisfied_by?(version_number)
|
79
|
+
rescue
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if satisfying_versions.empty?
|
85
|
+
@ui.warn("No versions matching constraint #{version} found in metadata")
|
86
|
+
return nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Select the highest version that satisfies the constraint
|
90
|
+
selected_version = satisfying_versions.sort_by { |v| Gem::Version.new(v['version']) rescue Gem::Version.new('0.0.0') }.last
|
91
|
+
@ui.success("Selected version #{selected_version['version']} for constraint #{version}")
|
92
|
+
return selected_version
|
93
|
+
rescue => e
|
94
|
+
@ui.error("Error parsing version constraint: #{e.message}")
|
95
|
+
# Fall back to exact match
|
96
|
+
@ui.warn("Falling back to exact version match")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find the requested version (exact match)
|
101
|
+
requested_version = available_versions.find { |v| v['version'] == version }
|
102
|
+
if requested_version
|
103
|
+
@ui.success("Found requested version: #{version}")
|
104
|
+
return requested_version
|
105
|
+
else
|
106
|
+
@ui.warn("Requested version #{version} not found in metadata")
|
107
|
+
return nil
|
108
|
+
end
|
109
|
+
else
|
110
|
+
# No specific version requested, use the latest version
|
111
|
+
latest_version = available_versions.sort_by { |v| Gem::Version.new(v['version']) rescue Gem::Version.new('0.0.0') }.last
|
112
|
+
@ui.detail("Using latest version: #{latest_version['version']}")
|
113
|
+
return latest_version
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Find the provider URL in a version
|
118
|
+
def find_provider_url(version_data, provider_name = nil)
|
119
|
+
return nil unless version_data && version_data.key?('providers') && version_data['providers'].is_a?(Array)
|
120
|
+
|
121
|
+
providers = version_data['providers']
|
122
|
+
|
123
|
+
# If provider_name is specified, find that specific provider
|
124
|
+
if provider_name && !provider_name.empty?
|
125
|
+
provider = providers.find { |p| p['name'] == provider_name }
|
126
|
+
if provider && provider.key?('url')
|
127
|
+
@ui.detail("Found URL for provider #{provider_name}: #{provider['url']}")
|
128
|
+
return provider['url']
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Otherwise, just return the first provider with a URL
|
133
|
+
providers.each do |provider|
|
134
|
+
if provider.key?('url')
|
135
|
+
@ui.detail("Using provider #{provider['name']}: #{provider['url']}")
|
136
|
+
return provider['url']
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
143
|
+
# Process a metadata file to ensure all S3 URLs are properly formatted
|
144
|
+
def process_metadata_file(metadata_path)
|
145
|
+
@ui.info("Processing metadata.json file for S3 URLs")
|
146
|
+
begin
|
147
|
+
# Read and parse the metadata.json file
|
148
|
+
metadata = JSON.parse(File.read(metadata_path))
|
149
|
+
|
150
|
+
# Check if the metadata has versions
|
151
|
+
if metadata.key?('versions') && metadata['versions'].is_a?(Array)
|
152
|
+
@ui.detail("Found #{metadata['versions'].size} versions in metadata")
|
153
|
+
|
154
|
+
# Update all S3 URLs in the metadata to ensure they'll be handled correctly
|
155
|
+
metadata['versions'].each_with_index do |version, index|
|
156
|
+
if version.key?('providers') && version['providers'].is_a?(Array)
|
157
|
+
version['providers'].each do |provider|
|
158
|
+
if provider.key?('url') && provider['url'].to_s.start_with?('s3://')
|
159
|
+
@ui.detail("Found S3 URL in metadata: #{provider['url']}")
|
160
|
+
# No need to modify the URL as the plugin already handles s3:// URLs
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Write the updated metadata back to the file
|
167
|
+
File.open(metadata_path, 'w') do |file|
|
168
|
+
file.write(JSON.pretty_generate(metadata))
|
169
|
+
end
|
170
|
+
|
171
|
+
@ui.detail("Metadata processing complete")
|
172
|
+
return metadata
|
173
|
+
else
|
174
|
+
@ui.detail("No versions found in metadata.json")
|
175
|
+
return nil
|
176
|
+
end
|
177
|
+
rescue JSON::ParserError => e
|
178
|
+
@ui.error("Failed to parse metadata.json: #{e.message}")
|
179
|
+
return nil
|
180
|
+
rescue StandardError => e
|
181
|
+
@ui.error("Error processing metadata.json: #{e.message}")
|
182
|
+
return nil
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'vagrant'
|
2
|
+
require 'vagrant/action/builtin/box_add'
|
3
|
+
|
4
|
+
module VagrantPlugins
|
5
|
+
module S3MultiDownloader
|
6
|
+
class HandleMetadataVersions
|
7
|
+
def initialize(app, env)
|
8
|
+
@app = app
|
9
|
+
@env = env
|
10
|
+
@logger = Log4r::Logger.new("vagrant::s3-multidownloader::metadata_versions")
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@env = env
|
15
|
+
|
16
|
+
# Patch Vagrant's box add action to recognize S3 URLs as metadata URLs
|
17
|
+
if !defined? @@patched_box_add
|
18
|
+
patch_box_add_action
|
19
|
+
@@patched_box_add = true
|
20
|
+
end
|
21
|
+
|
22
|
+
@app.call(env)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def patch_box_add_action
|
28
|
+
@logger.info("Patching Vagrant::Action::Builtin::BoxAdd to support S3 metadata URLs")
|
29
|
+
|
30
|
+
# Override the method that determines if a URL is a metadata URL
|
31
|
+
if Vagrant::Action::Builtin::BoxAdd.instance_methods.include?(:metadata_url?)
|
32
|
+
Vagrant::Action::Builtin::BoxAdd.class_eval do
|
33
|
+
alias_method :original_metadata_url?, :metadata_url?
|
34
|
+
|
35
|
+
def metadata_url?(url, env=nil)
|
36
|
+
if url.to_s.start_with?("s3://")
|
37
|
+
# For S3 URLs, consider them as metadata URLs if they end with metadata.json
|
38
|
+
# or if they don't have a file extension (implying they might be a base URL)
|
39
|
+
path = URI.parse(url.to_s).path
|
40
|
+
return true if path.end_with?("metadata.json")
|
41
|
+
return true if File.extname(path).empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Otherwise use the original logic
|
45
|
+
original_metadata_url?(url, env)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Override the check that prevents version constraints with direct file URLs
|
51
|
+
if Vagrant::Action::Builtin::BoxAdd.instance_methods.include?(:validate_box_url)
|
52
|
+
Vagrant::Action::Builtin::BoxAdd.class_eval do
|
53
|
+
alias_method :original_validate_box_url, :validate_box_url
|
54
|
+
|
55
|
+
def validate_box_url
|
56
|
+
if @url && @url.to_s.start_with?("s3://")
|
57
|
+
# Allow version constraints with S3 URLs
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Otherwise use the original validation
|
62
|
+
original_validate_box_url
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Also patch the method that validates version constraints
|
68
|
+
if Vagrant::Action::Builtin::BoxAdd.instance_methods.include?(:validate_version)
|
69
|
+
Vagrant::Action::Builtin::BoxAdd.class_eval do
|
70
|
+
alias_method :original_validate_version, :validate_version
|
71
|
+
|
72
|
+
def validate_version
|
73
|
+
# If we have an S3 URL, skip the version validation completely
|
74
|
+
if @url && @url.to_s.start_with?("s3://")
|
75
|
+
@logger.debug("Skipping version validation for S3 URL")
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Fall back to the original validation for non-S3 URLs
|
80
|
+
original_validate_version
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'vagrant/util/downloader'
|
3
|
+
|
4
|
+
module VagrantPlugins
|
5
|
+
module S3MultiDownloader
|
6
|
+
class HandleS3Urls
|
7
|
+
def initialize(app, env)
|
8
|
+
@app = app
|
9
|
+
@env = env
|
10
|
+
@logger = Log4r::Logger.new("vagrant::s3-multidownloader")
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@env = env
|
15
|
+
|
16
|
+
# Save references to the original Downloader methods
|
17
|
+
unless defined? @@original_curl_execute
|
18
|
+
@@original_curl_execute = Vagrant::Util::Downloader.instance_method(:execute_curl)
|
19
|
+
end
|
20
|
+
|
21
|
+
unless defined? @@original_download
|
22
|
+
@@original_download = Vagrant::Util::Downloader.instance_method(:download!)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Override both download! and execute_curl to handle s3:// URLs
|
26
|
+
# We need to intercept at both levels to ensure we catch all S3 URL usage
|
27
|
+
unless Vagrant::Util::Downloader.instance_methods.include?(:original_download)
|
28
|
+
Vagrant::Util::Downloader.class_eval do
|
29
|
+
alias_method :original_download, :download!
|
30
|
+
|
31
|
+
def download!
|
32
|
+
if @source.to_s.start_with?("s3://")
|
33
|
+
ui = @ui || @logger
|
34
|
+
ui.info("Intercepted S3 download at download! level: #{@source}")
|
35
|
+
downloader = VagrantPlugins::S3MultiDownloader::Downloader.new(
|
36
|
+
@source, @destination, ui,
|
37
|
+
continue: @continue,
|
38
|
+
headers: @headers,
|
39
|
+
env: @env
|
40
|
+
)
|
41
|
+
return downloader.download!
|
42
|
+
end
|
43
|
+
original_download
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Also override execute_curl to catch any direct curl executions with S3 URLs
|
49
|
+
unless Vagrant::Util::Downloader.instance_methods.include?(:original_execute_curl)
|
50
|
+
Vagrant::Util::Downloader.class_eval do
|
51
|
+
alias_method :original_execute_curl, :execute_curl
|
52
|
+
|
53
|
+
def execute_curl(options, subprocess_options, &data_proc)
|
54
|
+
# More aggressively check for S3 URLs in the options array
|
55
|
+
# This needs to catch any occurrence of s3:// in the curl command
|
56
|
+
s3_url = nil
|
57
|
+
options.each do |opt|
|
58
|
+
if opt.to_s =~ /^s3:\/\//
|
59
|
+
s3_url = opt.to_s
|
60
|
+
break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if s3_url
|
65
|
+
ui = @ui || @logger
|
66
|
+
ui.info("Intercepted S3 URL in curl command: #{s3_url}")
|
67
|
+
|
68
|
+
# Find the output path from the curl options
|
69
|
+
output_index = options.find_index("--output")
|
70
|
+
output_path = output_index ? options[output_index + 1] : nil
|
71
|
+
|
72
|
+
if output_path
|
73
|
+
ui.detail("Output path: #{output_path}")
|
74
|
+
# Use our S3 downloader instead
|
75
|
+
downloader = VagrantPlugins::S3MultiDownloader::Downloader.new(
|
76
|
+
s3_url, output_path, ui, env: @env
|
77
|
+
)
|
78
|
+
|
79
|
+
begin
|
80
|
+
ui.detail("Downloading from S3 via custom downloader: #{s3_url}")
|
81
|
+
downloader.download!
|
82
|
+
ui.detail("S3 download completed successfully")
|
83
|
+
return 0 # Return success status code
|
84
|
+
rescue => e
|
85
|
+
ui.error("S3 download error: #{e.message}")
|
86
|
+
ui.error(e.backtrace.join("\n"))
|
87
|
+
return 1 # Return error status code
|
88
|
+
end
|
89
|
+
else
|
90
|
+
ui.error("Could not determine output path for S3 download")
|
91
|
+
return 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# For non-S3 URLs, use the original method
|
96
|
+
original_execute_curl(options, subprocess_options, &data_proc)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Continue middleware chain
|
102
|
+
@app.call(env)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'vagrant'
|
2
|
+
require_relative 'downloader'
|
3
|
+
require_relative 'middleware/handle_s3_urls'
|
4
|
+
require_relative 'middleware/handle_metadata_versions'
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module S3MultiDownloader
|
8
|
+
class Plugin < Vagrant.plugin('2')
|
9
|
+
name 'S3MultiDownloader'
|
10
|
+
description 'Enables downloading Vagrant boxes from S3-compatible storage with multithreading'
|
11
|
+
|
12
|
+
# Hook into all possible places where box downloads might be triggered
|
13
|
+
action_hook(:s3_multi_downloader_box_url, :authenticate_box_url) do |hook|
|
14
|
+
hook.prepend(HandleS3Urls)
|
15
|
+
hook.prepend(HandleMetadataVersions)
|
16
|
+
end
|
17
|
+
|
18
|
+
action_hook(:s3_multi_downloader_box_add, :box_add) do |hook|
|
19
|
+
hook.prepend(HandleS3Urls)
|
20
|
+
hook.prepend(HandleMetadataVersions)
|
21
|
+
end
|
22
|
+
|
23
|
+
action_hook(:s3_multi_downloader_box_update, :box_update) do |hook|
|
24
|
+
hook.prepend(HandleS3Urls)
|
25
|
+
hook.prepend(HandleMetadataVersions)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add more hooks to ensure coverage of all possible download paths
|
29
|
+
action_hook(:s3_multi_downloader_initialize, :environment_load) do |hook|
|
30
|
+
hook.append(HandleS3Urls)
|
31
|
+
hook.append(HandleMetadataVersions)
|
32
|
+
end
|
33
|
+
|
34
|
+
action_hook(:s3_multi_downloader_download, :download_box) do |hook|
|
35
|
+
hook.prepend(HandleS3Urls)
|
36
|
+
hook.prepend(HandleMetadataVersions)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
begin
|
2
|
+
require 'vagrant'
|
3
|
+
rescue LoadError
|
4
|
+
raise 'The Vagrant S3 Multi Downloader plugin must be run within Vagrant.'
|
5
|
+
end
|
6
|
+
|
7
|
+
# Load downloader first so it can be used by the plugin
|
8
|
+
require_relative 'vagrant-s3-multidownloader/downloader'
|
9
|
+
require_relative 'vagrant-s3-multidownloader/middleware/handle_s3_urls'
|
10
|
+
require_relative 'vagrant-s3-multidownloader/middleware/handle_metadata_versions'
|
11
|
+
require_relative 'vagrant-s3-multidownloader/plugin'
|
12
|
+
|
13
|
+
module VagrantPlugins
|
14
|
+
module S3MultiDownloader
|
15
|
+
# Register S3 protocol handler early
|
16
|
+
def self.register_handlers
|
17
|
+
# Apply patches to Vagrant's downloader system immediately
|
18
|
+
VagrantPlugins::S3MultiDownloader::Downloader.patch_vagrant_downloader
|
19
|
+
|
20
|
+
# Log that we've loaded
|
21
|
+
logger = Log4r::Logger.new("vagrant::s3-multidownloader")
|
22
|
+
logger.info("S3 Multi Downloader plugin loaded")
|
23
|
+
logger.info("S3 URL protocol handler registered")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Automatically register our handlers when this module is loaded
|
27
|
+
register_handlers
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'vagrant-s3-multidownloader'
|
3
|
+
spec.version = '0.3.0'
|
4
|
+
spec.summary = 'Download Vagrant boxes from S3-compatible storage using AWS SDK.'
|
5
|
+
spec.description = 'Registers a custom downloader for the s3:// protocol to fetch boxes from any S3 API endpoint.'
|
6
|
+
spec.authors = ['Maksim Razumov']
|
7
|
+
spec.email = ['maksim.razumov@xiag.ch']
|
8
|
+
spec.files = Dir.glob('lib/**/*') + ['vagrant-s3-multidownloader.gemspec']
|
9
|
+
spec.require_paths = ['lib']
|
10
|
+
spec.add_runtime_dependency 'vagrant', '>= 2.2.0'
|
11
|
+
spec.add_runtime_dependency 'aws-sdk-s3', '~> 1.0'
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vagrant-s3-multidownloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maksim Razumov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: vagrant
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk-s3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
description: Registers a custom downloader for the s3:// protocol to fetch boxes from
|
42
|
+
any S3 API endpoint.
|
43
|
+
email:
|
44
|
+
- maksim.razumov@xiag.ch
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- lib/vagrant-s3-multidownloader.rb
|
50
|
+
- lib/vagrant-s3-multidownloader/downloader.rb
|
51
|
+
- lib/vagrant-s3-multidownloader/metadata_handler.rb
|
52
|
+
- lib/vagrant-s3-multidownloader/middleware/handle_metadata_versions.rb
|
53
|
+
- lib/vagrant-s3-multidownloader/middleware/handle_s3_urls.rb
|
54
|
+
- lib/vagrant-s3-multidownloader/plugin.rb
|
55
|
+
- vagrant-s3-multidownloader.gemspec
|
56
|
+
homepage:
|
57
|
+
licenses: []
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.3.27
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: Download Vagrant boxes from S3-compatible storage using AWS SDK.
|
78
|
+
test_files: []
|