shrine-transloadit 0.5.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "transloadit"
2
4
 
3
5
  require "uri"
@@ -7,403 +9,246 @@ require "openssl"
7
9
  class Shrine
8
10
  module Plugins
9
11
  module Transloadit
10
- class Error < Shrine::Error; end
12
+ # Transloadit's default destination path for export robots.
13
+ DEFAULT_PATH = "${unique_prefix}/${file.url_name}"
11
14
 
12
- class ResponseError < Error
13
- attr_reader :response
15
+ class Error < Shrine::Error
16
+ end
14
17
 
15
- def initialize(response)
16
- @response = response
17
- super("#{response["error"]}: #{response["reason"] || response["message"]}")
18
- end
18
+ class InvalidSignature < Error
19
19
  end
20
20
 
21
- # Accepts Transloadit credentials via `:auth_key` and `:auth_secret`.
22
- #
23
- # If :cache storage wasn't assigned, it will be assigned to a URL storage
24
- # for direct uploads.
25
- #
26
- # If promoting was not yet overriden, it is set to automatically trigger
27
- # Transloadit processing defined in `Shrine#transloadit_process`.
28
- def self.configure(uploader, opts = {})
29
- uploader.opts[:transloadit_auth_key] = opts.fetch(:auth_key, uploader.opts[:transloadit_auth_key])
30
- uploader.opts[:transloadit_auth_secret] = opts.fetch(:auth_secret, uploader.opts[:transloadit_auth_secret])
31
-
32
- raise Error, "The :auth_key is required for transloadit plugin" if uploader.opts[:transloadit_auth_key].nil?
33
- raise Error, "The :auth_secret is required for transloadit plugin" if uploader.opts[:transloadit_auth_secret].nil?
34
-
35
- uploader.opts[:backgrounding_promote] ||= proc { transloadit_process }
21
+ LOG_SUBSCRIBER = -> (event) do
22
+ Shrine.logger.info "Transloadit (#{event.duration}ms) – #{{
23
+ processor: event[:processor],
24
+ uploader: event[:uploader],
25
+ }.inspect}"
36
26
  end
37
27
 
38
- # It loads the backgrounding plugin, so that it can override promoting.
39
- def self.load_dependencies(uploader, opts = {})
40
- uploader.plugin :backgrounding
28
+ # Accepts Transloadit credentials via `:auth_key` and `:auth_secret`.
29
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
30
+ uploader.opts[:transloadit] ||= { processors: {}, savers: {}, credentials: {} }
31
+ uploader.opts[:transloadit].merge!(opts)
32
+
33
+ fail Error, "The :auth option is required" unless uploader.opts[:transloadit][:auth]
34
+
35
+ # instrumentation plugin integration
36
+ uploader.subscribe(:transloadit, &log_subscriber) if uploader.respond_to?(:subscribe)
41
37
  end
42
38
 
43
39
  module AttacherClassMethods
44
- # Loads the attacher from the data, and triggers Transloadit
45
- # processing. Intended to be used in a background job.
46
- def transloadit_process(data)
47
- attacher = self.load(data)
48
- cached_file = attacher.uploaded_file(data["attachment"])
49
- attacher.transloadit_process(cached_file)
50
- attacher
40
+ def transloadit_processor(name = :default, &block)
41
+ if block
42
+ shrine_class.opts[:transloadit][:processors][name.to_sym] = block
43
+ else
44
+ shrine_class.opts[:transloadit][:processors][name.to_sym] or
45
+ fail Error, "transloadit processor #{name.inspect} not registered"
46
+ end
51
47
  end
52
48
 
53
- # This should be called when receiving a webhook, where arguments are
54
- # params that Transloadit POSTed to the endpoint. It checks the
55
- # signature, loads the attacher, saves processing results to the record.
56
- def transloadit_save(params)
57
- shrine_class.verify_transloadit_signature!(params)
58
- response = JSON.parse(params["transloadit"])
59
- data = response["fields"]["attacher"]
60
- attacher = self.load(data)
61
- cached_file = attacher.uploaded_file(data["attachment"])
62
- attacher.transloadit_save(response, valid: attacher.get == cached_file)
63
- attacher
49
+ def transloadit_saver(name = :default, &block)
50
+ if block
51
+ shrine_class.opts[:transloadit][:savers][name.to_sym] = block
52
+ else
53
+ shrine_class.opts[:transloadit][:savers][name.to_sym] or
54
+ fail Error, "transloadit saver #{name.inspect} not registered"
55
+ end
64
56
  end
