shrine 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

@@ -1,40 +1,3 @@
1
- class Shrine
2
- module Plugins
3
- # The delete_uploaded plugin will automatically delete files after they
4
- # have been uploaded. This is especially useful when doing processing, to
5
- # ensure that temporary files have been deleted after upload. One exception
6
- # are `Shrine::UploadedFile` files, they won't get deleted for stability
7
- # reasons.
8
- #
9
- # plugin :delete_uploaded
10
- #
11
- # By default this behaviour will be applied to all storages, but you can
12
- # limit this only to specified storages:
13
- #
14
- # plugin :delete_uploaded, storages: [:store]
15
- module DeleteUploaded
16
- def self.configure(uploader, storages: :all)
17
- uploader.opts[:delete_uploaded_storages] = storages
18
- end
19
-
20
- module InstanceMethods
21
- private
22
-
23
- # Deletes the uploaded file unless it's an UploadedFile.
24
- def copy(io, context)
25
- super
26
- if io.respond_to?(:delete) && !io.is_a?(UploadedFile)
27
- io.delete if delete_uploaded?(io)
28
- end
29
- end
30
-
31
- def delete_uploaded?(io)
32
- opts[:delete_uploaded_storages] == :all ||
33
- opts[:delete_uploaded_storages].include?(storage_key)
34
- end
35
- end
36
- end
37
-
38
- register_plugin(:delete_uploaded, DeleteUploaded)
39
- end
40
- end
1
+ warn "The delete_uploaded Shrine plugin has been renamed to \"delete_raw\". Loading the plugin through \"delete_uploaded\" will not work in Shrine 2."
2
+ require "shrine/plugins/delete_raw"
3
+ Shrine::Plugins.register_plugin(:delete_uploaded, Shrine::Plugins::DeleteRaw)
@@ -5,6 +5,11 @@ class Shrine
5
5
  #
6
6
  # plugin :determine_mime_type
7
7
  #
8
+ # By default the UNIX [file] utility is used to determine the MIME type, but
9
+ # you can change it:
10
+ #
11
+ # plugin :determine_mime_type, analyzer: :filemagic
12
+ #
8
13
  # The plugin accepts the following analyzers:
9
14
  #
10
15
  # :file
@@ -27,19 +32,10 @@ class Shrine
27
32
  # *extension*. Note that unlike other solutions, this analyzer is not
28
33
  # guaranteed to return the actual MIME type of the file.
29
34
  #
30
- # By default the UNIX [file] utility is used to detrmine the MIME type, but
31
- # you can change it:
32
- #
33
- # plugin :determine_mime_type, analyzer: :filemagic
34
- #
35
35
  # If none of these quite suit your needs, you can use a custom analyzer:
36
36
  #
37
37
  # plugin :determine_mime_type, analyzer: ->(io) do
38
- # if io.path.end_with?(".odt")
39
- # "application/vnd.oasis.opendocument.text"
40
- # else
41
- # MimeMagic.by_magic(io).type
42
- # end
38
+ # # returns the extracted MIME type
43
39
  # end
44
40
  #
45
41
  # [file]: http://linux.die.net/man/1/file
@@ -77,13 +73,17 @@ class Shrine
77
73
  def extract_mime_type(io)
78
74
  analyzer = opts[:mime_type_analyzer]
79
75
 
80
- if io.respond_to?(:mime_type)
76
+ mime_type = if io.respond_to?(:mime_type)
81
77
  io.mime_type
82
78
  elsif analyzer.is_a?(Symbol)
83
79
  send(:"_extract_mime_type_with_#{analyzer}", io)
84
80
  else
85
81
  analyzer.call(io)
86
82
  end
83
+
84
+ io.rewind
85
+
86
+ mime_type
87
87
  end
88
88
 
89
89
  private
@@ -98,7 +98,6 @@ class Shrine
98
98
  mime_type, _ = Open3.capture2(*cmd, io.path)
