shrine-lambda 0.0.1

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.
@@ -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: []