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