99
99
  else
100
100
  mime_type, _ = Open3.capture2(*cmd, "-", stdin_data: io.read(MAGIC_NUMBER), binmode: true)
101
- io.rewind
102
101
  end
103
102
 
104
103
  mime_type.strip unless mime_type.empty?
@@ -107,16 +106,12 @@ class Shrine
107
106
  # Uses the ruby-filemagic gem to magically extract the MIME type.
108
107
  def _extract_mime_type_with_filemagic(io)
109
108
  filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
110
- data = io.read(MAGIC_NUMBER)
111
- io.rewind
112
- filemagic.buffer(data)
109
+ filemagic.buffer(io.read(MAGIC_NUMBER))
113
110
  end
114
111
 
115
112
  # Uses the mimemagic gem to extract the MIME type.
116
113
  def _extract_mime_type_with_mimemagic(io)
117
- result = MimeMagic.by_magic(io).type
118
- io.rewind
119
- result
114
+ MimeMagic.by_magic(io).type
120
115
  end
121
116
 
122
117
  # Uses the mime-types gem to determine MIME type from file extension.
@@ -18,10 +18,10 @@ class Shrine
18
18
  #
19
19
  # You should always mount a new endpoint for each uploader that you want to
20
20
  # enable direct uploads for. This now gives your Ruby application a `POST
21
- # /attachments/images/:storage/:name` route, which accepts a "file" query
21
+ # /attachments/images/:storage/upload` route, which accepts a "file" query
22
22
  # parameter, and returns the uploaded file in JSON format:
23
23
  #
24
- # # POST /attachments/images/cache/avatar (file upload)
24
+ # # POST /attachments/images/cache/upload (file upload)
25
25
  # {
26
26
  # "id": "43kewit94.jpg",
27
27
  # "storage": "cache",
@@ -50,8 +50,9 @@ class Shrine
50
50
  # ## Presigning
51
51
  #
52
52
  # An alternative to the direct endpoint is uploading directly to the
53
- # underlying storage (S3). These uploads usually require extra information
54
- # from the server, you can enable that route by passing `presign: true`:
53
+ # underlying storage (currently only supported by Amazon S3). These uploads
54
+ # usually require extra information from the server, you can enable that
55
+ # route by passing `presign: true`:
55
56
  #
56
57
  # plugin :direct_upload, presign: true
57
58
  #
@@ -78,24 +79,38 @@ class Shrine
78
79
  #
79
80
  # GET /cache/presign?extension=.png
80
81
  #
82
+ # You can change how the key is generated with `:presign_location`:
83
+ #
84
+ # plugin :direct_upload, presign: true, presign_location: ->(request) { "${filename}" }
85
+ #
81
86
  # If you want additional options to be passed to Storage::S3#presign, you
82
- # can pass a block to `:presign`, and it will yield Roda's request object:
87
+ # can pass `:presign_options` with a hash or a block (which gets yielded
88
+ # Roda's request object):
83
89
  #
84
- # plugin :direct_upload, presign: ->(request) do
85
- # {
86
- # content_length_range: 0..(5*1024*1024), # limit the filesize to 5 MB
87
- # content_type: request.params["content_type"], # use "content_type" query parameter
88
- # }
90
+ # plugin :direct_upload, presign: true, presign_options: {acl: "public-read"}
91
+ #
92
+ # plugin :direct_upload, presign: true, presign_options: ->(request) do
93
+ # options = {}
94
+ # options[:content_length_range] = 0..(5*1024*1024) # limit the filesize to 5 MB
95
+ # options[:content_type] = request.params["content_type"] # use "content_type" query parameter
96
+ # options
89
97
  # end
90
98
  #
91
99
  # See the [Direct Uploads to S3] guide for further instructions on how to
