shrine-transloadit 0.5.0 → 1.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.
@@ -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