65
57
  end
66
58
 
67
59
  module AttacherMethods
68
- # Triggers Transloadit processing defined by the user in
69
- # `Shrine#transloadit_process`. It dumps the attacher in the payload of
70
- # the request, so that it's included in the webhook and that we know
71
- # which webhook belongs to which record/attachment.
72
- #
73
- # After the Transloadit assembly was submitted, the response is saved
74
- # into cached file's metadata, which can then be reloaded at will for
75
- # checking progress of the assembly.
76
- #
77
- # Raises a `Shrine::Plugins::Transloadit::ResponseError` if Transloadit returned an error.
78
- def transloadit_process(cached_file = get)
79
- assembly = store.transloadit_process(cached_file, context)
80
- assembly.options[:fields] ||= {}
81
- assembly.options[:fields]["attacher"] = self.dump.merge("attachment" => cached_file.to_json)
82
- response = assembly.create!
83
- raise ResponseError.new(response.body) if response["error"]
84
- cached_file.metadata["transloadit_response"] = response.body.to_json
85
- swap(cached_file) or _set(cached_file)
60
+ def transloadit_process(name = :default, *args, **kwargs)
61
+ processor = self.class.transloadit_processor(name)
62
+ instrument_transloadit(name) do
63
+ instance_exec(*args, **kwargs, &processor)
64
+ end
86
65
  end
87
66
 
88
- # It receives the result of Transloadit processing, and converts it into
89
- # Shrine's representation(s) of an uploaded file (either as a single
90
- # file or a hash of versions).
91
- #
92
- # If attachment has changed in the meanwhile, meaning the result of
93
- # this processing is no longer valid, it deletes the processed files
94
- # from the main storage.
95
- #
96
- # Raises a `Shrine::Plugins::Transloadit::ResponseError` if Transloadit returned an error.
97
- def transloadit_save(response, valid: true)
98
- raise ResponseError.new(response) if response["error"]
99
-
100
- if versions = response["fields"]["versions"]
101
- stored_file = versions.inject({}) do |hash, (name, step_name)|
102
- results = response["results"].fetch(step_name)
103
- uploaded_files = results.map { |result| store.transloadit_uploaded_file(result) }
104
- multiple = response["fields"]["multiple"].to_h[name]
105
-
106
- if multiple == "list"
107
- hash.merge!(name => uploaded_files)
108
- elsif uploaded_files.one?
109
- hash.merge!(name => uploaded_files[0])
110
- else
111
- raise Error, "Step produced multiple files but wasn't marked as multiple"
112
- end
113
- end
114
- else
115
- results = response["results"].values.last
116
- uploaded_files = results.map { |result| store.transloadit_uploaded_file(result) }
117
- multiple = response["fields"]["multiple"]
118
-
119
- if multiple == "list"
120
- stored_file = uploaded_files
121
- elsif uploaded_files.one?
122
- stored_file = uploaded_files[0]
123
- else
124
- raise Error, "Step produced multiple files but wasn't marked as multiple"
125
- end
67
+ def transloadit_save(name = :default, *args, **kwargs)
68
+ unless name.respond_to?(:to_sym)
69
+ args.prepend(name)
70
+ name = :default
126
71
  end
127
72
 
128
- if valid
129
- swap(stored_file)
130
- else
131
- _delete(stored_file, action: :abort)
132
- end
73
+ saver = self.class.transloadit_saver(name)
74
+ instance_exec(*args, **kwargs, &saver)
75
+ end
76
+
77
+ def transloadit_step(*args, **kwargs)
78
+ shrine_class.transloadit_step(*args, **kwargs)
133
79
  end
134
- end
135
80
 
136
- module ClassMethods
137
- # Creates a new Transloadit client, so that the expiration timestamp is
138
- # refreshed on new processing requests.
139
81
  def transloadit
140
- ::Transloadit.new(
141
- key: opts[:transloadit_auth_key],
142
- secret: opts[:transloadit_auth_secret],
143
- )
82
+ shrine_class.transloadit
144
83
  end
145
84
 
