aws-sdk-resources 2.11.549

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/lib/aws-sdk-resources.rb +91 -0
  3. data/lib/aws-sdk-resources/batch.rb +143 -0
  4. data/lib/aws-sdk-resources/builder.rb +85 -0
  5. data/lib/aws-sdk-resources/builder_sources.rb +105 -0
  6. data/lib/aws-sdk-resources/collection.rb +107 -0
  7. data/lib/aws-sdk-resources/definition.rb +331 -0
  8. data/lib/aws-sdk-resources/documenter.rb +70 -0
  9. data/lib/aws-sdk-resources/documenter/base_operation_documenter.rb +279 -0
  10. data/lib/aws-sdk-resources/documenter/data_operation_documenter.rb +25 -0
  11. data/lib/aws-sdk-resources/documenter/has_many_operation_documenter.rb +69 -0
  12. data/lib/aws-sdk-resources/documenter/has_operation_documenter.rb +66 -0
  13. data/lib/aws-sdk-resources/documenter/operation_documenter.rb +20 -0
  14. data/lib/aws-sdk-resources/documenter/resource_operation_documenter.rb +53 -0
  15. data/lib/aws-sdk-resources/documenter/waiter_operation_documenter.rb +77 -0
  16. data/lib/aws-sdk-resources/errors.rb +15 -0
  17. data/lib/aws-sdk-resources/operation_methods.rb +83 -0
  18. data/lib/aws-sdk-resources/operations.rb +280 -0
  19. data/lib/aws-sdk-resources/options.rb +17 -0
  20. data/lib/aws-sdk-resources/request.rb +39 -0
  21. data/lib/aws-sdk-resources/request_params.rb +140 -0
  22. data/lib/aws-sdk-resources/resource.rb +243 -0
  23. data/lib/aws-sdk-resources/services/ec2.rb +21 -0
  24. data/lib/aws-sdk-resources/services/ec2/instance.rb +29 -0
  25. data/lib/aws-sdk-resources/services/iam.rb +19 -0
  26. data/lib/aws-sdk-resources/services/s3.rb +20 -0
  27. data/lib/aws-sdk-resources/services/s3/bucket.rb +131 -0
  28. data/lib/aws-sdk-resources/services/s3/encryption.rb +21 -0
  29. data/lib/aws-sdk-resources/services/s3/encryption/client.rb +369 -0
  30. data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +174 -0
  31. data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +63 -0
  32. data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +38 -0
  33. data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +50 -0
  34. data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +13 -0
  35. data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +56 -0
  36. data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +29 -0
  37. data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +69 -0
  38. data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +29 -0
  39. data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +71 -0
  40. data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +58 -0
  41. data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +79 -0
  42. data/lib/aws-sdk-resources/services/s3/file_downloader.rb +169 -0
  43. data/lib/aws-sdk-resources/services/s3/file_part.rb +75 -0
  44. data/lib/aws-sdk-resources/services/s3/file_uploader.rb +58 -0
  45. data/lib/aws-sdk-resources/services/s3/multipart_file_uploader.rb +187 -0
  46. data/lib/aws-sdk-resources/services/s3/multipart_upload.rb +42 -0
  47. data/lib/aws-sdk-resources/services/s3/multipart_upload_error.rb +16 -0
  48. data/lib/aws-sdk-resources/services/s3/object.rb +290 -0
  49. data/lib/aws-sdk-resources/services/s3/object_copier.rb +99 -0
  50. data/lib/aws-sdk-resources/services/s3/object_multipart_copier.rb +180 -0
  51. data/lib/aws-sdk-resources/services/s3/object_summary.rb +73 -0
  52. data/lib/aws-sdk-resources/services/s3/presigned_post.rb +651 -0
  53. data/lib/aws-sdk-resources/services/sns.rb +7 -0
  54. data/lib/aws-sdk-resources/services/sns/message_verifier.rb +171 -0
  55. data/lib/aws-sdk-resources/services/sqs.rb +7 -0
  56. data/lib/aws-sdk-resources/services/sqs/queue_poller.rb +521 -0
  57. data/lib/aws-sdk-resources/source.rb +39 -0
  58. metadata +118 -0