92
- # hook this up in a form.
100
+ # hook the presigned uploads to a form.
101
+ #
102
+ # ### Testing presigns
103
+ #
104
+ # If you want to test presigned uploads, but don't want to pay the
105
+ # performance cost of using Amazon S3 storage in tests, you can simply swap
106
+ # out S3 with a storage like FileSystem. The presigns will still get
107
+ # generated, but will simply point to this endpoint's upload route.
93
108
  #
94
109
  # ## Allowed storages
95
110
  #
96
111
  # While Shrine only accepts cached attachments on form submits (for security
97
112
  # reasons), you can use this endpoint to upload files to any storage, just
98
- # add it do allowed storages:
113
+ # add it to allowed storages:
99
114
  #
100
115
  # plugin :direct_upload, allowed_storages: [:cache, :store]
101
116
  #
@@ -141,9 +156,17 @@ class Shrine
141
156
  uploader.plugin :rack_file
142
157
  end
143
158
 
144
- def self.configure(uploader, allowed_storages: [:cache], presign: nil, max_size: nil)
159
+ def self.configure(uploader, allowed_storages: [:cache], presign: nil, presign_options: {}, presign_location: nil, max_size: nil)
160
+ if presign.respond_to?(:call)
161
+ warn "Passing a block to :presign in direct_upload plugin is deprecated and will be removed in Shrine 2. Use :presign_options instead."
162
+ presign_options = presign
163
+ presign = true
164
+ end
165
+
145
166
  uploader.opts[:direct_upload_allowed_storages] = allowed_storages
146
167
  uploader.opts[:direct_upload_presign] = presign
168
+ uploader.opts[:direct_upload_presign_options] = presign_options
169
+ uploader.opts[:direct_upload_presign_location] = presign_location
147
170
  uploader.opts[:direct_upload_max_size] = max_size
148
171
 
149
172
  uploader.assign_upload_endpoint(App) unless uploader.const_defined?(:UploadEndpoint)
@@ -165,7 +188,7 @@ class Shrine
165
188
 
166
189
  # Returns the Roda direct upload endpoint.
167
190
  def direct_endpoint
168
- warn "#{self}.direct_endpoint is deprecated and will be removed in Shrine 2, you should use #{self}::UploadEndpoint instead."
191
+ warn "Shrine.direct_endpoint is deprecated and will be removed in Shrine 2, you should use Shrine::UploadEndpoint instead."
169
192
  self::UploadEndpoint
170
193
  end
171
194
  end
@@ -175,36 +198,109 @@ class Shrine
175
198
  # with the file upload and returns the uploaded file as JSON.
176
199
  class App < Roda
177
200
  plugin :default_headers, "Content-Type"=>"application/json"
201
+ plugin :json_parser
178
202
 
179
203
  route do |r|
180
204
  r.on ":storage" do |storage_key|
181
- allow_storage!(storage_key)
182
- @uploader = shrine_class.new(storage_key.to_sym)
205
+ @uploader = get_uploader(storage_key)
183
206
 
184
- r.post ":name" do |name|
207
+ r.post ["upload", ":name"] do |name|
185
208
  file = get_file
186
- context = {name: name, phase: :cache}
209
+ context = get_context(name)
187
210
 
188
- json @uploader.upload(file, context)
189
- end unless presign
211
+ uploaded_file = upload(file, context)
212
+
213
+ json uploaded_file
214
+ end unless presign? && presign_storage?
190
215
 
191
216
  r.get "presign" do
192
- location = SecureRandom.hex(30) + request.params["extension"].to_s
193
- options = presign.call(request) if presign.respond_to?(:call)
217
+ location = get_presign_location
218
+ options = get_presign_options
194
219
 
195
- signature = @uploader.storage.presign(location, options || {})
220
+ presign_data = generate_presign(location, options)
196
221
 
197
- json Hash[url: signature.url, fields: signature.fields]
198
- end if presign
222
+ json presign_data
223
+ end if presign?
199
224
  end
