shrine-lambda 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eb563c825e410212d1c72e8b6c6204cc9bffffc2
4
+ data.tar.gz: eb4cdd5915f16353b01e003e54eff043cbaa8d5b
5
+ SHA512:
6
+ metadata.gz: a5ccc3f20d9fad0c75a57e37b5efa3e307ff1fce909945828167a21048ba246562239b99ff056ad7d8d48247b7f4fe8d575aab0353274cda4b95458aeb72f46a
7
+ data.tar.gz: 378d0031a0472f926c9887a85690816179d0fb7285ef204250d74b13c752d7707f34200d2c86da0fd68365d1fe4070303aa9f288aedc3276208e6f6f25dd00b4
@@ -0,0 +1,19 @@
1
+ # Change Log
2
+
3
+ ## [0.0.1](https://github.com/texpert/shrine-lambda/tree/0.0.1) (2018-02-12)
4
+ **Merged pull requests:**
5
+
6
+ - Prepairing for release. [\#10](https://github.com/texpert/shrine-lambda/pull/10) ([texpert](https://github.com/texpert))
7
+ - Plugin documentation. [\#9](https://github.com/texpert/shrine-lambda/pull/9) ([texpert](https://github.com/texpert))
8
+ - Comply with Shrine's `upload\_options` plugin to be able to set ACL's … [\#8](https://github.com/texpert/shrine-lambda/pull/8) ([texpert](https://github.com/texpert))
9
+ - Finalized and documented the authorization and saving methods. [\#7](https://github.com/texpert/shrine-lambda/pull/7) ([texpert](https://github.com/texpert))
10
+ - Fixed the class of `lambda\_client`. [\#6](https://github.com/texpert/shrine-lambda/pull/6) ([texpert](https://github.com/texpert))
11
+ - AWS session security token is now used to calculate request signature… [\#5](https://github.com/texpert/shrine-lambda/pull/5) ([texpert](https://github.com/texpert))
12
+ - Authorization of Lambda requests to `callbackURL` implemented by calc… [\#4](https://github.com/texpert/shrine-lambda/pull/4) ([texpert](https://github.com/texpert))
13
+ - `:lambda\_process` methods implementation and `:configure` method refactoring [\#3](https://github.com/texpert/shrine-lambda/pull/3) ([texpert](https://github.com/texpert))
14
+ - Fix of `:configure` and implementation of client and `:lambda\_function\_list` methods. [\#2](https://github.com/texpert/shrine-lambda/pull/2) ([texpert](https://github.com/texpert))
15
+ - Initial gem and plugin config [\#1](https://github.com/texpert/shrine-lambda/pull/1) ([texpert](https://github.com/texpert))
16
+
17
+
18
+
19
+ \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Aurel Branzeanu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,349 @@
1
+ # Shrine::Plugins::Lambda
2
+ Provides [AWS Lambda] integration for [Shrine] File Attachment toolkit for Ruby applications
3
+
4
+ ## Description
5
+
6
+ ### AWS Lambda
7
+
8
+ [AWS Lambda] is a serverless computing platform provided by [Amazon] as a part of the [Amazon Web Services]. It is a
9
+ compute service that could run someone's uploaded code in response to events and/or requests, and automatically manages
10
+ and scales the compute resources required by that code.
11
+
12
+ ### Shrine
13
+
14
+ [Shrine] is the best and most versatile file attachment toolkit for Ruby applications, developed by [Janko
15
+ Marohnić][Janko]. It has a vast collection of plugins with support for direct uploads, background processing and
16
+ deleting, processing on upload or on-the-fly, and ability to use with other ORMs
17
+
18
+ ### Shrine-Lambda
19
+
20
+ Shrine-Lambda is a plugin for invoking [AWS Lambda] functions for processing files already stored in some [AWS S3
21
+ bucket][AWS S3]. Specifically, it was designed for invoking an image resizing [AWS Lambda] function like [this
22
+ one][lambda-image-resize], but it could be used to invoke any other function, due to [Shrine]'s modular plugin
23
+ architecture design.
24
+
25
+ The function is invoked to run asynchronously. Function's result will be sent by [AWS Lambda] back to the
26
+ invoking application in a HTTP request's payload. The HTTP request would target a callback URL specified in the
27
+ Shrine-Lambda's setup. So, the invoking application must provide a HTTP endpoint (a webhook) to catch the results.
28
+
29
+ #### Setup
30
+
31
+ Add Shrine-Lambda gem to the application's Gemfile:
32
+
33
+ ```
34
+ gem 'shrine-lambda'
35
+ ```
36
+
37
+ Run `$ bundle install` command in the application's root folder to install the gem.
38
+
39
+
40
+ Note, that for working with AWS, the AWS credentials (the `access_key_id` and the `secret_access_key`) should be set
41
+ either in the [Shrine] initializer, or in [default profile][AWS profiles] in the `~/.aws` folder.
42
+
43
+ ```
44
+ # config/initializers/shrine.rb:
45
+
46
+ ...
47
+
48
+ s3_options = { access_key_id: 'your_aws_access_key_id',
49
+ secret_access_key: 'your_aws_secret_access_key',
50
+ region: 'your AWS bucket region' }
51
+ ```
52
+
53
+ Also, for Lamda functions to work, various [AWS Lamda permissions] should be managed on the [Amazon Web Services] side.
54
+
55
+ Add to the [Shrine]'s initializer file the Shrine-Lambda plugin registration with the `:callback_url` parameter, and
56
+ the [AWS Lambda] functions list retrieval call (which will retrieve the functions list on application initialization
57
+ and will store the list into the `Shrine.opts[:lambda_function_list]` for further checking):
58
+
59
+ ```
60
+ # config/initializers/shrine.rb:
61
+
62
+ ...
63
+
64
+ Shrine.plugin :lambda, s3_options.merge(callback_url: "https://#{ENV.fetch('APP_HOST')}/lambda")
65
+ Shrine.lambda_function_list
66
+ ```
67
+
68
+ By default, Shrine-Lambda is using the S3 bucket named `:cache` for retrieving the original file, and the `:store`
69
+ named S3 bucket for storing the resulting files.
70
+
71
+ Srine-Lamda uses the [Shrine backgrounding plugin] for asynchronous operation, so this plugin should be also included
72
+ into the Shrine's initializer.
73
+
74
+ Here is a full example of a Shrine initializer of a [Rails] application using [Roda] endpoints for presigned_url's
75
+ (used for direct file uploads to [AWS S3]) and [AWS Lambda] callbacks:
76
+
77
+ ```
78
+ # config/initializers/shrine.rb:
79
+
80
+ # frozen_string_literal: true
81
+
82
+ require 'shrine'
83
+
84
+ if Rails.env.test?
85
+ require 'shrine/storage/file_system'
86
+
87
+ Shrine.storages = {
88
+ cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'),
89
+ store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/store'),
90
+ }
91
+ else
92
+ require 'shrine/storage/s3'
93
+
94
+ secrets = Rails.application.secrets
95
+
96
+ s3_options = { access_key_id: secrets.aws_access_key_id,
97
+ secret_access_key: secrets.aws_secret_access_key,
98
+ region: 'us-east-2' }
99
+
100
+ if Rails.env.production?
101
+ cache_bucket = store_bucket = secrets.aws_s3_bucket
102
+ else
103
+ cache_bucket = 'texpert-test-cache'
104
+ store_bucket = 'texpert-test-store'
105
+ end
106
+
107
+ Shrine.storages = {
108
+ cache: Shrine::Storage::S3.new(prefix: 'cache', **s3_options.merge(bucket: cache_bucket)),
109
+ store: Shrine::Storage::S3.new(prefix: 'store', **s3_options.merge(bucket: store_bucket))
110
+ }
111
+
112
+ lambda_callback_url = if Rails.env.development?
113
+ "http://#{ENV['USER']}.localtunnel.me/rapi/lambda"
114
+ else
115
+ "https://#{ENV.fetch('APP_HOST')}/rapi/lambda"
116
+ end
117
+
118
+ Shrine.plugin :lambda, s3_options.merge(callback_url: lambda_callback_url)
119
+ Shrine.lambda_function_list
120
+
121
+ Shrine.plugin :presign_endpoint, presign_options: ->(request) do
122
+ filename = request.params['filename']
123
+ extension = File.extname(filename)
124
+ content_type = Rack::Mime.mime_type(extension)
125
+
126
+ {
127
+ content_length_range: 0..1.gigabyte, # limit filesize to 1 GB
128
+ content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
129
+ content_type: content_type, # set correct content type
130
+ }
131
+ end
132
+ end
133
+
134
+ Shrine.plugin :activerecord
135
+ Shrine.plugin :backgrounding
136
+ Shrine.plugin :cached_attachment_data # for forms
137
+ Shrine.plugin :logging, logger: Rails.logger
138
+ Shrine.plugin :rack_file # for non-Rails apps
139
+ Shrine.plugin :remote_url, max_size: 1.gigabyte
140
+
141
+ Shrine::Attacher.promote { |data| PromoteJob.perform_later(data) }
142
+ Shrine::Attacher.delete { |data| DeleteJob.perform_later(data) }
143
+
144
+ ```
145
+
146
+ Take notice that the promote job is a default `Shrine::Attacher.promote { |data| PromoteJob.perform_later(data) }`.
147
+ This is made to be able to use other than AWS storages in the test environment (like Shrine's `FileSystem` storage)
148
+ and, also, other uploaders which are not using [AWS Lambda]. This job better to be overrided to a `LambdaPromoteJob`
149
+ directly in the uploaders' classes which will use [AWS Lambda].
150
+
151
+ Another thing used in this initializer is the [localtunnel] application for exposing the localhost to the world for
152
+ catching the Lambda callback requests.
153
+
154
+ #### How it works
155
+
156
+ Shrine-Lamnda works in such a way that an "assembly" should be created in the `LambdaUploader`, which contains all
157
+ the information about how the file should be processed. A random generated string is appended to the assembly, stored
158
+ into the cached file metadata, and used by the Lambda function to sign the requests to the `:lambda_callback_url`,
159
+ along with the `:access_key_id` from the temporary credentials Lambda function is running with.
160
+
161
+ Processing itself happens asynchronously - the invoked Lambda function will issue a PUT HTTP request to the
162
+ `:lambda_callback_url`, specified in the Shrine's initializer, with the request's payload containing the processing
163
+ results.
164
+
165
+ The request should be intercepted by a endpoint at the `:lambda_callback_url`, and its payload transferred to the
166
+ `lambda_save` method on successful request authorization.
167
+
168
+ The authorization is calculatating the HTTP request signature using the random string stored in the cached file and
169
+ the Lambda function's `:access_key_id` received in the request authorization header. Then, the calculated signature is
170
+ compared to the received in the same authorization header Lambda signature.
171
+
172
+ #### Usage
173
+
174
+ Shrine-Lambda assemblies are built inside the `#lambda_process` method in the `LambdaUploader` class:
175
+
176
+ ```
177
+ # app/uploaders/lambda_uploader.rb:
178
+
179
+ # frozen_string_literal: true
180
+
181
+ class LambdaUploader < Uploader
182
+ Attacher.promote { |data| LambdaPromoteJob.perform_later(data) } unless Rails.env.test?
183
+
184
+ plugin :upload_options, store: ->(_io, context) do
185
+ if %i[avatar logo].include?(context[:name])
186
+ {acl: "public-read"}
187
+ else
188
+ {acl: "private"}
189
+ end
190
+ end
191
+
192
+ plugin :versions
193
+
194
+ def lambda_process(io, context)
195
+ assembly = { function: 'ImageResizeOnDemand' } # Here the AWS Lambda function name is specified
196
+
197
+ # Check if the original file format is a image format supported by the Sharp.js library
198
+ if %w[image/gif image/jpeg image/pjpeg image/png image/svg+xml image/tiff image/x-tiff image/webm]
199
+ .include?(io&.data&.dig('metadata', 'mime_type'))
200
+ case context[:name]
201
+ when :avatar
202
+ assembly[:versions] =
203
+ [{ name: :size40, storage: :store, width: 40, height: 40, format: :jpg }]
204
+ when :logo
205
+ assembly[:versions] =
206
+ [{ name: :size270_180, storage: :store, width: 270, height: 180, format: :jpg }]
207
+ when :doc
208
+ assembly[:versions] =
209
+ [
210
+ { name: :size40, storage: :store, width: 40, height: 40, format: :png },
211
+ { name: :size80, storage: :store, width: 80, height: 80, format: :jpg },
212
+ { name: :size120, storage: :store, width: 120, height: 120, format: :jpg }
213
+ ]
214
+ end
215
+ end
216
+ assembly
217
+ end
218
+ end
219
+
220
+ ```
221
+
222
+ The above example is built to interact with the [lambda-image-resize] function, which is using the [Sharp] Javascript
223
+ library for image processing. It is not yet implemented in this function to use the `:target_storage` as default
224
+ bucket for all the processed files, that's why the `:storage` key is specified on every file version. If the file's
225
+ mime type is not supported by the [Sharp] library, no `:versions` will be inserted into the `:assembly` so the
226
+ original file will just be copied to the `:store` S3 bucket.
227
+
228
+ The [Shrine upload_options plugin] is used to specify the S3 bucket ACL and the [Shrine versions plugin] is used to
229
+ enable the uploader to deal with different processed versions of the original file.
230
+
231
+ The default options used by Shrine-Lambda plugin are the following:
232
+
233
+ ```
234
+ { callbackURL: Shrine.opts[:callback_url],
235
+ copy_original: true,
236
+ storages: Shrine.buckets_to_use(%i[cache store]),
237
+ target_storage: :store }
238
+ ```
239
+
240
+ These options could be overrided in the `LambdaUploader` specifying them as the `assembly` keys:
241
+
242
+ ```
243
+ assembly[:callbackURL] = some_callback_url]
244
+ assembly[:copy_original = false # If this is `false`, only the processed file versions will be stored
245
+ assembly[:storages] = Shrine.buckets_to_use(%i[cache store other_store])
246
+ assembly[:target_storage] = :other_store
247
+
248
+ ```
249
+
250
+ Any S3 buckets could be specified, as long as the buckets are defined in the Shrine's initializer file.
251
+
252
+
253
+ #### Webhook
254
+
255
+ A `:callbackUrl` endpoint should be implemented to catch the [AWS Lambda] processing results, authorize, and save them.
256
+ Here is an example of a [Roda] endpoint:
257
+
258
+ ```
259
+ # lib/rapi/base.rb:
260
+
261
+ # frozen_string_literal: true
262
+
263
+ # On Rails autoload is done by ActiveSupport from the `autoload_paths` - no need to require files
264
+ # require 'roda'
265
+ # require 'roda/plugins/json'
266
+ # require 'roda/plugins/static_routing'
267
+
268
+ module RAPI
269
+ class Base < Roda
270
+ plugin :json
271
+ plugin :request_headers
272
+ plugin :static_routing
273
+
274
+ static_put '/lambda' do
275
+ auth_result = Shrine::Attacher.lambda_authorize(request.headers, request.body.read)
276
+ if !auth_result
277
+ response.status = 403
278
+ { 'Error' => 'Signature mismatch' }
279
+ elsif auth_result.is_a?(Array)
280
+ attacher = auth_result[0]
281
+ if attacher.lambda_save(auth_result[1])
282
+ { 'Result' => 'OK' }
283
+ else
284
+ response.status = 500
285
+ { 'Error' => 'Backend record update error' }
286
+ end
287
+ else
288
+ response.status = 500
289
+ { 'Error' => 'Backend Lambda authorization error' }
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ ```
296
+
297
+ #### Backgrounding
298
+
299
+ Even though submitting a Lambda assembly doesn't require any uploading, it still does a HTTP request, so it is better
300
+ to put it into a background job. This is configured in the `LambdaUploader` class:
301
+
302
+ `Attacher.promote { |data| LambdaPromoteJob.perform_later(data) } unless Rails.env.test?`
303
+
304
+ Then the job file should be implemented:
305
+
306
+ ```
307
+ # app/jobs/lambda_promote_job.rb:
308
+
309
+ # frozen_string_literal: true
310
+
311
+ class LambdaPromoteJob < ApplicationJob
312
+ def perform(data)
313
+ Timeout.timeout(30) { Shrine::Attacher.lambda_process(data) }
314
+ end
315
+ end
316
+
317
+ ```
318
+
319
+ ## Inspiration
320
+
321
+ I want to thank [Janko Marohnić][Janko] for the awesome [Shrine] gem and, also, for guiding me to look at his
322
+ implementation of a similar plugin - [Shrine-Transloadit].
323
+
324
+ Also thanks goes to [Tim Uckun] for providing a link to the [article about resizing images on the fly][AWS blog
325
+ article], which pointed me to use the [Sharp] library for image resizing.
326
+
327
+ ## License
328
+
329
+ [MIT](/LICENSE.txt)
330
+
331
+ [Amazon]: https://www.amazon.com
332
+ [Amazon Web Services]: https://aws.amazon.com
333
+ [AWS blog article]: https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/
334
+ [AWS Lambda]: https://aws.amazon.com/lambda
335
+ [AWS Lamda permissions]: https://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html
336
+ [AWS profiles]: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
337
+ [AWS S3]: https://aws.amazon.com/s3/
338
+ [Janko]: https://github.com/janko-m
339
+ [lambda-image-resize]: https://github.com/texpert/lambda-image-resize.js
340
+ [localtunnel]: https://github.com/localtunnel/localtunnel
341
+ [Rails]: http://rubyonrails.org
342
+ [Roda]: http://roda.jeremyevans.net
343
+ [Sharp]: https://github.com/lovell/sharp
344
+ [Shrine]: https://github.com/janko-m/shrine
345
+ [Shrine backgrounding plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Backgrounding.html
346
+ [Shrine-Transloadit]: https://github.com/janko-m/shrine-transloadit
347
+ [Shrine upload_options plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/UploadOptions.html
348
+ [Shrine versions plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Versions.html
349
+ [Tim Uckun]: https://github.com/timuckun
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-lambda'
4
+
5
+ class Shrine
6
+ module Plugins
7
+ module Lambda
8
+ SETTINGS = { access_key_id: :optional,
9
+ callback_url: :required,
10
+ convert_params: :optional,
11
+ endpoint: :optional,
12
+ log_formatter: :optional,
13
+ log_level: :optional,
14
+ logger: :optional,
15
+ profile: :optional,
16
+ region: :optional,
17
+ retry_limit: :optional,
18
+ secret_access_key: :optional,
19
+ session_token: :optional,
20
+ stub_responses: :optional,
21
+ validate_params: :optional }.freeze
22
+
23
+ Error = Class.new(Shrine::Error)
24
+
25
+ # If promoting was not yet overridden, it is set to automatically trigger
26
+ # Lambda processing defined in `Shrine#lambda_process`.
27
+ def self.configure(uploader, settings = {})
28
+ settings.each do |key, value|
29
+ raise Error, "The :#{key} is not supported by the Lambda plugin" unless SETTINGS[key]
30
+ uploader.opts[key] = value || uploader.opts[key]
31
+ if SETTINGS[key] == :required && uploader.opts[key].nil?
32
+ raise Error, "The :#{key} is required for Lambda plugin"
33
+ end
34
+ end
35
+
36
+ uploader.opts[:backgrounding_promote] ||= proc { lambda_process }
37
+ end
38
+
39
+ # It loads the backgrounding plugin, so that it can override promoting.
40
+ def self.load_dependencies(uploader, _opts = {})
41
+ uploader.plugin :backgrounding
42
+ end
43
+
44
+ module AttacherClassMethods
45
+ # Loads the attacher from the data, and triggers its instance AWS Lambda
46
+ # processing method. Intended to be used in a background job.
47
+ def lambda_process(data)
48
+ attacher = load(data)
49
+ attacher.lambda_process(data)
50
+ attacher
51
+ end
52
+
53
+ # Parses the payload of the Lambda request to the `callbackUrl` and loads the Shrine Attacher from the
54
+ # received context.
55
+ # Fetches the signing key from the attacher's record metadata and uses it for calculating the signature of the
56
+ # received from Lambda request. Then it compares the calculated and received signatures, returning an error if
57
+ # the signatures mismatch.
58
+ #
59
+ # If the signatures are equal, it returns the attacher and the hash of the parsed result from Lambda.
60
+ # @param [Hash] headers from the Lambda request
61
+ # @param [String] body of the Lambda request
62
+ # @return [Array] Shrine Attacher and the Lambda result (the request body parsed to a hash).
63
+ def lambda_authorize(headers, body)
64
+ result = JSON.parse(body)
65
+ attacher = load(result.delete('context'))
66
+ incoming_auth_header = auth_header_hash(headers['Authorization'])
67
+
68
+ signer = build_signer(incoming_auth_header['Credential'].split('/'),
69
+ JSON.parse(attacher.record.__send__(:"#{attacher.data_attribute}"))['metadata']['key'],
70
+ headers['x-amz-security-token'])
71
+ signature = signer.sign_request(
72
+ http_method: 'PUT',
73
+ url: Shrine.opts[:callback_url],
74
+ headers: { 'X-Amz-Date' => headers['X-Amz-Date'] },
75
+ body: body
76
+ )
77
+ calculated_signature = auth_header_hash(signature.headers['authorization'])['Signature']
78
+ return false if incoming_auth_header['Signature'] != calculated_signature
79
+ [attacher, result]
80
+ end
81
+
82
+ private
83
+
84
+ def build_signer(headers, secret_access_key, security_token = nil)
85
+ Aws::Sigv4::Signer.new(
86
+ service: headers[3],
87
+ region: headers[2],
88
+ access_key_id: headers[0],
89
+ secret_access_key: secret_access_key,
90
+ session_token: security_token,
91
+ apply_checksum_header: false,
92
+ unsigned_headers: %w[content-length user-agent x-amzn-trace-id]
93
+ )
94
+ end
95
+
96
+ # @param [String] header is the `Authorization` header string
97
+ # @return [Hash] the `Authorization` header string transformed into a Hash
98
+ def auth_header_hash(header)
99
+ auth_header = header.split(/ |, |=/)
100
+ auth_header.shift
101
+ Hash[*auth_header]
102
+ end
103
+ end
104
+
105
+ module AttacherMethods
106
+ # Triggers AWS Lambda processing defined by the user in the uploader's `Shrine#lambda_process`,
107
+ # first checking if the specified Lambda function is available (raising an error if not).
108
+ #
109
+ # Generates a random key, stores the key into the cached file metadata, and passes the key to the Lambda
110
+ # function for signing the request.
111
+ #
112
+ # Stores the DB record class and name, attacher data atribute and uploader class names, into the context
113
+ # attribute of the Lambda function invokation payload. Also stores the cahced file has and the generated path
114
+ # into the payload.
115
+ #
116
+ # After the AWS Lambda function was invoked, it raises a `Shrine::Error`if the response is containing errors.
117
+ # No more response analysis is performed, because Lambda is invoked asynchronously (note the
118
+ # `invocation_type`: 'Event' in the `invoke` call). The results will be sent by Lambda by HTTP requests to
119
+ # the specified `callbackUrl`.
120
+ def lambda_process(data)
121
+ cached_file = uploaded_file(data['attachment'])
122
+ assembly = Shrine.lambda_default_values
123
+ assembly.merge!(store.lambda_process(cached_file, context))
124
+ function = assembly.delete(:function)
125
+ raise Error, 'No Lambda function specified!' unless function
126
+ raise Error, "Function #{function} not available on Lambda!" unless function_available?(function)
127
+
128
+ prepare_assembly(assembly, cached_file, context)
129
+ assembly[:context] = data.except('attachment', 'action', 'phase')
130
+ response = lambda_client.invoke(function_name: function,
131
+ invocation_type: 'Event',
132
+ payload: assembly.to_json)
133
+ raise Error, "#{response.function_error}: #{response.payload.read}" if response.function_error
134
+ swap(cached_file) || _set(cached_file)
135
+ end
136
+
137
+ # Receives the `result` hash after Lambda request was authorized. The result could contain an array of
138
+ # processed file versions data hashes, or a single file data hash, if there were no versions and the original
139
+ # attached file was just moved to the target storage bucket.
140
+ #
141
+ # Deletes the signing key, if it is present in the original file's metadata, converts the result to a JSON
142
+ # string, and writes this string into the `data_attribute` of the Shrine attacher's record.
143
+ #
144
+ # Chooses the `save_methodz` either for the ActiveRecord or for Sequel, and saves the record.
145
+ # @param [Hash] result
146
+ def lambda_save(result)
147
+ versions = result['versions']
148
+ attr_content = if versions
149
+ tmp_hash = versions.inject(:merge!)
150
+ tmp_hash.dig('original', 'metadata')&.delete('key')
151
+ tmp_hash.to_json
152
+ else
153
+ result['metadata']&.delete('key')
154
+ result.to_json
155
+ end
156
+
157
+ record.__send__(:"#{data_attribute}=", attr_content)
158
+ save_method = if record.is_a?(ActiveRecord::Base)
159
+ :save
160
+ elsif record.is_a?(::Sequel::Model)
161
+ :save_changes
162
+ end
163
+ record.__send__(save_method, validate: false)
164
+ end
165
+
166
+ private
167
+
168
+ # A cached instance of an AWS Lambda client.
169
+ def lambda_client
170
+ @lambda_client ||= Shrine.lambda_client
171
+ end
172
+
173
+ # Checks if the specified Lambda function is available.
174
+ # @param [Symbol] function name
175
+ def function_available?(function)
176
+ Shrine.opts[:lambda_function_list].map(&:function_name).include?(function.to_s)
177
+ end
178
+
179
+ def prepare_assembly(assembly, cached_file, context)
180
+ assembly[:path] = store.generate_location(cached_file, context)
181
+ assembly[:storages].each do |s|
182
+ upload_options = get_upload_options(cached_file, context, s)
183
+ s[1][:upload_options] = upload_options if upload_options
184
+ end
185
+ cached_file.metadata['key'] = SecureRandom.base64(12)
186
+ assembly[:attachment] = cached_file
187
+ end
188
+
189
+ def get_upload_options(cached_file, context, storage)
190
+ options = store.opts[:upload_options][storage[0].to_sym]
191
+ options = options.call(cached_file, context) if options.respond_to?(:call)
192
+ options
193
+ end
194
+ end
195
+
196
+ module ClassMethods
197
+ # Creates a new AWS Lambda client
198
+ # @param (see Aws::Lambda::Client#initialize)
199
+ def lambda_client(access_key_id: opts[:access_key_id],
200
+ secret_access_key: opts[:secret_access_key],
201
+ region: opts[:region], **args)
202
+
203
+ Aws::Lambda::Client.new(args.merge!(access_key_id: access_key_id,
204
+ secret_access_key: secret_access_key,
205
+ region: region))
206
+ end
207
+
208
+ # Memoize and returns a list of your Lambda functions. For each function, the
209
+ # response includes the function configuration information.
210
+ #
211
+ # @param (see Aws::Lambda::Client#list_functions)
212
+ # @param force [Boolean] reloading the list via request to AWS if true
213
+ def lambda_function_list(master_region: nil, function_version: 'ALL', marker: nil, items: 100, force: false)
214
+ fl = opts[:lambda_function_list]
215
+ return fl unless force || fl.nil? || fl.empty?
216
+ opts[:lambda_function_list] = lambda_client.list_functions(master_region: master_region,
217
+ function_version: function_version,
218
+ marker: marker,
219
+ max_items: items).functions
220
+ end
221
+
222
+ # @param [Array] buckets that will be sent to Lambda function for use
223
+ def buckets_to_use(buckets)
224
+ buckets.map do |b|
225
+ { b.to_s => { name: Shrine.storages[b].bucket.name, prefix: Shrine.storages[b].prefix } }
226
+ end.inject(:merge!)
227
+ end
228
+
229
+ def lambda_default_values
230
+ { callbackURL: Shrine.opts[:callback_url],
231
+ copy_original: true,
232
+ storages: Shrine.buckets_to_use(%i[cache store]),
233
+ target_storage: :store }
234
+ end
235
+ end
236
+ end
237
+
238
+ register_plugin(:lambda, Lambda)
239
+ end
240
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'shrine-lambda'
5
+ gem.version = '0.0.1'
6
+
7
+ gem.required_ruby_version = '>= 2.3'
8
+
9
+ gem.summary = 'AWS Lambda integration plugin for Shrine.'
10
+ gem.homepage = 'https://github.com/texpert/shrine-lambda'
11
+ gem.authors = ['Aurel Branzeanu']
12
+ gem.description = <<-DESC
13
+ AWS Lambda integration plugin for Shrine File Attachment toolkit for Ruby applications.
14
+ Used for invoking AWS Lambda functions for processing files already stored in some AWS S3 bucket.
15
+ DESC
16
+ gem.email = ['branzeanu.aurel@gmail.com']
17
+ gem.license = 'MIT'
18
+
19
+ gem.files = Dir['CHANGELOG.md', 'README.md', 'LICENSE', 'lib/**/*.rb', '*.gemspec']
20
+ gem.require_path = 'lib/shrine/plugins'
21
+
22
+ gem.metadata = {
23
+ "bug_tracker_uri" => "https://github.com/texpert/shrine-lambda/issues",
24
+ "changelog_uri" => "https://github.com/texpert/shrine-lambda/CHANGELOG.md",
25
+ "source_code_uri" => "https://github.com/texpert/shrine-lambda"
26
+ }
27
+
28
+ gem.add_dependency 'aws-sdk-lambda', '~> 1.0'
29
+ gem.add_dependency 'aws-sdk-s3', '~> 1.2'
30
+ gem.add_dependency 'shrine', '~> 2.6'
31
+
32
+ gem.add_development_dependency 'dotenv'
33
+ gem.add_development_dependency 'rake'
34
+ gem.add_development_dependency 'rubocop', '~> 0.52'
35
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shrine-lambda
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Aurel Branzeanu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-lambda
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.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.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: shrine
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.52'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.52'
97
+ description: |2
98
+ AWS Lambda integration plugin for Shrine File Attachment toolkit for Ruby applications.
99
+ Used for invoking AWS Lambda functions for processing files already stored in some AWS S3 bucket.
100
+ email:
101
+ - branzeanu.aurel@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - CHANGELOG.md
107
+ - LICENSE
108
+ - README.md
109
+ - lib/shrine/plugins/shrine-lambda.rb
110
+ - shrine-lambda.gemspec
111
+ homepage: https://github.com/texpert/shrine-lambda
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ bug_tracker_uri: https://github.com/texpert/shrine-lambda/issues
116
+ changelog_uri: https://github.com/texpert/shrine-lambda/CHANGELOG.md
117
+ source_code_uri: https://github.com/texpert/shrine-lambda
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib/shrine/plugins
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '2.3'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.6.13
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: AWS Lambda integration plugin for Shrine.
138
+ test_files: []