@@ -0,0 +1,99 @@
1
+ require 'thread'
2
+
3
+ module Aws
4
+ module S3
5
+ # @api private
6
+ class ObjectCopier
7
+
8
+ # @param [S3::Object] object
9
+ def initialize(object, options = {})
10
+ @object = object
11
+ @options = options.merge(client: @object.client)
12
+ end
13
+
14
+ def copy_from(source, options = {})
15
+ copy_object(source, @object, merge_options(source, options))
16
+ end
17
+
18
+ def copy_to(target, options = {})
19
+ copy_object(@object, target, merge_options(target, options))
20
+ end
21
+
22
+ private
23
+
24
+ def copy_object(source, target, options)
25
+ target_bucket, target_key = copy_target(target)
26
+ options[:bucket] = target_bucket
27
+ options[:key] = target_key
28
+ options[:copy_source] = copy_source(source)
29
+ if options.delete(:multipart_copy)
30
+ apply_source_client(source, options)
31
+ ObjectMultipartCopier.new(@options).copy(options)
32
+ else
33
+ @object.client.copy_object(options)
34
+ end
35
+ end
36
+
37
+ def copy_source(source)
38
+ case source
39
+ when String then source
40
+ when Hash
41
+ src = "#{source[:bucket]}/#{escape(source[:key])}"
42
+ src += "?versionId=#{source[:version_id]}" if source.key?(:version_id)
43
+ src
44
+ when S3::Object, S3::ObjectSummary
45
+ "#{source.bucket_name}/#{escape(source.key)}"
46
+ when S3::ObjectVersion
47
+ "#{source.bucket_name}/#{escape(source.object_key)}?versionId=#{source.id}"
48
+ else
49
+ msg = "expected source to be an Aws::S3::Object, Hash, or String"
50
+ raise ArgumentError, msg
51
+ end
52
+ end
53
+
54
+ def copy_target(target)
55
+ case target
56
+ when String then target.match(/([^\/]+?)\/(.+)/)[1,2]
57
+ when Hash then target.values_at(:bucket, :key)
58
+ when S3::Object then [target.bucket_name, target.key]
59
+ else
60
+ msg = "expected target to be an Aws::S3::Object, Hash, or String"
61
+ raise ArgumentError, msg
62
+ end
63
+ end
64
+
65
+ def merge_options(source_or_target, options)
66
+ if Hash === source_or_target
67
+ source_or_target.inject(options.dup) do |opts, (key, value)|
68
+ opts[key] = value unless [:bucket, :key, :version_id].include?(key)
69
+ opts
70
+ end
71
+ else
72
+ options.dup
73
+ end
74
+ end
75
+
76
+ def apply_source_client(source, options)
77
+
78
+ if source.respond_to?(:client)
79
+ options[:copy_source_client] ||= source.client
80
+ end
81
+
82
+ if options[:copy_source_region]
83
+ config = @object.client.config
84
+ config = config.each_pair.inject({}) { |h, (k,v)| h[k] = v; h }
85
+ config[:region] = options.delete(:copy_source_region)
86
+ options[:copy_source_client] ||= S3::Client.new(config)
87
+ end
88
+
89
+ options[:copy_source_client] ||= @object.client
90
+
91
+ end
92
+
93
+ def escape(str)
94
+ Seahorse::Util.uri_path_escape(str)
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,180 @@
1
+ require 'thread'
2
+ require 'cgi'
3
+
4
+ module Aws
5
+ module S3
6
+ # @api private
7
+ class ObjectMultipartCopier
8
+
9
+ FIVE_MB = 5 * 1024 * 1024 # 5MB
10
+
11
+ FILE_TOO_SMALL = "unable to multipart copy files smaller than 5MB"
12
+
13
+ MAX_PARTS = 10_000
14
+
15
+ # @option options [Client] :client
16
+ # @option [Integer] :min_part_size (52428800) Size of copied parts.
17
+ # Defaults to 50MB.
18
+ # will be constructed from the given `options' hash.
19
+ # @option [Integer] :thread_count (10) Number of concurrent threads to
20
+ # use for copying parts.
21
+ def initialize(options = {})
22
+ @thread_count = options.delete(:thread_count) || 10
23
+ @min_part_size = options.delete(:min_part_size) || (FIVE_MB * 10)
24
+ @client = options[:client] || Client.new
25
+ end
26
+
27
+ # @return [Client]
28
+ attr_reader :client
29
+
30
+ # @option (see S3::Client#copy_object)
31
+ def copy(options = {})
32
+ size = source_size(options)
33
+ options[:upload_id] = initiate_upload(options)
34
+ begin
35
+ parts = copy_parts(size, default_part_size(size), options)
36
+ complete_upload(parts, options)
37
+ rescue => error
38
+ abort_upload(options)
39
+ raise error
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def initiate_upload(options)
46
+ options = options_for(:create_multipart_upload, options)
47
+ @client.create_multipart_upload(options).upload_id
48
+ end
49
+
50
+ def copy_parts(size, default_part_size, options)
51
+ queue = PartQueue.new(compute_parts(size, default_part_size, options))
52
+ threads = []
53
+ @thread_count.times do
54
+ threads << copy_part_thread(queue)
55
+ end
56
+ threads.map(&:value).flatten.sort_by{ |part| part[:part_number] }
57
+ end
58
+
59
+ def copy_part_thread(queue)
60
+ Thread.new do
61
+ begin
62
+ completed = []
63
+ while part = queue.shift
64
+ completed << copy_part(part)
65
+ end
66
+ completed
67
+ rescue => error
68
+ queue.clear!
69
+ raise error
70
+ end
71
+ end
72
+ end
73
+
74
+ def copy_part(part)
75
+ {
76
+ etag: @client.upload_part_copy(part).copy_part_result.etag,
77
+ part_number: part[:part_number],
78
+ }
79
+ end
80
+
81
+ def complete_upload(parts, options)
82
+ options = options_for(:complete_multipart_upload, options)
83
+ options[:multipart_upload] = { parts: parts }
84
+ @client.complete_multipart_upload(options)
85
+ end
86
+
87
+ def abort_upload(options)
88
+ @client.abort_multipart_upload({
89
+ bucket: options[:bucket],
90
+ key: options[:key],
91
+ upload_id: options[:upload_id],
92
+ })
93
+ end
94
+
95
+ def compute_parts(size, default_part_size, options)
96
+ part_number = 1
97
+ offset = 0
98
+ parts = []
99
+ options = options_for(:upload_part_copy, options)
100
+ while offset < size
101
+ parts << options.merge({
102
+ part_number: part_number,
103
+ copy_source_range: byte_range(offset, default_part_size, size),
104
+ })
105
+ part_number += 1
106
+ offset += default_part_size
107
+ end
108
+ parts
109
+ end
110
+
111
+ def byte_range(offset, default_part_size, size)
112
+ if offset + default_part_size < size
113
+ "bytes=#{offset}-#{offset + default_part_size - 1}"
114
+ else
115
+ "bytes=#{offset}-#{size - 1}"
116
+ end
117
+ end
118
+
119
+ def source_size(options)
120
+ return options.delete(:content_length) if options[:content_length]
121
+
122
+ client = options[:copy_source_client] || @client
123
+
124
+ if vid_match = options[:copy_source].match(/([^\/]+?)\/(.+)\?versionId=(.+)/)
125
+ bucket, key, version_id = vid_match[1,3]
126
+ else
127
+ bucket, key = options[:copy_source].match(/([^\/]+?)\/(.+)/)[1,2]
128
+ end
129
+
130
+ key = CGI.unescape(key)
131
+ opts = { bucket: bucket, key: key }
132
+ opts[:version_id] = version_id if version_id
133
+ client.head_object(opts).content_length
134
+ end
135
+
136
+ def default_part_size(source_size)
137
+ if source_size < FIVE_MB
138
+ raise ArgumentError, FILE_TOO_SMALL
139
+ else
140
+ [(source_size.to_f / MAX_PARTS).ceil, @min_part_size].max.to_i
141
+ end
142
+ end
143
+
144
+ def options_for(operation_name, options)
145
+ API_OPTIONS[operation_name].inject({}) do |hash, opt_name|
146
+ hash[opt_name] = options[opt_name] if options.key?(opt_name)
147
+ hash
148
+ end
149
+ end
150
+
151
+ # @api private
152
+ def self.options_for(shape_name)
153
+ Client.api.metadata['shapes'][shape_name].member_names
154
+ end
155
+
156
+ API_OPTIONS = {
157
+ create_multipart_upload: options_for('CreateMultipartUploadRequest'),
158
+ upload_part_copy: options_for('UploadPartCopyRequest'),
159
+ complete_multipart_upload: options_for('CompleteMultipartUploadRequest'),
160
+ }
161
+
162
+ class PartQueue
163
+
164
+ def initialize(parts = [])
165
+ @parts = parts
166
+ @mutex = Mutex.new
167
+ end
168
+
169
+ def shift
170
+ @mutex.synchronize { @parts.shift }
171
+ end
172
+
173
+ def clear!
174
+ @mutex.synchronize { @parts.clear }
175
+ end
176
+
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,73 @@
1
+ module Aws
2
+ module S3
3
+ class ObjectSummary
4
+
5
+ alias content_length size
6
+
7
+ # @param (see Object#copy_from)
8
+ # @options (see Object#copy_from)
9
+ # @return (see Object#copy_from)
10
+ # @see Object#copy_from
11
+ def copy_from(source, options = {})
12
+ object.copy_from(source, options)
13
+ end
14
+
15
+ # @param (see Object#copy_to)
16
+ # @options (see Object#copy_to)
17
+ # @return (see Object#copy_to)
18
+ # @see Object#copy_to
19
+ def copy_to(target, options = {})
20
+ object.copy_to(target, options)
21
+ end
22
+
23
+ # @param (see Object#move_to)
24
+ # @options (see Object#move_to)
25
+ # @return (see Object#move_to)
26
+ # @see Object#move_to
27
+ def move_to(target, options = {})
28
+ object.move_to(target, options)
29
+ end
30
+
31
+ # @param (see Object#presigned_post)
32
+ # @options (see Object#presigned_post)
33
+ # @return (see Object#presigned_post)
34
+ # @see Object#presigned_post
35
+ def presigned_post(options = {})
36
+ object.presigned_post(options)
37
+ end
38
+
39
+ # @param (see Object#presigned_url)
40
+ # @options (see Object#presigned_url)
41
+ # @return (see Object#presigned_url)
42
+ # @see Object#presigned_url
43
+ def presigned_url(http_method, params = {})
44
+ object.presigned_url(http_method, params)
45
+ end
46
+
47
+ # @param (see Object#public_url)
48
+ # @options (see Object#public_url)
49
+ # @return (see Object#public_url)
50
+ # @see Object#public_url
51
+ def public_url(options = {})
52
+ object.public_url(options)
53
+ end
54
+
55
+ # @param (see Object#upload_file)
56
+ # @options (see Object#upload_file)
57
+ # @return (see Object#upload_file)
58
+ # @see Object#upload_file
59
+ def upload_file(source, options = {})
60
+ object.upload_file(source, options)
61
+ end
62
+
63
+ # @param (see Object#download_file)
64
+ # @options (see Object#download_file)
65
+ # @return (see Object#download_file)
66
+ # @see Object#download_file
67
+ def download_file(destination, options = {})
68
+ object.download_file(destination, options)
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,651 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Aws
5
+ module S3
6
+
7
+ # @note Normally you do not need to construct a {PresignedPost} yourself.
8
+ # See {Bucket#presigned_post} and {Object#presigned_post}.
9
+ #
10
+ # ## Basic Usage
11
+ #
12
+ # To generate a presigned post, you need AWS credentials, the region
13
+ # your bucket is in, and the name of your bucket. You can apply constraints
14
+ # to the post object as options to {#initialize} or by calling
15
+ # methods such as {#key} and {#content_length_range}.
16
+ #
17
+ # The following two examples are equivalent.
18
+ #
19
+ # ```ruby
20
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket, {
21
+ # key: '/uploaded/object/key',
22
+ # content_length_range: 0..1024,
23
+ # acl: 'public-read',
24
+ # metadata: {
25
+ # 'original-filename' => '${filename}'
26
+ # }
27
+ # })
28
+ # post.fields
29
+ # #=> { ... }
30
+ #
31
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket).
32
+ # key('/uploaded/object/key').
33
+ # content_length_range(0..1024).
34
+ # acl('public-read').
35
+ # metadata('original-filename' => '${filename}').
36
+ # fields
37
+ # #=> { ... }
38
+ # ```
39
+ #
40
+ # ## HTML Forms
41
+ #
42
+ # You can use a {PresignedPost} object to build an HTML form. It is
43
+ # recommended to use some helper to build the form tag and input
44
+ # tags that properly escapes values.
45
+ #
46
+ # ### Form Tag
47
+ #
48
+ # To upload a file to Amazon S3 using a browser, you need to create
49
+ # a post form. The {#url} method returns the value you should use
50
+ # as the form action.
51
+ #
52
+ # ```erb
53
+ # <form action="<%= @post.url %>" method="post" enctype="multipart/form-data">
54
+ # ...
55
+ # </form>
56
+ # ```
57
+ #
58
+ # The follow attributes must be set on the form:
59
+ #
60
+ # * `action` - This must be the {#url}.
61
+ # * `method` - This must be `post`.
62
+ # * `enctype` - This must be `multipart/form-data`.
63
+ #
64
+ # ### Form Fields
65
+ #
66
+ # The {#fields} method returns a hash of form fields to render inside
67
+ # the form. Typically these are rendered as hidden input fields.
68
+ #
69
+ # ```erb
70
+ # <% @post.fields.each do |name, value| %>
71
+ # <input type="hidden" name="<%= name %>" value="<%= value %>"/>
72
+ # <% end %>
73
+ # ```
74
+ #
75
+ # Lastly, the form must have a file field with the name `file`.
76
+ #
77
+ # ```erb
78
+ # <input type="file" name="file"/>
79
+ # ```
80
+ #
81
+ # ## Post Policy
82
+ #
83
+ # When you construct a {PresignedPost}, you must specify every form
84
+ # field name that will be posted by the browser. If you omit a form
85
+ # field sent by the browser, Amazon S3 will reject the request.
86
+ # You can specify accepted form field values three ways:
87
+ #
88
+ # * Specify exactly what the value must be.
89
+ # * Specify what value the field starts with.
90
+ # * Specify the field may have any value.
91
+ #
92
+ # ### Field Equals
93
+ #
94
+ # You can specify that a form field must be a certain value.
95
+ # Simply pass an option like `:content_type` to the constructor,
96
+ # or call the associated method.
97
+ #
98
+ # ```ruby
99
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket).
100
+ # post.content_type('text/plain')
101
+ # ```
102
+ #
103
+ # If any of the given values are changed by the user in the form, then
104
+ # Amazon S3 will reject the POST request.
105
+ #
106
+ # ### Field Starts With
107
+ #
108
+ # You can specify prefix values for many of the POST form fields.
109
+ # To specify a required prefix, use the `:<fieldname>_starts_with`
110
+ # option or call the associated `#<field_name>_starts_with` method.
111
+ #
112
+ # ```ruby
113
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket, {
114
+ # key_starts_with: '/images/',
115
+ # content_type_starts_with: 'image/',
116
+ # # ...
117
+ # })
118
+ # ```
119
+ #
120
+ # When using starts with, the form must contain a field where the
121
+ # user can specify the value. The {PresignedPost} will not add
122
+ # a value for these fields.
123
+ #
124
+ # ### Any Field Value
125
+ #
126
+ # To white-list a form field to send any value, you can name that
127
+ # field with `:allow_any` or {#allow_any}.
128
+ #
129
+ # ```ruby
130
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket, {
131
+ # key: 'object-key',
132
+ # allow_any: ['Filename'],
133
+ # # ...
134
+ # })
135
+ # ```
136
+ #
137
+ # ### Metadata
138
+ #
139
+ # You can add rules for metadata fields using `:metadata`, {#metadata},
140
+ # `:metadata_starts_with` and {#metadata_starts_with}. Unlike other
141
+ # form fields, you pass a hash value to these options/methods:
142
+ #
143
+ # ```ruby
144
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket).
145
+ # key('/fixed/key').
146
+ # metadata(foo: 'bar')
147
+ #
148
+ # post.fields['x-amz-meta-foo']
149
+ # #=> 'bar'
150
+ # ```
151
+ #
152
+ # ### The `${filename}` Variable
153
+ #
154
+ # The string `${filename}` is automatically replaced with the name of the
155
+ # file provided by the user and is recognized by all form fields. It is
156
+ # not supported with `starts_with` conditions.
157
+ #
158
+ # If the browser or client provides a full or partial path to the file,
159
+ # only the text following the last slash (/) or backslash (\) will be used
160
+ # (e.g., "C:\Program Files\directory1\file.txt" will be interpreted
161
+ # as "file.txt"). If no file or file name is provided, the variable is
162
+ # replaced with an empty string.
163
+ #
164
+ # In the following example, we use `${filename}` to store the original
165
+ # filename in the `x-amz-meta-` hash with the uploaded object.
166
+ #
167
+ # ```ruby
168
+ # post = Aws::S3::PresignedPost.new(creds, region, bucket, {
169
+ # key: '/fixed/key',
170
+ # metadata: {
171
+ # 'original-filename': '${filename}'
172
+ # }
173
+ # })
174
+ # ```
175
+ #
176
+ class PresignedPost
177
+
178
+ # @param [Credentials] credentials Security credentials for signing
179
+ # the post policy.
180
+ # @param [String] bucket_region Region of the target bucket.
181
+ # @param [String] bucket_name Name of the target bucket.
182
+ # @option options [Time] :signature_expiration Specify when the signature on
183
+ # the post will expire. Defaults to one hour from creation of the
184
+ # presigned post. May not exceed one week from creation time.
185
+ # @option options [String] :key See {PresignedPost#key}.
186
+ # @option options [String] :key_starts_with See {PresignedPost#key_starts_with}.
187
+ # @option options [String] :acl See {PresignedPost#acl}.
188
+ # @option options [String] :acl_starts_with See {PresignedPost#acl_starts_with}.
189
+ # @option options [String] :cache_control See {PresignedPost#cache_control}.
190
+ # @option options [String] :cache_control_starts_with See {PresignedPost#cache_control_starts_with}.
191
+ # @option options [String] :content_type See {PresignedPost#content_type}.
192
+ # @option options [String] :content_type_starts_with See {PresignedPost#content_type_starts_with}.
193
+ # @option options [String] :content_disposition See {PresignedPost#content_disposition}.
194
+ # @option options [String] :content_disposition_starts_with See {PresignedPost#content_disposition_starts_with}.
195
+ # @option options [String] :content_encoding See {PresignedPost#content_encoding}.
196
+ # @option options [String] :content_encoding_starts_with See {PresignedPost#content_encoding_starts_with}.
197
+ # @option options [String] :expires See {PresignedPost#expires}.
198
+ # @option options [String] :expires_starts_with See {PresignedPost#expires_starts_with}.
199
+ # @option options [Range<Integer>] :content_length_range See {PresignedPost#content_length_range}.
200
+ # @option options [String] :success_action_redirect See {PresignedPost#success_action_redirect}.
201
+ # @option options [String] :success_action_redirect_starts_with See {PresignedPost#success_action_redirect_starts_with}.
202
+ # @option options [String] :success_action_status See {PresignedPost#success_action_status}.
203
+ # @option options [String] :storage_class See {PresignedPost#storage_class}.
204
+ # @option options [String] :website_redirect_location See {PresignedPost#website_redirect_location}.
205
+ # @option options [Hash<String,String>] :metadata See {PresignedPost#metadata}.
206
+ # @option options [Hash<String,String>] :metadata_starts_with See {PresignedPost#metadata_starts_with}.
207
+ # @option options [String] :server_side_encryption See {PresignedPost#server_side_encryption}.
208
+ # @option options [String] :server_side_encryption_aws_kms_key_id See {PresignedPost#server_side_encryption_aws_kms_key_id}.
209
+ # @option options [String] :server_side_encryption_customer_algorithm See {PresignedPost#server_side_encryption_customer_algorithm}.
210
+ # @option options [String] :server_side_encryption_customer_key See {PresignedPost#server_side_encryption_customer_key}.
211
+ def initialize(credentials, bucket_region, bucket_name, options = {})
212
+ @credentials = credentials.credentials
213
+ @bucket_region = bucket_region
214
+ @bucket_name = bucket_name
215
+ @url = options.delete(:url) || bucket_url
216
+ @fields = {}
217
+ @key_set = false
218
+ @signature_expiration = Time.now + 3600
219
+ @conditions = [{ 'bucket' => @bucket_name }]
220
+ options.each do |option_name, option_value|
221
+ case option_name
222
+ when :allow_any then allow_any(option_value)
223
+ when :signature_expiration then @signature_expiration = option_value
224
+ else send("#{option_name}", option_value)
225
+ end
226
+ end
227
+ end
228
+
229
+ # @return [String] The URL to post a file upload to. This should be
230
+ # the form action.
231
+ attr_reader :url
232
+
233
+ # @return [Hash] A hash of fields to render in an HTML form
234
+ # as hidden input fields.
235
+ def fields
236
+ check_required_values!
237
+ datetime = Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
238
+ fields = @fields.dup
239
+ fields.update('policy' => policy(datetime))
240
+ fields.update(signature_fields(datetime))
241
+ fields.update('x-amz-signature' => signature(datetime, fields['policy']))
242
+ end
243
+
244
+ # A list of form fields to white-list with any value.
245
+ # @param [Sting, Array<String>] field_names
246
+ # @return [self]
247
+ def allow_any(*field_names)
248
+ field_names.flatten.each do |field_name|
249
+ @key_set = true if field_name.to_s == 'key'
250
+ starts_with(field_name, '')
251
+ end
252
+ self
253
+ end
254
+
255
+ # @api private
256
+ def self.define_field(field, *args)
257
+ options = args.last.is_a?(Hash) ? args.pop : {}
258
+ field_name = args.last || field.to_s
259
+
260
+ define_method("#{field}") do |value|
261
+ with(field_name, value)
262
+ end
263
+
264
+ if options[:starts_with]
265
+ define_method("#{field}_starts_with") do |value|
266
+ starts_with(field_name, value)
267
+ end
268
+ end
269
+ end
270
+
271
+ # @!group Fields
272
+
273
+ # The key to use for the uploaded object. Use can use `${filename}`
274
+ # as a variable in the key. This will be replaced with the name
275
+ # of the file as provided by the user.
276
+ #
277
+ # For example, if the key is given as `/user/betty/${filename}` and
278
+ # the file uploaded is named `lolcatz.jpg`, the resultant key will
279
+ # be `/user/betty/lolcatz.jpg`.
280
+ #
281
+ # @param [String] key
282
+ # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html)
283
+ # @return [self]
284
+ def key(key)
285
+ @key_set = true
286
+ with('key', key)
287
+ end
288
+
289
+ # Specify a prefix the uploaded
290
+ # @param [String] prefix
291
+ # @see #key
292
+ # @return [self]
293
+ def key_starts_with(prefix)
294
+ @key_set = true
295
+ starts_with('key', prefix)
296
+ end
297
+
298
+ # @!method acl(canned_acl)
299
+ # Specify the cannedl ACL (access control list) for the object.
300
+ # May be one of the following values:
301
+ #
302
+ # * `private`
303
+ # * `public-read`
304
+ # * `public-read-write`
305
+ # * `authenticated-read`
306
+ # * `bucket-owner-read`
307
+ # * `bucket-owner-full-control`
308
+ #
309
+ # @param [String] canned_acl
310
+ # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html
311
+ # @return [self]
312
+ #
313
+ # @!method acl_starts_with(prefix)
314
+ # @param [String] prefix
315
+ # @see #acl
316
+ # @return [self]
317
+ define_field(:acl, starts_with: true)
318
+
319
+ # @!method cache_control(value)
320
+ # Specify caching behavior along the request/reply chain.
321
+ # @param [String] value
322
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.
323
+ # @return [self]
324
+ #
325
+ # @!method cache_control_starts_with(prefix)
326
+ # @param [String] prefix
327
+ # @see #cache_control
328
+ # @return [self]
329
+ define_field(:cache_control, 'Cache-Control', starts_with: true)
330
+
331
+ # @return [String]
332
+ # @!method content_type(value)
333
+ # A standard MIME type describing the format of the contents.
334
+ # @param [String] value
335
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
336
+ # @return [self]
337
+ #
338
+ # @!method content_type_starts_with(prefix)
339
+ # @param [String] prefix
340
+ # @see #content_type
341
+ # @return [self]
342
+ define_field(:content_type, 'Content-Type', starts_with: true)
343
+
344
+ # @!method content_disposition(value)
345
+ # Specifies presentational information for the object.
346
+ # @param [String] value
347
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.5.1
348
+ # @return [self]
349
+ #
350
+ # @!method content_disposition_starts_with(prefix)
351
+ # @param [String] prefix
352
+ # @see #content_disposition
353
+ # @return [self]
354
+ define_field(:content_disposition, 'Content-Disposition', starts_with: true)
355
+
356
+ # @!method content_encoding(value)
357
+ # Specifies what content encodings have been applied to the object
358
+ # and thus what decoding mechanisms must be applied to obtain the
359
+ # media-type referenced by the Content-Type header field.
360
+ # @param [String] value
361
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
362
+ # @return [self]
363
+ #
364
+ # @!method content_encoding_starts_with(prefix)
365
+ # @param [String] prefix
366
+ # @see #content_encoding
367
+ # @return [self]
368
+ define_field(:content_encoding, 'Content-Encoding', starts_with: true)
369
+
370
+ # The date and time at which the object is no longer cacheable.
371
+ # @note This does not affect the expiration of the presigned post
372
+ # signature.
373
+ # @param [Time] time
374
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
375
+ # @return [self]
376
+ def expires(time)
377
+ with('Expires', time.httpdate)
378
+ end
379
+
380
+ # @param [String] prefix
381
+ # @see #expires
382
+ # @return [self]
383
+ def expires_starts_with(prefix)
384
+ starts_with('Expires', prefix)
385
+ end
386
+
387
+ # The minimum and maximum allowable size for the uploaded content.
388
+ # @param [Range<Integer>] byte_range
389
+ # @return [self]
390
+ def content_length_range(byte_range)
391
+ min = byte_range.begin
392
+ max = byte_range.end
393
+ max -= 1 if byte_range.exclude_end?
394
+ @conditions << ['content-length-range', min, max]
395
+ self
396
+ end
397
+
398
+ # @!method success_action_redirect(value)
399
+ # The URL to which the client is redirected
400
+ # upon successful upload. If {#success_action_redirect} is not
401
+ # specified, Amazon S3 returns the empty document type specified
402
+ # by {#success_action_status}.
403
+ #
404
+ # If Amazon S3 cannot interpret the URL, it acts as if the field
405
+ # is not present. If the upload fails, Amazon S3 displays an error
406
+ # and does not redirect the user to a URL.
407
+ #
408
+ # @param [String] value
409
+ # @return [self]
410
+ #
411
+ # @!method success_action_redirect_starts_with(prefix)
412
+ # @param [String] prefix
413
+ # @see #success_action_redirect
414
+ # @return [self]
415
+ define_field(:success_action_redirect, starts_with: true)
416
+
417
+ # @!method success_action_status(value)
418
+ # The status code returned to the client upon
419
+ # successful upload if {#success_action_redirect} is not
420
+ # specified.
421
+ #
422
+ # Accepts the values `200`, `201`, or `204` (default).
423
+ #
424
+ # If the value is set to 200 or 204, Amazon S3 returns an empty
425
+ # document with a 200 or 204 status code. If the value is set to 201,
426
+ # Amazon S3 returns an XML document with a 201 status code.
427
+ #
428
+ # If the value is not set or if it is set to an invalid value, Amazon
429
+ # S3 returns an empty document with a 204 status code.
430
+ #
431
+ # @param [String] The status code returned to the client upon
432
+ # @return [self]
433
+ define_field(:success_action_status)
434
+
435
+ # @!method storage_class(value)
436
+ # Storage class to use for storing the object. Defaults to
437
+ # `STANDARD`. Must be one of:
438
+ #
439
+ # * `STANDARD`
440
+ # * `REDUCED_REDUNDANCY`
441
+ #
442
+ # You cannot specify `GLACIER` as the storage class. To transition
443
+ # objects to the GLACIER storage class you can use lifecycle
444
+ # configuration.
445
+ # @param [String] value Storage class to use for storing the
446
+ # @return [self]
447
+ define_field(:storage_class, 'x-amz-storage-class')
448
+
449
+ # @!method website_redirect_location(value)
450
+ # If the bucket is configured as a website,
451
+ # redirects requests for this object to another object in the
452
+ # same bucket or to an external URL. Amazon S3 stores this value
453
+ # in the object metadata.
454
+ #
455
+ # The value must be prefixed by, "/", "http://" or "https://".
456
+ # The length of the value is limited to 2K.
457
+ #
458
+ # @param [String] value
459
+ # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
460
+ # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html
461
+ # @see http://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html
462
+ # @return [self]
463
+ define_field(:website_redirect_location, 'x-amz-website-redirect-location')
464
+
465
+ # Metadata hash to store with the uploaded object. Hash keys will be
466
+ # prefixed with "x-amz-meta-".
467
+ # @param [Hash<String,String>] hash
468
+ # @return [self]
469
+ def metadata(hash)
470
+ hash.each do |key, value|
471
+ with("x-amz-meta-#{key}", value)
472
+ end
473
+ self
474
+ end
475
+
476
+ # Specify allowable prefix for each key in the metadata hash.
477
+ # @param [Hash<String,String>] hash
478
+ # @see #metadata
479
+ # @return [self]
480
+ def metadata_starts_with(hash)
481
+ hash.each do |key, value|
482
+ starts_with("x-amz-meta-#{key}", value)
483
+ end
484
+ self
485
+ end
486
+
487
+ # @!endgroup
488
+
489
+ # @!group Server-Side Encryption Fields
490
+
491
+ # @!method server_side_encryption(value)
492
+ # Specifies a server-side encryption algorithm to use when Amazon
493
+ # S3 creates an object. Valid values include:
494
+ #
495
+ # * `aws:kms`
496
+ # * `AES256`
497
+ #
498
+ # @param [String] value
499
+ # @return [self]
500
+ define_field(:server_side_encryption, 'x-amz-server-side-encryption')
501
+
502
+ # @!method server_side_encryption_aws_kms_key_id(value)
503
+ # If {#server_side_encryption} is called with the value of `aws:kms`,
504
+ # this method specifies the ID of the AWS Key Management Service
505
+ # (KMS) master encryption key to use for the object.
506
+ # @param [String] value
507
+ # @return [self]
508
+ define_field(:server_side_encryption_aws_kms_key_id, 'x-amz-server-side-encryption-aws-kms-key-id')
509
+
510
+ # @!endgroup
511
+
512
+ # @!group Server-Side Encryption with Customer-Provided Key Fields
513
+
514
+ # @!method server_side_encryption_customer_algorithm(value)
515
+ # Specifies the algorithm to use to when encrypting the object.
516
+ # Must be set to `AES256` when using customer-provided encryption
517
+ # keys. Must also call {#server_side_encryption_customer_key}.
518
+ # @param [String] value
519
+ # @see #server_side_encryption_customer_key
520
+ # @return [self]
521
+ define_field(:server_side_encryption_customer_algorithm, 'x-amz-server-side-encryption-customer-algorithm')
522
+
523
+ # Specifies the customer-provided encryption key for Amazon S3 to use
524
+ # in encrypting data. This value is used to store the object and then
525
+ # it is discarded; Amazon does not store the encryption key.
526
+ #
527
+ # You must also call {#server_side_encryption_customer_algorithm}.
528
+ #
529
+ # @param [String] value
530
+ # @see #server_side_encryption_customer_algorithm
531
+ # @return [self]
532
+ def server_side_encryption_customer_key(value)
533
+ field_name = 'x-amz-server-side-encryption-customer-key'
534
+ with(field_name, base64(value))
535
+ with(field_name + '-MD5', base64(OpenSSL::Digest::MD5.digest(value)))
536
+ end
537
+
538
+ # @param [String] prefix
539
+ # @see #server_side_encryption_customer_key
540
+ # @return [self]
541
+ def server_side_encryption_customer_key_starts_with(prefix)
542
+ field_name = 'x-amz-server-side-encryption-customer-key'
543
+ starts_with(field_name, prefix)
544
+ end
545
+
546
+ # @!endgroup
547
+
548
+ private
549
+
550
+ def with(field_name, value)
551
+ fvar = '${filename}'
552
+ if index = value.rindex(fvar)
553
+ if index + fvar.size == value.size
554
+ @fields[field_name] = value
555
+ starts_with(field_name, value[0,index])
556
+ else
557
+ msg = "${filename} only supported at the end of #{field_name}"
558
+ raise ArgumentError, msg
559
+ end
560
+ else
561
+ @fields[field_name] = value.to_s
562
+ @conditions << { field_name => value.to_s }
563
+ end
564
+ self
565
+ end
566
+
567
+ def starts_with(field_name, value, &block)
568
+ @conditions << ['starts-with', "$#{field_name}", value.to_s]
569
+ self
570
+ end
571
+
572
+ def check_required_values!
573
+ unless @key_set
574
+ msg = "key required; you must provide a key via :key, "
575
+ msg << ":key_starts_with, or :allow_any => ['key']"
576
+ raise msg
577
+ end
578
+ end
579
+
580
+ def bucket_url
581
+ url = EndpointProvider.resolve(@bucket_region, 's3')
582
+ url = URI.parse(url)
583
+ if Plugins::S3BucketDns.dns_compatible?(@bucket_name, true)
584
+ url.host = @bucket_name + '.' + url.host
585
+ else
586
+ url.path = '/' + @bucket_name
587
+ end
588
+ if @bucket_region == 'us-east-1'
589
+ # keep legacy behavior by default
590
+ url.host = Plugins::S3IADRegionalEndpoint.legacy_host(url.host)
591
+ end
592
+ url.to_s
593
+ end
594
+
595
+ # @return [Hash]
596
+ def policy(datetime)
597
+ check_required_values!
598
+ policy = {}
599
+ policy['expiration'] = @signature_expiration.utc.iso8601
600
+ policy['conditions'] = @conditions.dup
601
+ signature_fields(datetime).each do |name, value|
602
+ policy['conditions'] << { name => value }
603
+ end
604
+ base64(Json.dump(policy))
605
+ end
606
+
607
+ def signature_fields(datetime)
608
+ fields = {}
609
+ fields['x-amz-credential'] = credential_scope(datetime)
610
+ fields['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'
611
+ fields['x-amz-date'] = datetime
612
+ if session_token = @credentials.session_token
613
+ fields['x-amz-security-token'] = session_token
614
+ end
615
+ fields
616
+ end
617
+
618
+ def signature(datetime, string_to_sign)
619
+ k_secret = @credentials.secret_access_key
620
+ k_date = hmac("AWS4" + k_secret, datetime[0,8])
621
+ k_region = hmac(k_date, @bucket_region)
622
+ k_service = hmac(k_region, 's3')
623
+ k_credentials = hmac(k_service, 'aws4_request')
624
+ hexhmac(k_credentials, string_to_sign)
625
+ end
626
+
627
+ def hmac(key, value)
628
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
629
+ end
630
+
631
+ def hexhmac(key, value)
632
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
633
+ end
634
+
635
+ def credential_scope(datetime)
636
+ parts = []
637
+ parts << @credentials.access_key_id
638
+ parts << datetime[0,8]
639
+ parts << @bucket_region
640
+ parts << 's3'
641
+ parts << 'aws4_request'
642
+ parts.join('/')
643
+ end
644
+
645
+ def base64(str)
646
+ Base64.strict_encode64(str)
647
+ end
648
+
649
+ end
650
+ end
651
+ end