200
225
  end
201
226
 
202
227
  private
203
228
 
229
+ attr_reader :uploader
230
+
231
+ # Instantiates the uploader, checking first if the storage is allowed.
232
+ def get_uploader(storage_key)
233
+ allow_storage!(storage_key)
234
+ shrine_class.new(storage_key.to_sym)
235
+ end
236
+
237
+ # Retrieves the context for the upload.
238
+ def get_context(name)
239
+ context = {phase: :cache}
240
+
241
+ if name != "upload"
242
+ warn "The \"POST /:storage/:name\" route of the direct_upload Shrine plugin is deprecated, and it will be removed in Shrine 3. Use \"POST /:storage/upload\" instead."
243
+ context[:name] = name
244
+ end
245
+
246
+ if presign? && !presign_storage?
247
+ context[:location] = request.params["key"]
248
+ end
249
+
250
+ context
251
+ end
252
+
253
+ # Uploads the file to the requested storage.
254
+ def upload(file, context)
255
+ uploader.upload(file, context)
256
+ end
257
+
258
+ # Generates a unique location, or calls `:presign_location`.
259
+ def get_presign_location
260
+ if presign_location
261
+ presign_location.call(request)
262
+ else
263
+ SecureRandom.hex(30) + request.params["extension"].to_s
264
+ end
265
+ end
266
+
267
+ # Returns dynamic options for generating the presign.
268
+ def get_presign_options
269
+ options = presign_options
270
+ options = options.call(request) if options.respond_to?(:call)
271
+ options || {}
272
+ end
273
+
274
+ # Generates the presign hash for the request.
275
+ def generate_presign(location, options)
276
+ if presign_storage?
277
+ generate_real_presign(location, options)
278
+ else
279
+ generate_fake_presign(location, options)
280
+ end
281
+ end
282
+
283
+ # Generates a presign by calling the storage.
284
+ def generate_real_presign(location, options)
285
+ signature = uploader.storage.presign(location, options)
286
+ {url: signature.url, fields: signature.fields}
287
+ end
288
+
289
+ # Generates a presign that points to the direct upload endpoint.
290
+ def generate_fake_presign(location, options)
291
+ url = request.url.sub(/presign$/, "upload")
292
+ {url: url, fields: {key: location}}
293
+ end
294
+
295
+ # Returns true if the storage supports presigns.
296
+ def presign_storage?
297
+ uploader.storage.respond_to?(:presign)
298
+ end
299
+
204
300
  # Halts the request if storage is not allowed.
205
- def allow_storage!(storage)
206
- if !allowed_storages.map(&:to_s).include?(storage)
207
- error! 403, "Storage #{storage.inspect} is not allowed."
301
+ def allow_storage!(storage_key)
302
+ if !allowed_storages.map(&:to_s).include?(storage_key)
303
+ error! 403, "Storage #{storage_key.inspect} is not allowed."
208
304
  end
209
305
  end
210
306
 
@@ -253,10 +349,18 @@ class Shrine
253
349
  shrine_class.opts[:direct_upload_allowed_storages]
254
350
  end
255
351
 
256
- def presign
352
+ def presign?
257
353
  shrine_class.opts[:direct_upload_presign]
258
354
  end
259
355
 
356
+ def presign_options
357
+ shrine_class.opts[:direct_upload_presign_options]
358
+ end
359
+
360
+ def presign_location
361
+ shrine_class.opts[:direct_upload_presign_location]
362
+ end
363
+
260
364
  def max_size
261
365
  shrine_class.opts[:direct_upload_max_size]
262
366
  end
@@ -97,8 +97,7 @@ class Shrine
97
97
 
98
98
  route do |r|
99
99
  r.on ":storage" do |storage_key|
100
- allow_storage!(storage_key)
101
- @storage = shrine_class.find_storage(storage_key)
100
+ @storage = get_storage(storage_key)
102
101
 
