shrine-aws-lambda 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d57be08878d86a78c04deae0d470e17d442c5c8f0c94b6abf3436c27697314f3
4
+ data.tar.gz: 87437b1050b4d5b095811d409af005ba5f9ce80921fa4ff09c2349f5f2ac6303
5
+ SHA512:
6
+ metadata.gz: 38b075d8edbec8c1309ed0eea89961a3bc3ac3628c0073ed2f1339acc9db7a56160b61435c7624b7c02cd67bd8f113629fb08281fd2c74c09c270c09ee76371d
7
+ data.tar.gz: 22d612ad85d247ca52f0f1e2ad65132052190e8b9d94927045ac66135e1d1d13cc900009f2273db9f31abf75c8b94dc3e426eb34808ca34e137bcffc79f2d1b1
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [v0.1.2](https://github.com/texpert/shrine-aws-lambda/tree/v0.1.2) (2022-07-03)
4
+
5
+ [Full Changelog](https://github.com/texpert/shrine-aws-lambda/compare/88f53efc0436de444f438d36b8a831b4013f5778...v0.1.2)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Initial, compatible with shrine-lambda, release [\#1](https://github.com/texpert/shrine-aws-lambda/pull/1) ([texpert](https://github.com/texpert))
10
+
11
+ ## Changes:
12
+
13
+ - **Breaking:** Change gem name to shrine-aws-lambda
14
+ - **Breaking:** Change class name from Shrine::Plugins::Lambda to Shrine::Plugins::AwsLambda
15
+ - **Breaking:** Change plugin registration symbol from `:lambda` to `:aws_lambda`
16
+
17
+ - Add `.tools-version` file and specified Ruby 2.7.6 version
18
+ - Add rubocop-rspec and rubocop-performane as development dependencies and fix the arised issues
19
+
20
+
21
+
22
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 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,420 @@
1
+ # Shrine::Plugins::AwsLambda
2
+ Provides [AWS Lambda] integration for [Shrine] File Attachment toolkit for Ruby applications
3
+
4
+ This is a gem, renamed from initial [shrine-lambda](https://github.com/texpert/shrine-lambda) to [shrine-aws-lambda](https://github.com/texpert/shrine-aws-lambda)
5
+ for clarity
6
+
7
+ ## Description
8
+
9
+ ### AWS Lambda
10
+
11
+ [AWS Lambda] is a serverless computing platform provided by [Amazon] as a part of the [Amazon Web Services]. It is a
12
+ compute service that could run someone's uploaded code in response to events and/or requests, and automatically manages
13
+ and scales the compute resources required by that code.
14
+
15
+ ### Shrine
16
+
17
+ [Shrine] is the best and most versatile file attachment toolkit for Ruby applications, developed by [Janko
18
+ Marohnić][Janko]. It has a vast collection of plugins with support for direct uploads, background processing and
19
+ deleting, processing on upload or on-the-fly, and ability to use with other ORMs
20
+
21
+ ### Shrine-AWS-Lambda
22
+
23
+ Shrine-AWS-Lambda is a plugin for invoking [AWS Lambda] functions for processing files already stored in some [AWS S3
24
+ bucket][AWS S3]. Specifically, it was designed for invoking an image resizing [AWS Lambda] function like [this
25
+ one][lambda-image-resize], but it could be used to invoke any other function, due to [Shrine]'s modular plugin
26
+ architecture design.
27
+
28
+ The function is invoked to run asynchronously. Function's result will be sent by [AWS Lambda] back to the
29
+ invoking application in a HTTP request's payload. The HTTP request would target a callback URL specified in the
30
+ Shrine-AWS-Lambda's setup. So, the invoking application must provide a HTTP endpoint (a webhook) to catch the results.
31
+
32
+ #### Setup
33
+
34
+ Add the Shrine-AWS-Lambda gem to the application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'shrine-aws-lambda'
38
+ ```
39
+
40
+ Run `$ bundle install` command in the application's root folder to install the gem.
41
+
42
+
43
+ Note, that for working with AWS, the AWS credentials (the `access_key_id` and the `secret_access_key`) should be set
44
+ either in the [Shrine] initializer, or in [default profile][AWS profiles] in the `~/.aws` folder.
45
+
46
+ ```ruby
47
+ # config/initializers/shrine.rb:
48
+
49
+ # ...
50
+
51
+ s3_options = { access_key_id: 'your_aws_access_key_id',
52
+ secret_access_key: 'your_aws_secret_access_key',
53
+ region: 'your AWS bucket region' }
54
+ ```
55
+
56
+ Also, for Lamda functions to work, various [AWS Lamda permissions] should be managed on the [Amazon Web Services] side.
57
+
58
+ Add to the [Shrine]'s initializer file the Shrine-AWS-Lambda plugin registration with the `:callback_url` parameter,
59
+ and the [AWS Lambda] functions list retrieval call (which will retrieve the functions list on application initialization
60
+ and will store the list into the `Shrine.opts[:lambda_function_list]` for further checking):
61
+
62
+ ```ruby
63
+ # config/initializers/shrine.rb:
64
+
65
+ # ...
66
+
67
+ shrine.plugin :aws_lambda, s3_options.merge(callback_url: "https://#{ENV.fetch('APP_HOST')}/lambda")
68
+ Shrine.lambda_function_list
69
+ ```
70
+
71
+ By default, Shrine-AWS-Lambda is using the S3 bucket named `:cache` for retrieving the original file, and the `:store`
72
+ named S3 bucket for storing the resulting files.
73
+
74
+ Srine-Lamda uses the [Shrine backgrounding plugin] for asynchronous operation, so this plugin should be also included
75
+ into the Shrine's initializer.
76
+
77
+ Here is a full example of a Shrine initializer of a [Rails] application using [Roda] endpoints for presigned_url's
78
+ (used for direct file uploads to [AWS S3]) and [AWS Lambda] callbacks:
79
+
80
+ ```ruby
81
+ # config/initializers/shrine.rb:
82
+
83
+ # frozen_string_literal: true
84
+
85
+ require 'shrine'
86
+
87
+ if Rails.env.test?
88
+ require 'shrine/storage/file_system'
89
+
90
+ Shrine.storages = {
91
+ cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'),
92
+ store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/store'),
93
+ }
94
+ else
95
+ require 'shrine/storage/s3'
96
+
97
+ secrets = Rails.application.secrets
98
+
99
+ s3_options = { access_key_id: secrets.aws_access_key_id,
100
+ secret_access_key: secrets.aws_secret_access_key,
101
+ region: 'us-east-2' }
102
+
103
+ if Rails.env.production?
104
+ cache_bucket = store_bucket = secrets.aws_s3_bucket
105
+ else
106
+ cache_bucket = 'texpert-test-cache'
107
+ store_bucket = 'texpert-test-store'
108
+ end
109
+
110
+ Shrine.storages = {
111
+ cache: Shrine::Storage::S3.new(prefix: 'cache', **s3_options.merge(bucket: cache_bucket)),
112
+ store: Shrine::Storage::S3.new(prefix: 'store', **s3_options.merge(bucket: store_bucket))
113
+ }
114
+
115
+ lambda_callback_url = if Rails.env.development?
116
+ "http://#{ENV['USER']}.localtunnel.me/rapi/lambda"
117
+ else
118
+ "https://#{ENV.fetch('APP_HOST')}/rapi/lambda"
119
+ end
120
+
121
+ shrine.plugin :aws_lambda, s3_options.merge(callback_url: lambda_callback_url)
122
+ Shrine.lambda_function_list
123
+
124
+ Shrine.plugin :presign_endpoint, presign_options: ->(request) do
125
+ filename = request.params['filename']
126
+ extension = File.extname(filename)
127
+ content_type = Rack::Mime.mime_type(extension)
128
+
129
+ {
130
+ content_length_range: 0..1.gigabyte, # limit filesize to 1 GB
131
+ content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
132
+ content_type: content_type, # set correct content type
133
+ }
134
+ end
135
+ end
136
+
137
+ Shrine.plugin :activerecord
138
+ Shrine.plugin :backgrounding
139
+ Shrine.plugin :cached_attachment_data # for forms
140
+ Shrine.plugin :logging, logger: Rails.logger
141
+ Shrine.plugin :rack_file # for non-Rails apps
142
+ Shrine.plugin :remote_url, max_size: 1.gigabyte
143
+
144
+ Shrine::Attacher.promote { |data| PromoteJob.perform_later(data) }
145
+ Shrine::Attacher.delete { |data| DeleteJob.perform_later(data) }
146
+
147
+ ```
148
+
149
+ Take notice that the promote job is a default `Shrine::Attacher.promote { |data| PromoteJob.perform_later(data) }`.
150
+ This is made to be able to use other than AWS storages in the test environment (like Shrine's `FileSystem` storage)
151
+ and, also, other uploaders which are not using [AWS Lambda]. This job better to be overrided to a `LambdaPromoteJob`
152
+ directly in the uploaders' classes which will use [AWS Lambda].
153
+
154
+ Another thing used in this initializer is the [localtunnel] application for exposing the localhost to the world for
155
+ catching the Lambda callback requests.
156
+
157
+ #### How it works
158
+
159
+ Shrine-Lamnda works in such a way that an "assembly" should be created in the `LambdaUploader`, which contains all
160
+ the information about how the file should be processed. A random generated string is appended to the assembly, stored
161
+ into the cached file metadata, and used by the Lambda function to sign the requests to the `:lambda_callback_url`,
162
+ along with the `:access_key_id` from the temporary credentials Lambda function is running with.
163
+
164
+ Processing itself happens asynchronously - the invoked Lambda function will issue a PUT HTTP request to the
165
+ `:lambda_callback_url`, specified in the Shrine's initializer, with the request's payload containing the processing
166
+ results.
167
+
168
+ The request should be intercepted by a endpoint at the `:lambda_callback_url`, and its payload transferred to the
169
+ `lambda_save` method on successful request authorization.
170
+
171
+ The authorization is calculatating the HTTP request signature using the random string stored in the cached file and
172
+ the Lambda function's `:access_key_id` received in the request authorization header. Then, the calculated signature is
173
+ compared to the received in the same authorization header Lambda signature.
174
+
175
+ #### Usage
176
+
177
+ Shrine-AWS-Lambda assemblies are built inside the `#lambda_process_versions` method in the `LambdaUploader` class:
178
+
179
+ ```ruby
180
+ # app/uploaders/lambda_uploader.rb:
181
+
182
+ # frozen_string_literal: true
183
+
184
+ class LambdaUploader < Uploader
185
+ Attacher.promote { |data| LambdaPromoteJob.perform_later(data) } unless Rails.env.test?
186
+
187
+ plugin :upload_options, store: ->(_io, context) do
188
+ if %i[avatar logo].include?(context[:name])
189
+ {acl: "public-read"}
190
+ else
191
+ {acl: "private"}
192
+ end
193
+ end
194
+
195
+ plugin :versions
196
+
197
+ def lambda_process_versions(io, context)
198
+ assembly = { function: 'ImageResizeOnDemand' } # Here the AWS Lambda function name is specified
199
+
200
+ # Check if the original file format is a image format supported by the Sharp.js library
201
+ if %w[image/gif image/jpeg image/pjpeg image/png image/svg+xml image/tiff image/x-tiff image/webm]
202
+ .include?(io&.data&.dig('metadata', 'mime_type'))
203
+ case context[:name]
204
+ when :avatar
205
+ assembly[:versions] =
206
+ [{ name: :size40, storage: :store, width: 40, height: 40, format: :jpg }]
207
+ when :logo
208
+ assembly[:versions] =
209
+ [{ name: :size270_180, storage: :store, width: 270, height: 180, format: :jpg }]
210
+ when :doc
211
+ assembly[:versions] =
212
+ [
213
+ { name: :size40, storage: :store, width: 40, height: 40, format: :png },
214
+ { name: :size80, storage: :store, width: 80, height: 80, format: :jpg },
215
+ { name: :size120, storage: :store, width: 120, height: 120, format: :jpg }
216
+ ]
217
+ end
218
+ end
219
+ assembly
220
+ end
221
+ end
222
+
223
+ ```
224
+
225
+ The above example is built to interact with the [lambda-image-resize] function, which is using the [Sharp] Javascript
226
+ library for image processing. It is not yet implemented in this function to use the `:target_storage` as default
227
+ bucket for all the processed files, that's why the `:storage` key is specified on every file version. If the file's
228
+ mime type is not supported by the [Sharp] library, no `:versions` will be inserted into the `:assembly` so the
229
+ original file will just be copied to the `:store` S3 bucket.
230
+
231
+ The [Shrine upload_options plugin] is used to specify the S3 bucket ACL and the [Shrine versions plugin] is used to
232
+ enable the uploader to deal with different processed versions of the original file.
233
+
234
+ The default options used by Shrine-AWS-Lambda plugin are the following:
235
+
236
+ ```ruby
237
+ { callbackURL: Shrine.opts[:callback_url],
238
+ copy_original: true,
239
+ storages: Shrine.buckets_to_use(%i[cache store]),
240
+ target_storage: :store }
241
+ ```
242
+
243
+ These options could be overrided in the `LambdaUploader` specifying them as the `assembly` keys:
244
+
245
+ ```ruby
246
+ assembly[:callbackURL] = some_callback_url]
247
+ assembly[:copy_original = false # If this is `false`, only the processed file versions will be stored
248
+ assembly[:storages] = Shrine.buckets_to_use(%i[cache store other_store])
249
+ assembly[:target_storage] = :other_store
250
+
251
+ ```
252
+
253
+ Any S3 buckets could be specified, as long as the buckets are defined in the Shrine's initializer file.
254
+
255
+
256
+ #### Webhook
257
+
258
+ A `:callbackUrl` endpoint should be implemented to catch the [AWS Lambda] processing results, authorize, and save them.
259
+ Here is an example of a [Roda] endpoint:
260
+
261
+ ```ruby
262
+ # lib/rapi/base.rb:
263
+
264
+ # frozen_string_literal: true
265
+
266
+ # On Rails autoload is done by ActiveSupport from the `autoload_paths` - no need to require files
267
+ # require 'roda'
268
+ # require 'roda/plugins/json'
269
+ # require 'roda/plugins/static_routing'
270
+
271
+ module RAPI
272
+ class Base < Roda
273
+ plugin :json
274
+ plugin :request_headers
275
+ plugin :static_routing
276
+
277
+ static_put '/lambda' do
278
+ auth_result = Shrine::Attacher.lambda_authorize(request.headers, request.body.read)
279
+ if !auth_result
280
+ response.status = 403
281
+ { 'Error' => 'Signature mismatch' }
282
+ elsif auth_result.is_a?(Array)
283
+ attacher = auth_result[0]
284
+ if attacher.lambda_save(auth_result[1])
285
+ { 'Result' => 'OK' }
286
+ else
287
+ response.status = 500
288
+ { 'Error' => 'Backend record update error' }
289
+ end
290
+ else
291
+ response.status = 500
292
+ { 'Error' => 'Backend Lambda authorization error' }
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ ```
299
+
300
+ #### Backgrounding
301
+
302
+ Even though submitting a Lambda assembly doesn't require any uploading, it still does a HTTP request, so it is better
303
+ to put it into a background job. This is configured in the `LambdaUploader` class:
304
+
305
+ `Attacher.promote { |data| LambdaPromoteJob.perform_later(data) } unless Rails.env.test?`
306
+
307
+ Then the job file should be implemented:
308
+
309
+ ```ruby
310
+ # app/jobs/lambda_promote_job.rb:
311
+
312
+ # frozen_string_literal: true
313
+
314
+ class LambdaPromoteJob < ApplicationJob
315
+ def perform(data)
316
+ Timeout.timeout(30) { Shrine::Attacher.lambda_process(data) }
317
+ end
318
+ end
319
+ ```
320
+
321
+ ### Gem Maintenance
322
+
323
+ #### Preparing a release
324
+
325
+ Merge all the pull requests that should make it into the new release into the `main` branch, then checkout and pull the
326
+ branch and run the `github_changelog_generator`, specifying the new version as a `--future-release` command line
327
+ parameter:
328
+
329
+ ```bash
330
+ $ git checkout main
331
+ $ git pull
332
+
333
+ $ github_changelog_generator -u texpert -p shrine-aws-lambda --future-release v0.1.1
334
+ ```
335
+
336
+ Then add the changes to `git`, commit and push the `Preparing the new release` commit directly into the `main` branch:
337
+
338
+ ```bash
339
+ $ git add .
340
+ $ git commit -m 'Preparing the new v0.1.1 release'
341
+ $ git push
342
+ ```
343
+
344
+ #### RubyGems credentials
345
+
346
+ Ensure you have the RubyGems credentials located in the `~/.gem/credentials` file.
347
+
348
+ #### Adding a gem owner
349
+
350
+ ```bash
351
+ $ gem owner shrine-aws-lambda -a friend@example.com
352
+ ```
353
+
354
+ #### Building a new gem version
355
+
356
+ Adjust the new gem version number in the `lib/shrine/plugins/lambda/version.rb` file. It is used when building the gem
357
+ by the following command:
358
+
359
+ ```bash
360
+ $ gem build shrine-aws-lambda.gemspec
361
+ ```
362
+
363
+ Assuming the version was set to `0.1.2`, a `shrine-aws-lambda-0.1.2.gem` binary file will be generated at the root of
364
+ the app (repo).
365
+
366
+ - The binary file shouldn't be added into the `git` tree, it will be pushed into the RubyGems and to the GitHub releases
367
+
368
+ #### Pushing a new gem release to RubyGems
369
+
370
+ ```bash
371
+ $ gem push shrine-aws-lambda-0.1.2.gem # don't forget to specify the correct version number
372
+ ```
373
+
374
+ #### Crafting the new release on GitHub
375
+
376
+ On the [Releases page](https://github.com/texpert/shrine-aws-lambda/releases) push the `Draft a new release` button.
377
+
378
+ The new release editing page opens, on which the following actions could be taken:
379
+
380
+ - Choose the repo branch (default is `main`)
381
+ - Insert a tag version (usually, the tag should correspond to the gem's new version, v0.0.1, for example)
382
+ - the tag will be created by GitHub on the last commit into the chosen branch
383
+ - Fill the release Title and Description
384
+ - Attach the binary file with the generated gem version
385
+ - If the release is not yet ready for production, mark the `This is a pre-release` checkbox
386
+ - Press either the `Publish release`, or the `Save draft button` if you want to publish it later
387
+ - After publishing the release, the the binary gem file will be available on GitHub and could be removed locally
388
+
389
+
390
+ ## Inspiration
391
+
392
+ I want to thank [Janko Marohnić][Janko] for the awesome [Shrine] gem and, also, for guiding me to look at his
393
+ implementation of a similar plugin - [Shrine-Transloadit].
394
+
395
+ Also thanks goes to [Tim Uckun] for providing a link to the [article about resizing images on the fly][AWS blog
396
+ article], which pointed me to use the [Sharp] library for image resizing.
397
+
398
+ ## License
399
+
400
+ [MIT](/LICENSE.txt)
401
+
402
+ [Amazon]: https://www.amazon.com
403
+ [Amazon Web Services]: https://aws.amazon.com
404
+ [AWS blog article]: https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/
405
+ [AWS Lambda]: https://aws.amazon.com/lambda
406
+ [AWS Lamda permissions]: https://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html
407
+ [AWS profiles]: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
408
+ [AWS S3]: https://aws.amazon.com/s3/
409
+ [Janko]: https://github.com/janko-m
410
+ [lambda-image-resize]: https://github.com/texpert/lambda-image-resize.js
411
+ [localtunnel]: https://github.com/localtunnel/localtunnel
412
+ [Rails]: http://rubyonrails.org
413
+ [Roda]: http://roda.jeremyevans.net
414
+ [Sharp]: https://github.com/lovell/sharp
415
+ [Shrine]: https://github.com/janko-m/shrine
416
+ [Shrine backgrounding plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Backgrounding.html
417
+ [Shrine-Transloadit]: https://github.com/janko-m/shrine-transloadit
418
+ [Shrine upload_options plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/UploadOptions.html
419
+ [Shrine versions plugin]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Versions.html
420
+ [Tim Uckun]: https://github.com/timuckun
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ module AwsLambda
6
+ VERSION = '0.1.2'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-lambda'
4
+ require 'shrine'
5
+
6
+ class Shrine
7
+ module Plugins
8
+ module AwsLambda
9
+ SETTINGS = { access_key_id: :optional,
10
+ callback_url: :required,
11
+ convert_params: :optional,
12
+ endpoint: :optional,
13
+ log_formatter: :optional,
14
+ log_level: :optional,
15
+ logger: :optional,
16
+ profile: :optional,
17
+ region: :optional,
18
+ retry_limit: :optional,
19
+ secret_access_key: :optional,
20
+ session_token: :optional,
21
+ stub_responses: :optional,
22
+ validate_params: :optional }.freeze
23
+
24
+ Error = Class.new(Shrine::Error)
25
+
26
+ # If promoting was not yet overridden, it is set to automatically trigger
27
+ # Lambda processing defined in `Shrine#lambda_process`.
28
+ def self.configure(uploader, settings = {})
29
+ SETTINGS.each do |key, value|
30
+ raise Error, "The :#{key} option is required for Lambda plugin" if value == :required && settings[key].nil?
31
+
32
+ uploader.opts[key] = settings.delete(key) if settings[key]
33
+ end
34
+
35
+ uploader.opts[:backgrounding_promote] = proc { lambda_process }
36
+
37
+ return unless logger
38
+
39
+ settings.each do |key, _value|
40
+ logger.info "The :#{key} option is not supported by the Lambda plugin"
41
+ end
42
+ end
43
+
44
+ def self.logger
45
+ return @logger if defined?(@logger)
46
+
47
+ @logger = if Shrine.respond_to?(:logger)
48
+ Shrine.logger
49
+ elsif uploader.respond_to?(:logger)
50
+ uploader.logger
51
+ end
52
+ end
53
+
54
+ # It loads the backgrounding plugin, so that it can override promoting.
55
+ def self.load_dependencies(uploader, _opts = {})
56
+ uploader.plugin :backgrounding
57
+ end
58
+
59
+ module AttacherClassMethods
60
+ # Loads the attacher from the data, and triggers its instance AWS Lambda
61
+ # processing method. Intended to be used in a background job.
62
+ def lambda_process(data)
63
+ attacher = load(data)
64
+ attacher.lambda_process(data)
65
+ attacher
66
+ end
67
+
68
+ # Parses the payload of the Lambda request to the `callbackUrl` and loads the Shrine Attacher from the
69
+ # received context.
70
+ # Fetches the signing key from the attacher's record metadata and uses it for calculating the signature of the
71
+ # received from Lambda request. Then it compares the calculated and received signatures, returning an error if
72
+ # the signatures mismatch.
73
+ #
74
+ # If the signatures are equal, it returns the attacher and the hash of the parsed result from Lambda, else -
75
+ # it returns false.
76
+ # @param [Hash] headers from the Lambda request
77
+ # @option headers [String] 'User-Agent' The AWS Lambda function user agent
78
+ # @option headers [String] 'Content-Type' 'application/json'
79
+ # @option headers [String] 'Host'
80
+ # @option headers [String] 'X-Amz-Date' The AWS Lambda function user agent
81
+ # @option headers [String] 'Authorization' The AWS authorization string
82
+ # @param [String] body of the Lambda request
83
+ # @return [Array] Shrine Attacher and the Lambda result (the request body parsed to a hash) if signature in
84
+ # received headers matches locally computed AWS signature
85
+ # @return [false] if signature in received headers does't match locally computed AWS signature
86
+ def lambda_authorize(headers, body)
87
+ result = JSON.parse(body)
88
+ attacher = load(result.delete('context'))
89
+ incoming_auth_header = auth_header_hash(headers['Authorization'])
90
+
91
+ signer = build_signer(
92
+ incoming_auth_header['Credential'].split('/'),
93
+ JSON.parse(attacher.record.__send__(:"#{attacher.data_attribute}") || '{}').dig('metadata', 'key') || 'key',
94
+ headers['x-amz-security-token']
95
+ )
96
+ signature = signer.sign_request(http_method: 'PUT',
97
+ url: Shrine.opts[:callback_url],
98
+ headers: { 'X-Amz-Date' => headers['X-Amz-Date'] },
99
+ body: body)
100
+ calculated_signature = auth_header_hash(signature.headers['authorization'])['Signature']
101
+ return false if incoming_auth_header['Signature'] != calculated_signature
102
+
103
+ [attacher, result]
104
+ end
105
+
106
+ private
107
+
108
+ def build_signer(headers, secret_access_key, security_token = nil)
109
+ Aws::Sigv4::Signer.new(
110
+ service: headers[3],
111
+ region: headers[2],
112
+ access_key_id: headers[0],
113
+ secret_access_key: secret_access_key,
114
+ session_token: security_token,
115
+ apply_checksum_header: false,
116
+ unsigned_headers: %w[content-length user-agent x-amzn-trace-id]
117
+ )
118
+ end
119
+
120
+ # @param [String] header is the `Authorization` header string
121
+ # @return [Hash] the `Authorization` header string transformed into a Hash
122
+ def auth_header_hash(header)
123
+ auth_header = header.split(/ |, |=/)
124
+ auth_header.shift
125
+ Hash[*auth_header]
126
+ end
127
+ end
128
+
129
+ module AttacherMethods
130
+ # Triggers AWS Lambda processing defined by the user in the uploader's `Shrine#lambda_process`,
131
+ # first checking if the specified Lambda function is available (raising an error if not).
132
+ #
133
+ # Generates a random key, stores the key into the cached file metadata, and passes the key to the Lambda
134
+ # function for signing the request.
135
+ #
136
+ # Stores the DB record class and name, attacher data atribute and uploader class names, into the context
137
+ # attribute of the Lambda function invokation payload. Also stores the cached file hash object and the
138
+ # generated path into the payload.
139
+ #
140
+ # After the AWS Lambda function invocation, a `Shrine::Error` will be raised if the response is containing
141
+ # errors. No more response analysis is performed, because Lambda is invoked asynchronously (note the
142
+ # `invocation_type`: 'Event' in the `invoke` call). The results will be sent by Lambda by HTTP requests to
143
+ # the specified `callbackUrl`.
144
+ def lambda_process(data)
145
+ cached_file = uploaded_file(data['attachment'])
146
+ assembly = lambda_default_values
147
+ assembly.merge!(store.lambda_process_versions(cached_file, context))
148
+ function = assembly.delete(:function)
149
+ raise Error, 'No Lambda function specified!' unless function
150
+ raise Error, "Function #{function} not available on Lambda!" unless function_available?(function)
151
+
152
+ prepare_assembly(assembly, cached_file, context)
153
+ assembly[:context] = data.except('attachment', 'action', 'phase')
154
+ response = lambda_client.invoke(function_name: function,
155
+ invocation_type: 'Event',
156
+ payload: assembly.to_json)
157
+ raise Error, "#{response.function_error}: #{response.payload.read}" if response.function_error
158
+
159
+ swap(cached_file) || _set(cached_file)
160
+ end
161
+
162
+ # Receives the `result` hash after Lambda request was authorized. The result could contain an array of
163
+ # processed file versions data hashes, or a single file data hash, if there were no versions and the original
164
+ # attached file was just moved to the target storage bucket.
165
+ #
166
+ # Deletes the signing key, if it is present in the original file's metadata, converts the result to a JSON
167
+ # string, and writes this string into the `data_attribute` of the Shrine attacher's record.
168
+ #
169
+ # Chooses the `save_method` either for the ActiveRecord or for Sequel, and saves the record.
170
+ # @param [Hash] result
171
+ def lambda_save(result)
172
+ versions = result['versions']
173
+ attr_content = if versions
174
+ tmp_hash = versions.reduce(:merge!)
175
+ tmp_hash.dig('original', 'metadata')&.delete('key')
176
+ tmp_hash.to_json
177
+ else
178
+ result['metadata']&.delete('key')
179
+ result.to_json
180
+ end
181
+
182
+ record.__send__(:"#{data_attribute}=", attr_content)
183
+ save_method = case record
184
+ when ActiveRecord::Base
185
+ :save
186
+ when ::Sequel::Model
187
+ :save_changes
188
+ end
189
+ record.__send__(save_method, validate: false)
190
+ end
191
+
192
+ private
193
+
194
+ def lambda_default_values
195
+ { callbackURL: Shrine.opts[:callback_url],
196
+ copy_original: true,
197
+ storages: buckets_to_use(%i[cache store]),
198
+ target_storage: :store }
199
+ end
200
+
201
+ # @param [Array] buckets that will be sent to Lambda function for use
202
+ def buckets_to_use(buckets)
203
+ buckets.map do |b|
204
+ { b.to_s => { name: Shrine.storages[b].bucket.name, prefix: Shrine.storages[b].prefix } }
205
+ end.reduce(:merge!)
206
+ end
207
+
208
+ # A cached instance of an AWS Lambda client.
209
+ def lambda_client
210
+ @lambda_client ||= Shrine.lambda_client
211
+ end
212
+
213
+ # Checks if the specified Lambda function is available.
214
+ # @param [Symbol] function name
215
+ def function_available?(function)
216
+ Shrine.opts[:lambda_function_list].map(&:function_name).include?(function.to_s)
217
+ end
218
+
219
+ def prepare_assembly(assembly, cached_file, context)
220
+ assembly[:path] = store.generate_location(cached_file, context)
221
+ assembly[:storages].each do |s|
222
+ upload_options = get_upload_options(cached_file, context, s)
223
+ s[1][:upload_options] = upload_options if upload_options
224
+ end
225
+ cached_file.metadata['key'] = SecureRandom.base64(12)
226
+ assembly[:attachment] = cached_file
227
+ end
228
+
229
+ def get_upload_options(cached_file, context, storage)
230
+ options = store.opts[:upload_options][storage[0].to_sym]
231
+ options = options.call(cached_file, context) if options.respond_to?(:call)
232
+ options
233
+ end
234
+ end
235
+
236
+ module ClassMethods
237
+ # Creates a new AWS Lambda client
238
+ # @param (see Aws::Lambda::Client#initialize)
239
+ def lambda_client(access_key_id: opts[:access_key_id],
240
+ secret_access_key: opts[:secret_access_key],
241
+ region: opts[:region], **args)
242
+
243
+ Aws::Lambda::Client.new(args.merge!(access_key_id: access_key_id,
244
+ secret_access_key: secret_access_key,
245
+ region: region))
246
+ end
247
+
248
+ # Memoize and returns a list of your Lambda functions. For each function, the
249
+ # response includes the function configuration information.
250
+ #
251
+ # @param (see Aws::Lambda::Client#list_functions)
252
+ # @param force [Boolean] reloading the list via request to AWS if true
253
+ def lambda_function_list(master_region: nil, function_version: 'ALL', marker: nil, items: 100, force: false)
254
+ fl = opts[:lambda_function_list]
255
+ return fl unless force || fl.nil? || fl.empty?
256
+
257
+ opts[:lambda_function_list] = lambda_client.list_functions(master_region: master_region,
258
+ function_version: function_version,
259
+ marker: marker,
260
+ max_items: items).functions
261
+ end
262
+ end
263
+ end
264
+
265
+ register_plugin(:aws_lambda, AwsLambda)
266
+ end
267
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+
5
+ require 'shrine/plugins/aws_lambda/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = 'shrine-aws-lambda'
9
+ gem.version = Shrine::Plugins::AwsLambda::VERSION
10
+ gem.authors = ['Aurel Branzeanu']
11
+ gem.email = ['branzeanu.aurel@gmail.com']
12
+ gem.homepage = 'https://github.com/texpert/shrine-aws-lambda'
13
+ gem.summary = 'AWS Lambda integration plugin for Shrine.'
14
+ gem.description = <<~DESC
15
+ AWS Lambda integration plugin for Shrine File Attachment toolkit for Ruby applications.
16
+ Used for invoking AWS Lambda functions for processing files already stored in some AWS S3 bucket.
17
+ DESC
18
+ gem.license = 'MIT'
19
+ gem.files = Dir['CHANGELOG.md', 'README.md', 'LICENSE', 'lib/**/*.rb', '*.gemspec']
20
+ gem.require_path = 'lib'
21
+
22
+ gem.metadata = { 'bug_tracker_uri' => 'https://github.com/texpert/shrine-aws-lambda/issues',
23
+ 'changelog_uri' => 'https://github.com/texpert/shrine-aws-lambda/CHANGELOG.md',
24
+ 'source_code_uri' => 'https://github.com/texpert/shrine-aws-lambda',
25
+ 'rubygems_mfa_required' => 'true' }
26
+
27
+ gem.required_ruby_version = '>= 2.7'
28
+
29
+ gem.add_dependency 'aws-sdk-lambda', '~> 1.0'
30
+ gem.add_dependency 'aws-sdk-s3', '~> 1.2'
31
+ gem.add_dependency 'shrine', '~> 2.6'
32
+
33
+ gem.add_development_dependency 'activerecord', '>= 4.2.0'
34
+ gem.add_development_dependency 'dotenv'
35
+ gem.add_development_dependency 'github_changelog_generator'
36
+ gem.add_development_dependency 'rake'
37
+ gem.add_development_dependency 'rspec'
38
+ gem.add_development_dependency 'rubocop'
39
+ gem.add_development_dependency 'rubocop-performance'
40
+ gem.add_development_dependency 'rubocop-rspec'
41
+ gem.add_development_dependency 'sqlite3' unless RUBY_ENGINE == 'jruby'
42
+
43
+ gem.post_install_message = <<~POSTINSTALL
44
+ POSTINSTALL
45
+ end
metadata ADDED
@@ -0,0 +1,223 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shrine-aws-lambda
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Aurel Branzeanu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-03 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: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: dotenv
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: github_changelog_generator
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-performance
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: |
182
+ AWS Lambda integration plugin for Shrine File Attachment toolkit for Ruby applications.
183
+ Used for invoking AWS Lambda functions for processing files already stored in some AWS S3 bucket.
184
+ email:
185
+ - branzeanu.aurel@gmail.com
186
+ executables: []
187
+ extensions: []
188
+ extra_rdoc_files: []
189
+ files:
190
+ - CHANGELOG.md
191
+ - LICENSE
192
+ - README.md
193
+ - lib/shrine/plugins/aws_lambda.rb
194
+ - lib/shrine/plugins/aws_lambda/version.rb
195
+ - shrine-aws-lambda.gemspec
196
+ homepage: https://github.com/texpert/shrine-aws-lambda
197
+ licenses:
198
+ - MIT
199
+ metadata:
200
+ bug_tracker_uri: https://github.com/texpert/shrine-aws-lambda/issues
201
+ changelog_uri: https://github.com/texpert/shrine-aws-lambda/CHANGELOG.md
202
+ source_code_uri: https://github.com/texpert/shrine-aws-lambda
203
+ rubygems_mfa_required: 'true'
204
+ post_install_message: ''
205
+ rdoc_options: []
206
+ require_paths:
207
+ - lib
208
+ required_ruby_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '2.7'
213
+ required_rubygems_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ requirements: []
219
+ rubygems_version: 3.1.6
220
+ signing_key:
221
+ specification_version: 4
222
+ summary: AWS Lambda integration plugin for Shrine.
223
+ test_files: []