shrine-lambda 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +349 -0
- data/lib/shrine/plugins/shrine-lambda.rb +240 -0
- data/shrine-lambda.gemspec +35 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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: []
|