103
102
  r.get /(.*)/ do |id|
104
103
  filename = request.path.split("/").last
@@ -107,14 +106,13 @@ class Shrine
107
106
  response["Content-Disposition"] = "#{disposition}; filename=#{filename.inspect}"
108
107
  response["Content-Type"] = Rack::Mime.mime_type(extname)
109
108
 
109
+ chunks = get_stream(id)
110
+ _, content_length = chunks.peek
111
+ response['Content-Length'] = content_length.to_s if content_length
112
+
110
113
  stream do |out|
111
- if @storage.respond_to?(:stream)
112
- @storage.stream(id) { |chunk| out << chunk }
113
- else
114
- io, buffer = @storage.open(id), ""
115
- out << io.read(16384, buffer) until io.eof?
116
- io.close
117
- io.delete if io.class.name == "Tempfile"
114
+ chunks.each do |chunk|
115
+ out << chunk
118
116
  end
119
117
  end
120
118
  end
@@ -123,10 +121,31 @@ class Shrine
123
121
 
124
122
  private
125
123
 
124
+ attr_reader :storage
125
+
126
+ def get_storage(storage_key)
127
+ allow_storage!(storage_key)
128
+ shrine_class.find_storage(storage_key)
129
+ end
130
+
131
+ def get_stream(id)
132
+ if storage.respond_to?(:stream)
133
+ storage.enum_for(:stream, id)
134
+ else
135
+ Enumerator.new do |y|
136
+ io = storage.open(id)
137
+ buffer = ""
138
+ y.yield(io.read(16*1024, buffer), io.size) until io.eof?
139
+ io.close
140
+ io.delete if io.class.name == "Tempfile"
141
+ end
142
+ end
143
+ end
144
+
126
145
  # Halts the request if storage is not allowed.
127
- def allow_storage!(storage)
128
- if !allowed_storages.map(&:to_s).include?(storage)
129
- error! 403, "Storage #{storage.inspect} is not allowed."
146
+ def allow_storage!(storage_key)
147
+ if !allowed_storages.map(&:to_s).include?(storage_key)
148
+ error! 403, "Storage #{storage_key.inspect} is not allowed."
130
149
  end
131
150
  end
132
151
 
@@ -22,24 +22,21 @@ class Shrine
22
22
  #
23
23
  # Shrine calls hooks in the following order when uploading a file:
24
24
  #
25
+ # * `before_upload`
25
26
  # * `around_upload`
26
- # * `before_upload`
27
+ # * `before_process`
27
28
  # * `around_process`
28
- # * `before_process`
29
- # * PROCESS
30
- # * `after_process`
29
+ # * `after_process`
30
+ # * `before_store`
31
31
  # * `around_store`
32
- # * `before_store`
33
- # * STORE
34
- # * `after_store`
35
- # * `after_upload`
32
+ # * `after_store`
33
+ # * `after_upload`
36
34
  #
37
35
  # Shrine calls hooks in the following order when deleting a file:
38
36
  #
37
+ # * `before_delete`
39
38
  # * `around_delete`
40
- # * `before_delete`
41
- # * DELETE
42
- # * `after_delete`
39
+ # * `after_delete`
43
40
  #
44
41
  # By default every `around_*` hook returns the result of the corresponding
45
42
  # operation:
@@ -51,39 +48,18 @@ class Shrine
51
48
  # result # it's good to always return the result for consistent behaviour
52
49
  # end
53
50
  # end
54
- #
55
- # It may be useful to know that you can realize some form of communication
56
- # between the hooks; whatever you save to the `context` hash will be
57
- # forwarded further down:
58
- #
59
- # class ImageUploader < Shrine
60
- # def before_process(io, context)
61
- # context[:_foo] = "bar"
62
- # super
63
- # end
64
- #
65
- # def before_store(io, context)
66
- # context[:_foo] #=> "bar"
67
- # super
68
- # end
69
- # end
70
- #
71
- # In that case you should always somehow mark this key as private (for
72
- # example with an underscore) so that it doesn't clash with any
73
- # existing keys.
74
51
  module Hooks
