tn_s3_file_uploader 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MGRmOTk0YjUyZjhhMTE5MjU5MjNhZjBiNmFmYTAwYWI2YThmMTVjYw==
5
+ data.tar.gz: !binary |-
6
+ NmM5MWNhYzIyZDlhNGIzZjRjOWM0ZmM1MjViZGE4Zjg1N2Q5ODc5Zg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MDhlMTJmNDFkZDU2YWNjZGI4M2JhZmYwZWRmOWMwZjc2ZmM4MjQ5M2U2M2I0
10
+ ZDczMDFiMGFhYzAzYTdmN2M2MTMyODRmNGJmZTRkYmNlZjNiNGFiZWRlMTQ1
11
+ NTdmNTZlYzE1NTc3MjJhMmE5MjFiZTQ3MDBmMjNjM2FmODQyMDg=
12
+ data.tar.gz: !binary |-
13
+ MzhmN2EzMWE3ZGZjOTM3Mzc2YTliNWZlYjZjMmVlN2MxZTNmMDJhYTEzZTdl
14
+ MTkwYjZhYTBmOWZjMTMwYzQ5NjlmZDc4Y2ExM2FiNmM2ODI5N2JkYzliZWYw
15
+ NmU4Yjg3YzI3ZTllNzRkZTFlMmM0YWJmNTZmYTBmNTUzYWQ4OGE=
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 Telenav, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software distributed
11
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12
+ either express or implied. See the License for the specific language governing permissions and limitations
13
+ under the License.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ tn_s3_file_uploader
2
+ ===================
3
+
4
+ Introduction
5
+ ------------
6
+ tn_s3_file_uploader is an Amazon S3 file uploader that can build destination folder structures based on the timestamp and
7
+ a number of other accepted substitutions.
8
+
9
+ Getting started
10
+ ---------------
11
+ Visit the [Getting started wiki page](https://github.com/ThinkNear/tn_s3_file_uploader/wiki/Getting-started)
12
+
13
+ License
14
+ -------
15
+ See the `LICENSE.txt` file for license information.
16
+
17
+ Contribute
18
+ ----------
19
+ Visit the [Contribute wiki page](https://github.com/ThinkNear/tn_s3_file_uploader/wiki/Contribute)
20
+
21
+ Questions? Problems? New Requests?
22
+ ----------------------------------
23
+ Visit the [Troubleshooting wiki page](https://github.com/ThinkNear/tn_s3_file_uploader/wiki/Troubleshooting)
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ require 'tn_s3_file_uploader'
3
+
4
+ # Executable - usage tn_s3_file_uploader --input-file-pattern='<<pattern matching file to be uploaded>>'
5
+ # --s3-output-pattern=<<S3 destination folder>>
6
+ #
7
+ # e.g. tn_s3_file_uploader --input-file-pattern='/etc/sonarqube/logs/*.log'
8
+ # --s3-output-pattern=bucket/sonarqube/%{file-extension}/y=%Y/m=%m/d=%d/h=%H/%{file-name}.%{file-extension}
9
+ options = TnS3FileUploader::CliParser.new.parse_cmd_line(ARGV)
10
+ TnS3FileUploader::Runner.new(options).run
11
+
@@ -0,0 +1,89 @@
1
+ require 'optparse'
2
+
3
+ module TnS3FileUploader
4
+
5
+ class CliParser
6
+
7
+ # Parses the CLI parameters and returns them in a hash
8
+ # Parameters 'host-unique-id', 'log-file-pattern', 's3-dest-folder' and 'partition-pattern' are mandatory and
9
+ # a missing argument exception will be raised if any of them is not provided
10
+ def parse_cmd_line(args)
11
+ options = {}
12
+ opts = OptionParser.new do |opts|
13
+ opts.banner = "Usage: tn_s3_file_uploader.rb [options]"
14
+
15
+ opts.separator ""
16
+ opts.separator "Specific options:"
17
+
18
+ opts.on("-s", "--input-file-pattern INPUT_FILE_PATTERN", "The file pattern to match the source log file") do |file_pattern|
19
+ options[:input_file_pattern] = file_pattern
20
+ end
21
+
22
+ opts.on("-o", "--s3-output-pattern S3_OUTPUT_PATTERN", "The S3 destination pattern name."\
23
+ "It accepts macros for building the resulting filename and folder structure. See documentation for details") do |s3_output_pattern|
24
+ options[:s3_output_pattern] = s3_output_pattern
25
+ end
26
+
27
+ options[:file_timestamp_resolution] = 300
28
+ opts.on("--file-timestamp-resolution RES", Integer, "The resolution of the destination filename in seconds (positive non-zero integer)") do |file_timestamp_resolution|
29
+ if valid_seconds?(file_timestamp_resolution)
30
+ options[:file_timestamp_resolution] = file_timestamp_resolution
31
+ else
32
+ puts "Warning: negative seconds value given: #{file_timestamp_resolution}, defaulting to 300 (5 minutes)"
33
+ end
34
+ end
35
+
36
+ opts.on("--honeybadger-api-key API-KEY", "API key for optional honeybadger error reporting support") do |honeybadger_api_key|
37
+ options[:honeybadger_api_key] = honeybadger_api_key
38
+ end
39
+
40
+ opts.on("--aws-access-key-id AWS-ACCESS-KEY-ID", "Provide the AWS access key id.") do |aws_access_key_id|
41
+ options[:aws_access_key_id] = aws_access_key_id
42
+ end
43
+
44
+ opts.on("--aws-secret-access-key AWS-SECRET-ACCESS-KEY", "Provide the AWS secret access key") do |aws_secret_access_key|
45
+ options[:aws_secret_access_key] = aws_secret_access_key
46
+ end
47
+
48
+ opts.on("-v", "--verbose", "Display verbose output") do |v|
49
+ options[:verbose] = !v.nil?
50
+ end
51
+
52
+ opts.separator ""
53
+
54
+ opts.on_tail("-h", "--help", "Show this message") do
55
+ puts opts
56
+ exit!
57
+ end
58
+ end
59
+
60
+ opts.parse! args
61
+
62
+ check_mandatory options
63
+
64
+ options
65
+ end
66
+
67
+ private
68
+
69
+ def check_mandatory(options)
70
+ mandatory_arguments = [:input_file_pattern, :s3_output_pattern]
71
+
72
+ missing_arguments = []
73
+ mandatory_arguments.each do |arg|
74
+ missing_arguments << arg.to_s.gsub!('_', '-') if options[arg].nil?
75
+ end
76
+
77
+ unless missing_arguments.empty?
78
+ raise OptionParser::MissingArgument,
79
+ "The following mandatory options are missing: #{ missing_arguments.join(', ') }"
80
+ end
81
+ end
82
+
83
+ def valid_seconds?(seconds)
84
+ seconds > 0
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,35 @@
1
+ require 'singleton'
2
+ require 'tn_s3_file_uploader/error_reporting/log_error_reporter'
3
+
4
+ module TnS3FileUploader
5
+
6
+ # Singleton that instruments all error reporters.
7
+ # Register your error reporter by calling `ErrorReportManager.instance.register_error_reporter`
8
+ # error reporters provided have to specify a method with name `report_error`
9
+ class ErrorReportManager
10
+ include Singleton
11
+
12
+ def initialize
13
+ @error_reporters = []
14
+ end
15
+
16
+ def register_error_reporter(error_reporter)
17
+ unless error_reporter.respond_to?(:report_error)
18
+ raise ArgumentError, 'Provided error_reporter instance does not support the report_error method'
19
+ end
20
+
21
+ @error_reporters << error_reporter
22
+ end
23
+
24
+ def count_error_reporters
25
+ @error_reporters.count
26
+ end
27
+
28
+ def report_error(exception, options = {})
29
+ @error_reporters.each do |error_reporter|
30
+ error_reporter.report_error(exception, options)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,20 @@
1
+ require 'honeybadger'
2
+
3
+ module TnS3FileUploader
4
+
5
+ # Error reporter that uses Honeybadger service to report errors
6
+ class HoneybadgerErrorReporter
7
+
8
+ # Configure honeybadger with provided api-key. Assumes api-key is not null
9
+ def initialize(api_key)
10
+ Honeybadger.configure do |config|
11
+ config.api_key = api_key
12
+ end
13
+ end
14
+
15
+ def report_error(exception, options = {})
16
+ Honeybadger.notify(exception, options)
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,17 @@
1
+ module TnS3FileUploader
2
+
3
+ # Simple error reporter that logs exception message and backtrace to stdout
4
+ class LogErrorReporter
5
+
6
+ def initialize(output)
7
+ @output = output
8
+ end
9
+
10
+ def report_error(exception, options ={} )
11
+ @output.puts options
12
+ @output.puts exception.message
13
+ @output.puts exception.backtrace.join("\n")
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,96 @@
1
+ require 'rubygems'
2
+
3
+ module TnS3FileUploader
4
+
5
+ # Examples
6
+ # partition = y=2014/m=06/d=18/h=18
7
+ # minute_partition = 45
8
+ # file_timestamp = 20140618184502
9
+ # date = Wed Jun 18 18:50:02 UTC 2014
10
+
11
+ # [ec2-user@ip-10-185-180-243 tmp]$ ./time.sh
12
+ # partition = y=2014/m=06/d=18/h=18
13
+ # minute_partition = 55
14
+ # file_timestamp = 20140618185540
15
+ # date = Wed Jun 18 19:03:40 UTC 2014
16
+ class FilePathGenerator
17
+
18
+ def initialize(time, options)
19
+ #Find the last rotation window
20
+ @options = options
21
+ @time = previous_rotation_window(time)
22
+ end
23
+
24
+
25
+ # Makes datetime and macro substitutions for input file 'file', based on the s3_output_pattern option
26
+ # Assumes input file 'file' and s3_output_pattern option are both valid.
27
+ # This method removes the bucket (everything until the first '/', including the '/') from the s3_output_pattern
28
+ # while applying the datetime/macro/substitutions
29
+ def dest_full_path_for(file)
30
+
31
+ output_file_pattern = remove_bucket(@options[:s3_output_pattern])
32
+
33
+ # Time#strftime is removing '%' characters on our macros. Our macro substitution must run first
34
+ subs = build_substitutions(file)
35
+ replace_macros!(output_file_pattern, subs)
36
+
37
+ substitute_datetime_macros(output_file_pattern)
38
+ end
39
+
40
+ private
41
+
42
+ def remove_bucket(output_file_pattern)
43
+ output_file_pattern.split('/')[1..-1].join('/')
44
+ end
45
+
46
+ # Makes the datetime substitutions on the give s3_output_pattern option
47
+ # For example:
48
+ # Given s3_output_pattern y=%Y/m=%m/d=%d/h=%H
49
+ # and time: Thu Jun 12 23:57:49 UTC 2014
50
+ # it will produce the following folder structure: y=2014/m=06/d=12/h=23
51
+ def substitute_datetime_macros(output_pattern)
52
+ @time.strftime(output_pattern)
53
+ end
54
+
55
+ # Generates rounded off timestamp based on rotation_seconds
56
+ def generate_file_timestamp
57
+ @time.strftime('%Y%m%d%H%M%S')
58
+ end
59
+
60
+ def replace_macros!(output_file_pattern, subs)
61
+ subs.each do |macro, sub|
62
+ output_file_pattern.gsub!(macro, sub)
63
+ end
64
+ end
65
+
66
+ def build_substitutions(file)
67
+ file_components = file.split('/').last.split('.')
68
+
69
+ if file_components.size == 1
70
+ file_name = file_components[0]
71
+ file_extension = ''
72
+ else
73
+ file_name = file_components[0..-2].join('.')
74
+ file_extension = file_components.last
75
+ end
76
+
77
+ ip_address = IPSocket.getaddress(Socket.gethostname).gsub('.', '-')
78
+ file_timestamp = generate_file_timestamp
79
+
80
+ {
81
+ '%{file-name}' => file_name,
82
+ '%{file-timestamp}' => file_timestamp,
83
+ '%{file-extension}' => file_extension,
84
+ '%{ip-address}' => ip_address
85
+ }
86
+ end
87
+
88
+ def previous_rotation_window(time)
89
+ rotation_seconds = @options[:file_timestamp_resolution]
90
+ t = time - rotation_seconds
91
+ floored_seconds = (t.to_f / rotation_seconds).floor * rotation_seconds
92
+ Time.at(floored_seconds).utc
93
+ end
94
+ end
95
+
96
+ end
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'tn_s3_file_uploader/s3'
3
+ require 'tn_s3_file_uploader/file_path_generator'
4
+
5
+ module TnS3FileUploader
6
+
7
+ class LogUploader
8
+
9
+ # Initialisation block
10
+ # Params:
11
+ # ::s3:: - S3 wrapper
12
+ def initialize(s3)
13
+ raise ArgumentError, "s3 client cannot be nil" if s3 == nil
14
+ @s3 = s3
15
+ end
16
+
17
+ # Uploads all (log) files that options[:input_file_pattern] matches to an S3 location
18
+ # based on the value of options[:s3_output_pattern]
19
+ # options[:input_file_pattern] should match at least one local file
20
+ # options[:s3_output_pattern] should contain the bucket, folder and destination filename
21
+ def upload_log_files(options)
22
+ raise ArgumentError, 's3_output_pattern cannot be empty' if blank?(options[:s3_output_pattern])
23
+ bucket = check_bucket_dest_path(options[:s3_output_pattern])
24
+ log_files = check_log_file(options[:input_file_pattern])
25
+
26
+ now = Time.now.utc
27
+ file_path_generator = FilePathGenerator.new(now, options)
28
+
29
+ log_files.each do |log_file|
30
+ destination_full_path = file_path_generator.dest_full_path_for(log_file)
31
+
32
+ puts "Found log file #{ log_file }, formatting file name for upload to S3 bucket #{ bucket } into folder #{ destination_full_path }"
33
+
34
+ # Note no leading or trailing slashes - this will break the upload to S3 (see our s3.rb)
35
+ @s3.upload_file(log_file, bucket, destination_full_path)
36
+ end
37
+
38
+ end
39
+
40
+ private
41
+ def check_log_file(log_file_pattern)
42
+ raise ArgumentError, 'log file pattern cannot be nil' if log_file_pattern == nil
43
+
44
+ last_folder_separator = log_file_pattern.rindex('.')
45
+ raise ArgumentError, "#{ log_file_pattern } is not a valid path. It lacks a file extension." if last_folder_separator == nil
46
+
47
+ files = Dir[log_file_pattern].entries
48
+ raise ArgumentError, "#{ log_file_pattern } did not match any files." if files.empty?
49
+
50
+ files
51
+ end
52
+
53
+ def check_bucket_dest_path(bucket_dest_path)
54
+ path_components = bucket_dest_path.split('/')
55
+ raise ArgumentError, "Bucket destination folder #{ bucket_dest_path } must have at least two path components, e.g. my/path." unless path_components.size > 1
56
+ path_components.first
57
+ end
58
+
59
+ def blank?(str)
60
+ str.nil? || str == ""
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'aws-sdk'
3
+ require 'honeybadger'
4
+ require 'tn_s3_file_uploader/log_uploader'
5
+
6
+ module TnS3FileUploader
7
+ class Runner
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @error_report_manager = ErrorReportManager.instance
12
+ end
13
+
14
+ def run
15
+ add_log_error_reporter
16
+ add_honeybadger
17
+ puts "Running TnS3FileUploader..." if @options[:verbose]
18
+
19
+ upload
20
+ rescue Exception => e
21
+ @error_report_manager.report_error(e, { :options => @options } )
22
+ end
23
+
24
+ private
25
+
26
+ def upload
27
+ if @options[:verbose]
28
+ puts "Using:"
29
+ puts "log file pattern = #{ @options[:input_file_pattern] }"
30
+ puts "s3 dest folder = #{ @options[:s3_output_pattern] }"
31
+ puts "file timestamp resolution = #{ options[:file_timestamp_resolution] }"
32
+ end
33
+
34
+ s3_client = create_s3_client
35
+ s3 = S3.new(s3_client)
36
+
37
+ log_uploader = LogUploader.new(s3)
38
+ log_uploader.upload_log_files(@options)
39
+ end
40
+
41
+ def create_s3_client
42
+ if @options[:aws_access_key_id].nil? && @options[:aws_secret_access_key].nil?
43
+ AWS::S3.new
44
+ else
45
+ AWS::S3.new(
46
+ :access_key_id => @options[:aws_access_key_id],
47
+ :secret_access_key => @options[:aws_secret_access_key]
48
+ )
49
+ end
50
+ end
51
+
52
+ def add_log_error_reporter
53
+ @error_report_manager.register_error_reporter(LogErrorReporter.new(STDOUT))
54
+ end
55
+
56
+ def add_honeybadger
57
+ unless @options[:honeybadger_api_key].nil?
58
+ honeybadger_error_reporter = HoneybadgerErrorReporter.new(@options[:honeybadger_api_key])
59
+ @error_report_manager.register_error_reporter(honeybadger_error_reporter)
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ gem 'aws-sdk'
3
+ require 'aws-sdk'
4
+ require 'honeybadger'
5
+ require 'tn_s3_file_uploader/file_path_generator'
6
+ module TnS3FileUploader
7
+
8
+ class S3
9
+
10
+ MAX_RETRIES = 2
11
+
12
+ def initialize(s3_client)
13
+ @s3_client = s3_client
14
+ end
15
+
16
+ # File must be fully qualified
17
+ # bucket is just string name, no slashes
18
+ # dest_path is fully qualified path to file on S3 including folders - NO leading or trailing slashes or
19
+ # it won't work!
20
+ def upload_file(file, bucket, dest_path)
21
+ raise ArgumentError, "file cannot be nil" if file == nil
22
+ raise ArgumentError, "bucket cannot be nil" if bucket == nil
23
+ raise ArgumentError, "dest_path cannot be nil" if dest_path == nil
24
+
25
+ file_path = Pathname.new(file)
26
+ raise ArgumentError, "#{file} is not a valid file" unless file_path.exist? && file_path.file?
27
+
28
+ upload(bucket, dest_path, file)
29
+ end
30
+
31
+ private
32
+ def upload(bucket, dest_full_path, file, retry_count = 0)
33
+ begin
34
+ s3_bucket = @s3_client.buckets[bucket]
35
+ s3_file_path = s3_bucket.objects[dest_full_path]
36
+
37
+ puts "Uploading file #{file} to S3 bucket #{bucket} and path #{dest_full_path}"
38
+
39
+ s3_file_path.write(File.open(file, 'rb'))
40
+ rescue StandardError, Timeout::Error => e
41
+ if retry_count < MAX_RETRIES
42
+ #This fixes a bug where the credentials may have rotated on the EC2 instance but the old values
43
+ #are still cached
44
+ sleep 1
45
+ @s3_client.config.credential_provider.refresh
46
+ upload(bucket, dest_full_path, file, retry_count + 1)
47
+ else
48
+ raise e
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,3 @@
1
+ module TnS3FileUploader
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'tn_s3_file_uploader/runner'
2
+ require 'tn_s3_file_uploader/cli_parser'
3
+ require 'tn_s3_file_uploader/file_path_generator'
4
+ require 'tn_s3_file_uploader/log_uploader'
5
+ require 'tn_s3_file_uploader/s3'
6
+
7
+ require 'tn_s3_file_uploader/error_reporting/error_report_manager'
8
+ require 'tn_s3_file_uploader/error_reporting/honeybadger_error_reporter'
9
+ require 'tn_s3_file_uploader/error_reporting/log_error_reporter'
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tn_s3_file_uploader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thinknear.com
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: honeybadger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.35'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.35'
41
+ description: S3 file uploader that can build folder structures based on timestamp.
42
+ Typically used in conjunction with Unix's logrotate.
43
+ email: opensource@thinknear.com
44
+ executables:
45
+ - tn_s3_file_uploader
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ - bin/tn_s3_file_uploader
52
+ - lib/tn_s3_file_uploader.rb
53
+ - lib/tn_s3_file_uploader/cli_parser.rb
54
+ - lib/tn_s3_file_uploader/error_reporting/error_report_manager.rb
55
+ - lib/tn_s3_file_uploader/error_reporting/honeybadger_error_reporter.rb
56
+ - lib/tn_s3_file_uploader/error_reporting/log_error_reporter.rb
57
+ - lib/tn_s3_file_uploader/file_path_generator.rb
58
+ - lib/tn_s3_file_uploader/log_uploader.rb
59
+ - lib/tn_s3_file_uploader/runner.rb
60
+ - lib/tn_s3_file_uploader/s3.rb
61
+ - lib/tn_s3_file_uploader/version.rb
62
+ homepage: http://www.thinknear.com
63
+ licenses:
64
+ - Copyright (c) ThinkNear 2014, Licensed under APLv2.0
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.2.2
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: S3 file uploader
86
+ test_files: []