146
- # Checks if the webhook has indeed been triggered by Transloadit, by
147
- # checking if sent signature matches the calculated signature, and
148
- # raising a `Shrine::Plugins::Transloadit::Error` if signatures don't
149
- # match.
150
- def verify_transloadit_signature!(params)
151
- sent_signature = params["signature"]
152
- payload = params["transloadit"]
153
- algorithm = OpenSSL::Digest.new('sha1')
154
- secret = opts[:transloadit_auth_secret]
155
- calculated_signature = OpenSSL::HMAC.hexdigest(algorithm, secret, payload)
156
- raise Error, "Received signature doesn't match the calculated signature" if calculated_signature != sent_signature
85
+ private
86
+
87
+ def instrument_transloadit(processor, &block)
88
+ return yield unless shrine_class.respond_to?(:instrument)
89
+
90
+ shrine_class.instrument(:transloadit, processor: processor, &block)
157
91
  end
158
92
  end
159
93
 
160
- module InstanceMethods
161
- # Converts Transloadit's representation of an uploaded file into
162
- # Shrine's representation. It currently only accepts files exported to
163
- # S3. All Transloadit's metadata is saved into a "transloadit"
164
- # attribute.
165
- #
166
- # When doing direct uploads to Transloadit you will only get a
167
- # temporary URL, which will be saved in the "id" attribute and it's
168
- # expected that the URL storage is used.
169
- def transloadit_uploaded_file(result)
170
- case url = result.fetch("url")
171
- when /amazonaws\.com/
172
- raise Error, "Cannot save a processed file which wasn't exported: #{url.inspect}" if url.include?("tmp.transloadit.com")
173
- path = URI(url).path
174
- id = path.match(%r{^(/#{storage.prefix})?/}).post_match
94
+ module ClassMethods
95
+ def transloadit_step(name, robot, **options)
96
+ use = options.delete(:use)
97
+
98
+ if Array(use).first.is_a?(::Transloadit::Step)
99
+ step = transloadit.step(name, robot, **options)
100
+ step.use(use) if use
101
+ step
175
102
  else
176
- raise Error, "The transloadit Shrine plugin doesn't support storage identified by #{url.inspect}"
103
+ transloadit.step(name, robot, use: use, **options)
177
104
  end
178
-
179
- self.class::UploadedFile.new(
180
- "id" => id,
181
- "storage" => storage_key.to_s,
182
- "metadata" => {
183
- "filename" => result.fetch("name"),
184
- "size" => result.fetch("size"),
185
- "mime_type" => result.fetch("mime"),
186
- "width" => (result["meta"] && result["meta"]["width"]),
187
- "height" => (result["meta"] && result["meta"]["height"]),
188
- "transloadit" => result["meta"],
189
- }
190
- )
191
105
  end
192
106
 
193
- # Generates a Transloadit import step from the Shrine::UploadedFile.
194
- # If it's from the S3 storage, an S3 import step will be generated.
195
- # Otherwise either a generic HTTP(S) or an FTP import will be generated.
196
- def transloadit_import_step(name, io, **step_options)
197
- uri = URI.parse(io.url)
198
-
199
- if defined?(Storage::S3) && io.storage.is_a?(Storage::S3)
200
- step = transloadit.step(name, "/s3/import",
201
- key: io.storage.client.config.access_key_id,
202
- secret: io.storage.client.config.secret_access_key,
203
- bucket: io.storage.bucket.name,
204
- bucket_region: io.storage.client.config.region,
205
- path: [*io.storage.prefix, io.id].join("/"),
206
- )
207
- elsif uri.scheme == "http" || uri.scheme == "https"
208
- step = transloadit.step(name, "/http/import",
209
- url: uri.to_s,
210
- )
211
- elsif uri.scheme == "ftp"
212
- step = transloadit.step(name, "/ftp/import",
213
- host: uri.host,
214
- user: uri.user,
215
- password: uri.password,
216
- path: uri.path,
217
- )
218
- else
219
- raise Error, "Cannot construct a transloadit import step from #{io.inspect}"
107
+ # Verifies the Transloadit signature of a webhook request. Raises
108
+ # `Shrine::Plugins::Transloadit::InvalidSignature` if signatures
109
+ # don't match.
110
+ def transloadit_verify!(params)
111
+ if transloadit_sign(params["transloadit"]) != params["signature"]
112
+ raise InvalidSignature, "received signature doesn't match calculated"
220
113
  end
114
+ end
221
115
 
222
- step.options.update(step_options)
116
+ # Creates a new Transloadit client each time. This way the expiration
117
+ # timestamp is refreshed on new processing requests.
118
+ def transloadit
119
+ ::Transloadit.new(**opts[:transloadit][:auth])
120
+ end
223
121
 
224
- step
122
+ def transloadit_credentials(storage_key)
123
+ opts[:transloadit][:credentials][storage_key] or
124
+ fail Error, "credentials not registered for storage #{storage_key.inspect}"
225
125
  end
226
126
 
227
- # Generates an export step from the current (permanent) storage.
228
- # At the moment only Amazon S3 is supported.
229
- def transloadit_export_step(name, path: nil, **step_options)
230
- if defined?(Storage::S3) && storage.is_a?(Storage::S3)
231
- path ||= "${unique_prefix}/${file.url_name}" # Transloadit's default path
232
-
233
- step = transloadit.step(name, "/s3/store",
234
- key: storage.client.config.access_key_id,
235
- secret: storage.client.config.secret_access_key,
236
- bucket: storage.bucket.name,
237
- bucket_region: storage.client.config.region,
238
- path: [*storage.prefix, path].join("/"),
239
- )
240
- else
241
- raise Error, "Cannot construct a transloadit export step from #{storage.inspect}"
242
- end
127
+ private
243
128
 
244
- step.options.update(step_options)
129
+ # Signs given string with Transloadit secret key.
130
+ def transloadit_sign(string)
131
+ algorithm = OpenSSL::Digest::SHA1.new
132
+ secret_key = opts[:transloadit][:auth][:secret]
245
133
 
246
- step
134
+ OpenSSL::HMAC.hexdigest(algorithm, secret_key, string)
247
135
  end
136
+ end
248
137
 
249
- # Creates a new TransloaditFile for building steps, with an optional
250
- # import step applied.
251
- def transloadit_file(io = nil)
252
- file = TransloaditFile.new(transloadit: transloadit)
253
- file = file.add_step(transloadit_import_step("import", io)) if io
254
- file
138
+ module InstanceMethods
139
+ def transloadit_files(results)
140
+ results.map { |result| transloadit_file(result) }
255
141
  end
256
142
 
257
- # Accepts a TransloaditFile, a hash of TransloaditFiles or a template,
258
- # and converts it into a Transloadit::Assembly.
259
- #
260
- # If a hash of versions are given, the version information is saved in
261
- # assembly's payload, so that later in the webhook it can be used to
262
- # construct a hash of versions.
263
- def transloadit_assembly(value, context: {}, **options)
264
- options = { steps: [], fields: {} }.merge(options)
265
-
266
- case value
267
- when TransloaditFile then transloadit_assembly_update_single!(value, context, options)
268
- when Hash then transloadit_assembly_update_versions!(value, context, options)
269
- when String then transloadit_assembly_update_template!(value, context, options)
270
- else
271
- raise Error, "Assembly value has to be either a TransloaditFile, a hash of TransloaditFile objects, or a template"
272
- end
143
+ def transloadit_file(result)
144
+ result = result.first if result.is_a?(Array)
145
+ uri = URI.parse(result.fetch("url"))
273
146
 
274
- if options[:steps].uniq(&:name) != options[:steps]
275
- raise Error, "There are different transloadit steps using the same name"
147
+ if defined?(Storage::S3) && storage.is_a?(Storage::S3)
148
+ prefix = "#{storage.prefix}/" if storage.prefix
149
+ id = uri.path.match(%r{^/#{prefix}})&.post_match or
150
+ fail Error, "URL path doesn't start with storage prefix: #{uri}"
151
+ elsif defined?(Storage::Url) && storage.is_a?(Storage::Url)
152
+ id = uri.to_s
153
+ else
154
+ fail Error, "storage not supported: #{storage.inspect}"
276
155
  end
277
156
 
278
- transloadit.assembly(options)
157
+ self.class::UploadedFile.new(
158
+ id: id,
159
+ storage: storage_key,
160
+ metadata: result.fetch("meta").merge(
161
+ "filename" => result.fetch("name"),
162
+ "size" => result.fetch("size"),
163
+ "mime_type" => result.fetch("mime"),
164
+ )
165
+ )
279
166
  end
280
167
 
281
- # An cached instance of a Tranloadit client.
282
- def transloadit
283
- @transloadit ||= self.class.transloadit
168
+ def transloadit_export_step(name = "export", **options)
169
+ unless options.key?(:credentials)
170
+ options[:credentials] = self.class.transloadit_credentials(storage_key).to_s
171
+ end
172
+
173
+ if defined?(Storage::S3) && storage.is_a?(Storage::S3)
174
+ transloadit_s3_store_step(name, **options)
175
+ elsif defined?(Storage::GoogleCloudStorage) && storage.is_a?(Storage::GoogleCloudStorage)
176
+ transloadit_google_store_step(name, **options)
177
+ elsif defined?(Storage::YouTube) && storage.is_a?(Storage::YouTube)
178
+ transloadit_youtube_store_step(name, **options)
179
+ else
180
+ fail Error, "cannot construct export step for #{storage.inspect}"
181
+ end
284
182
  end
285
183
 
286
184
  private
287
185
 
288
- # Updates assembly options for single files.
289
- def transloadit_assembly_update_single!(transloadit_file, context, options)
290
- raise Error, "The given TransloaditFile is missing an import step" if !transloadit_file.imported?
291
- unless transloadit_file.exported?
292
- path = generate_location(transloadit_file, context) + ".${file.ext}"
293
- export_step = transloadit_export_step("export", path: path)
294
- transloadit_file = transloadit_file.add_step(export_step)
295
- end
296
- options[:steps] += transloadit_file.steps
297
- options[:fields]["multiple"] = transloadit_file.multiple
186
+ def transloadit_s3_store_step(name, path: DEFAULT_PATH, **options)
187
+ transloadit_step name, "/s3/store",
188
+ path: [*storage.prefix, path].join("/"),
189
+ **options
298
190
  end
299
191
 
300
- # Updates assembly options for a hash of versions.
301
- def transloadit_assembly_update_versions!(versions, context, options)
302
- raise Error, "The versions Shrine plugin isn't loaded" if !defined?(Shrine::Plugins::Versions)
303
- options[:fields]["versions"] = {}
304
- options[:fields]["multiple"] = {}
305
- versions.each do |name, transloadit_file|
306
- raise Error, "The :#{name} version value is not a TransloaditFile" if !transloadit_file.is_a?(TransloaditFile)
307
- raise Error, "The given TransloaditFile is missing an import step" if !transloadit_file.imported?
308
- unless transloadit_file.exported?
309
- path = generate_location(transloadit_file, context.merge(version: name)) + ".${file.ext}"
310
- export_step = transloadit_export_step("export_#{name}", path: path)
311
- transloadit_file = transloadit_file.add_step(export_step)
312
- end
313
- options[:steps] |= transloadit_file.steps
314
- options[:fields]["versions"][name] = transloadit_file.name
315
- options[:fields]["multiple"][name] = transloadit_file.multiple
316
- end
192
+ def transloadit_google_store_step(name, path: DEFAULT_PATH, **options)
193
+ transloadit_step name, "/google/store",
194
+ path: [*storage.prefix, path].join("/"),
195
+ **options
317
196
  end
318
197
 
319
- # Updates assembly options for a template.
320
- def transloadit_assembly_update_template!(template, context, options)
321
- options[:template_id] = template
198
+ def transloadit_youtube_store_step(name, **options)
199
+ transloadit_step name, "/youtube/store",
200
+ **options
322
201
  end
323
- end
324
202
 
325
- module FileMethods
326
- # Converts the "transloadit_response" that was saved in cached file's
327
- # metadata into a reloadable object which is normally returned by the
328
- # Transloadit gem.
329
- def transloadit_response
330
- @transloadit_response ||= (
331
- body = metadata["transloadit_response"] or return
332
- body.instance_eval { def body; self; end }
333
- response = ::Transloadit::Response.new(body)
334
- response.extend ::Transloadit::Response::Assembly
335
- response
336
- )
203
+ def transloadit_step(*args, **kwargs)
204
+ self.class.transloadit_step(*args, **kwargs)
337
205
  end
338
206
  end
339
207
 
340
- class TransloaditFile
341
- attr_reader :transloadit, :steps
342
-
343
- def initialize(transloadit:, steps: [], multiple: nil)
344
- @transloadit = transloadit
345
- @steps = steps
346
- @multiple = multiple
347
- end
348
-
349
- # Adds a step to the pipeline, automatically setting :use to the
350
- # previous step, so that later multiple pipelines can be seamlessly
351
- # joined into a single assembly.
352
- #
353
- # It returns a new TransloaditFile with the step added.
354
- def add_step(*args)
355
- if args[0].is_a?(::Transloadit::Step)
356
- step = args[0]
208
+ module FileMethods
209
+ def transloadit_import_step(name = "import", **options)
210
+ if defined?(Storage::S3) && storage.is_a?(Storage::S3)
211
+ transloadit_s3_import_step(name, **options)
212
+ elsif url && URI(url).is_a?(URI::HTTP)
213
+ transloadit_http_import_step(name, **options)
214
+ elsif url && URI(url).is_a?(URI::FTP)
215
+ transloadit_ftp_import_step(name, **options)
357
216
  else
358
- step = transloadit.step(*args)
217
+ fail Error, "cannot construct import step from #{self.inspect}"
359
218
  end
360
-
361
- unless step.options[:use]
362
- step.use steps.last if steps.any?
363
- end
364
-
365
- transloadit_file(steps: steps + [step])
366
219
  end
367
220
 
368
- # Specify the result format.
369
- def multiple(format = nil)
370
- if format
371
- transloadit_file(multiple: format)
372
- else
373
- @multiple
374
- end
375
- end
221
+ private
376
222
 
377
- # The key that Transloadit will use in its processing results for the
378
- # corresponding processed file. This equals to the name of the last
379
- # step, unless the last step is an export step.
380
- def name
381
- if exported?
382
- @steps[-2].name
383
- else
384
- @steps.last.name
223
+ def transloadit_s3_import_step(name, **options)
224
+ unless options.key?(:credentials)
225
+ options[:credentials] = shrine_class.transloadit_credentials(storage_key).to_s
385
226
  end
386
- end
387
227
 
388
- # Returns true if this TransloaditFile includes an import step.
389
- def imported?
390
- @steps.any? && @steps.first.robot.end_with?("/import")
228
+ transloadit_step name, "/s3/import",
229
+ path: [*storage.prefix, id].join("/"),
230
+ **options
391
231
  end
392
232
 
393
- # Returns true if this TransloaditFile includes an export step.
394
- def exported?
395
- @steps.any? && @steps.last.robot.end_with?("/store")
233
+ def transloadit_http_import_step(name, **options)
234
+ transloadit_step name, "/http/import",
235
+ url: url,
236
+ **options
396
237
  end
397
238
 
398
- private
239
+ def transloadit_ftp_import_step(name, **options)
240
+ uri = URI.parse(url)
399
241
 
400
- def transloadit_file(**options)
401
- TransloaditFile.new(
402
- transloadit: @transloadit,
403
- steps: @steps,
404
- multiple: @multiple,
242
+ transloadit_step name, "/ftp/import",
243
+ host: uri.host,
244
+ user: uri.user,
245
+ password: uri.password,
246
+ path: uri.path,
405
247
  **options
406
- )
248
+ end
249
+
250
+ def transloadit_step(*args, **kwargs)
251
+ shrine_class.transloadit_step(*args, **kwargs)
407
252
  end
408
253
  end
409
254
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "shrine-transloadit"
3
- gem.version = "0.5.0"
3
+ gem.version = "1.0.1"
4
4
 
5
5
  gem.required_ruby_version = ">= 2.2"
6
6
 
@@ -13,14 +13,12 @@ Gem::Specification.new do |gem|
13
13
  gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "*.gemspec"]
14
14
  gem.require_path = "lib"
15
15
 
16
- gem.add_dependency "shrine", "~> 2.9"
16
+ gem.add_dependency "shrine", "~> 3.0"
17
17
  gem.add_dependency "transloadit", "~> 2.0"
18
18
 
19
19
  gem.add_development_dependency "rake"
20
20
  gem.add_development_dependency "minitest"
21
- gem.add_development_dependency "minitest-hooks"
22
- gem.add_development_dependency "dotenv"
23
- gem.add_development_dependency "aws-sdk-s3", "~> 1.2"
24
- gem.add_development_dependency "sequel"
25
- gem.add_development_dependency "sqlite3"
21
+ gem.add_development_dependency "aws-sdk-s3", "~> 1.14"
22
+ gem.add_development_dependency "shrine-url", "~> 2.4"
23
+ gem.add_development_dependency "dry-monitor"
26
24
  end