75
52
  module InstanceMethods
76
53
  def upload(io, context = {})
77
54
  result = nil
55
+ before_upload(io, context)
78
56
  around_upload(io, context) { result = super }
57
+ after_upload(io, context)
79
58
  result
80
59
  end
81
60
 
82
61
  def around_upload(*args)
83
- before_upload(*args)
84
- result = yield
85
- after_upload(*args)
86
- result
62
+ yield
87
63
  end
88
64
 
89
65
  def before_upload(*)
@@ -95,16 +71,15 @@ class Shrine
95
71
 
96
72
  def processed(io, context)
97
73
  result = nil
74
+ before_process(io, context)
98
75
  around_process(io, context) { result = super }
76
+ after_process(io, context)
99
77
  result
100
78
  end
101
79
  private :processed
102
80
 
103
81
  def around_process(*args)
104
- before_process(*args)
105
- result = yield
106
- after_process(*args)
107
- result
82
+ yield
108
83
  end
109
84
 
110
85
  def before_process(*)
@@ -116,15 +91,14 @@ class Shrine
116
91
 
117
92
  def store(io, context = {})
118
93
  result = nil
94
+ before_store(io, context)
119
95
  around_store(io, context) { result = super }
96
+ after_store(io, context)
120
97
  result
121
98
  end
122
99
 
123
100
  def around_store(*args)
124
- before_store(*args)
125
- result = yield
126
- after_store(*args)
127
- result
101
+ yield
128
102
  end
129
103
 
130
104
  def before_store(*)
@@ -136,14 +110,14 @@ class Shrine
136
110
 
137
111
  def delete(io, context = {})
138
112
  result = nil
113
+ before_delete(io, context)
139
114
  around_delete(io, context) { result = super }
115
+ after_delete(io, context)
140
116
  result
141
117
  end
142
118
 
143
119
  def around_delete(*args)
144
- before_delete(*args)
145
- result = yield
146
- after_delete(*args)
120
+ yield
147
121
  end
148
122
 
149
123
  def before_delete(*)
@@ -26,10 +26,13 @@ class Shrine
26
26
  uploader.opts[:keep_files] << :replaced if replaced
27
27
  end
28
28
 
29
- module InstanceMethods
30
- # We hook to the generic deleting, and check the appropriate phases.
31
- def delete(io, context = {})
32
- super unless opts[:keep_files].include?(context[:phase])
29
+ module AttacherMethods
30
+ def replace
31
+ super unless shrine_class.opts[:keep_files].include?(:replaced)
32
+ end
33
+
34
+ def destroy
35
+ super unless shrine_class.opts[:keep_files].include?(:destroyed)
33
36
  end
34
37
  end
35
38
  end
@@ -12,8 +12,8 @@ class Shrine
12
12
  # going on, or you simply want to have it logged for future debugging.
13
13
  # By default the logging output looks something like this:
14
14
  #
15
- # 2015-10-09T20:06:06.676Z #25602: UPLOAD[direct] ImageUploader[:avatar] User[29543] 1 file (0.1s)
16
- # 2015-10-09T20:06:06.854Z #25602: PROCESS[promote]: ImageUploader[:avatar] User[29543] 3 files (0.22s)
15
+ # 2015-10-09T20:06:06.676Z #25602: STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s)
16
+ # 2015-10-09T20:06:06.854Z #25602: PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s)
17
17
  # 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
18
18
  #
19
19
  # The plugin accepts the following options:
@@ -36,16 +36,20 @@ class Shrine
36
36
  # grep. If this is important to you, you can switch to another format:
37
37
  #
38
38
  # plugin :logging, format: :json
39
- # # {"action":"upload","phase":"direct","uploader":"ImageUploader","attachment":"avatar",...}
39
+ # # {"action":"upload","phase":"cache","uploader":"ImageUploader","attachment":"avatar",...}
40
40
  #
41
41
  # plugin :logging, format: :heroku
42
- # # action=upload phase=direct uploader=ImageUploader attachment=avatar record_class=User ...
42
+ # # action=upload phase=cache uploader=ImageUploader attachment=avatar record_class=User ...
43
43
  #
44
44
  # Logging is by default disabled in tests, but you can enable it by setting
45
45
  # `Shrine.logger.level = Logger::INFO`.
46
46
  module Logging
47
+ def self.load_dependencies(uploader, *)
48
+ uploader.plugin :hooks
49
+ end
50
+
47
51
  def self.configure(uploader, logger: nil, stream: $stdout, format: :human)
48
- uploader.logger = logger if logger
52
+ uploader.opts[:logging_logger] = logger
49
53
  uploader.opts[:logging_stream] = stream
50
54
  uploader.opts[:logging_format] = format
51
55
  end
@@ -57,7 +61,7 @@ class Shrine
57
61
 
58
62
  # Initializes a new logger if it hasn't been initialized.
59
63
  def logger
60
- @logger ||= (
64
+ @logger ||= opts[:logging_logger] || (
61
65
  logger = Logger.new(opts[:logging_stream])
62
66
  logger.level = Logger::INFO
63
67
  logger.level = Logger::WARN if ENV["RACK_ENV"] == "test"
@@ -78,22 +82,22 @@ class Shrine
78
82
  end
79
83
 
80
84
  module InstanceMethods
81
- def store(io, context = {})
85
+ def around_process(io, context)
86
+ log("process", io, context) { super }
87
+ end
88
+
89
+ def around_store(io, context)
82
90
  log("store", io, context) { super }
83
91
  end
84
92
 
85
- def delete(io, context = {})
93
+ def around_delete(io, context)
86
94
  log("delete", io, context) { super }
87
95
  end
88
96
 
89
97
  private
90
98
 
91
- def processed(io, context = {})
92
- log("process", io, context) { super }
93
- end
94
-
95
99
  # Collects the data and sends it for logging.
96
- def log(action, io, context)
100
+ def log(action, input, context)
97
101
  result, duration = benchmark { yield }
98
102
 
99
103
  _log(
@@ -103,7 +107,7 @@ class Shrine
103
107
  attachment: context[:name],
104
108
  record_class: (context[:record].class if context[:record]),
105
109
  record_id: (context[:record].id if context[:record].respond_to?(:id)),
106
- files: count(io),
110
+ files: (action == "process" ? [count(input), count(result)] : count(result)),
107
111
  duration: ("%.2f" % duration).to_f,
108
112
  ) unless result.nil?
109
113
 
@@ -124,16 +128,18 @@ class Shrine
124
128
  components.last << "[:#{data[:attachment]}]" if data[:attachment]
125
129
  components << "#{data[:record_class]}" if data[:record_class]
126
130
  components.last << "[#{data[:record_id]}]" if data[:record_id]
127
- components << (data[:files] > 1 ? "#{data[:files]} files" : "#{data[:files]} file")
131
+ components << "#{Array(data[:files]).join("-")} #{"file#{"s" if Array(data[:files]).any?{|n| n > 1}}"}"
128
132
  components << "(#{data[:duration]}s)"
129
133
  components.join(" ")
130
134
  end
131
135
 
132
136
  def _log_message_json(data)
137
+ data[:files] = Array(data[:files]).join("-")
133
138
  data.to_json
134
139
  end
135
140
 
136
141
  def _log_message_heroku(data)
142
+ data[:files] = Array(data[:files]).join("-")
137
143
  data.map { |key, value| "#{key}=#{value}" }.join(" ")
138
144
  end
139
145