shrine 2.6.1 → 2.7.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.
- checksums.yaml +4 -4
- data/README.md +66 -27
- data/doc/attacher.md +8 -0
- data/doc/carrierwave.md +2 -2
- data/doc/creating_storages.md +3 -2
- data/doc/direct_s3.md +71 -56
- data/doc/multiple_files.md +3 -2
- data/doc/refile.md +18 -15
- data/doc/regenerating_versions.md +1 -1
- data/doc/securing_uploads.md +8 -8
- data/doc/testing.md +27 -21
- data/lib/shrine.rb +35 -24
- data/lib/shrine/plugins/activerecord.rb +22 -3
- data/lib/shrine/plugins/copy.rb +1 -1
- data/lib/shrine/plugins/data_uri.rb +3 -3
- data/lib/shrine/plugins/determine_mime_type.rb +24 -10
- data/lib/shrine/plugins/direct_upload.rb +4 -1
- data/lib/shrine/plugins/download_endpoint.rb +126 -63
- data/lib/shrine/plugins/keep_files.rb +1 -1
- data/lib/shrine/plugins/logging.rb +1 -0
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/presign_endpoint.rb +258 -0
- data/lib/shrine/plugins/rack_file.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +85 -0
- data/lib/shrine/plugins/remote_url.rb +5 -7
- data/lib/shrine/plugins/sequel.rb +1 -1
- data/lib/shrine/plugins/signature.rb +1 -1
- data/lib/shrine/plugins/upload_endpoint.rb +238 -0
- data/lib/shrine/storage/file_system.rb +3 -2
- data/lib/shrine/storage/s3.rb +62 -54
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +3 -4
- metadata +22 -33
data/doc/refile.md
CHANGED
@@ -193,20 +193,22 @@ multiple uploads directly to S3.
|
|
193
193
|
|
194
194
|
## Direct uploads
|
195
195
|
|
196
|
-
Shrine borrows Refile's idea of direct uploads, and ships with
|
197
|
-
|
198
|
-
generating presigns.
|
196
|
+
Shrine borrows Refile's idea of direct uploads, and ships with
|
197
|
+
`upload_endpoint` and `presign_endpoint` plugins which provide endpoints for
|
198
|
+
uploading files and generating presigns.
|
199
199
|
|
200
200
|
```rb
|
201
|
-
Shrine.plugin :
|
202
|
-
#
|
203
|
-
|
201
|
+
Shrine.plugin :upload_endpoint
|
202
|
+
Shrine.upload_endpoint(:cache) # Rack app that uploads files to specified storage
|
203
|
+
|
204
|
+
Shrine.plugin :upload_endpoint
|
205
|
+
Shrine.presign_endpoint(:cache) # Rack app that generates presigns for specified storage
|
204
206
|
```
|
205
207
|
|
206
208
|
Unlike Refile, Shrine doesn't ship with complete JavaScript which you can just
|
207
209
|
include to make it work. Instead, you're expected to use one of the excellent
|
208
|
-
JavaScript libraries for generic file uploads like [
|
209
|
-
also the [Direct Uploads to S3] guide.
|
210
|
+
JavaScript libraries for generic file uploads like [FineUploader], [Dropzone]
|
211
|
+
or [jQuery-File-Upload]. See also the [Direct Uploads to S3] guide.
|
210
212
|
|
211
213
|
## Migrating from Refile
|
212
214
|
|
@@ -287,22 +289,21 @@ Shrine.storages = {
|
|
287
289
|
|
288
290
|
#### `.app`, `.mount_point`, `.automount`
|
289
291
|
|
290
|
-
The `
|
291
|
-
|
292
|
+
The `upload_endpoint` and `presign_endpoint` plugins provide methods for
|
293
|
+
generating Rack apps, but you need to mount them explicitly:
|
292
294
|
|
293
295
|
```rb
|
294
296
|
# config/routes.rb
|
295
297
|
Rails.application.routes.draw do
|
296
|
-
# adds `POST /
|
297
|
-
mount ImageUploader
|
298
|
+
# adds `POST /images/upload` endpoint
|
299
|
+
mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
|
298
300
|
end
|
299
301
|
```
|
300
302
|
|
301
303
|
#### `.allow_uploads_to`
|
302
304
|
|
303
|
-
|
304
|
-
|
305
|
-
```
|
305
|
+
The `Shrine.upload_endpoint` and `Shrine.presign_endpoint` require you to
|
306
|
+
specify the storage that will be used.
|
306
307
|
|
307
308
|
#### `.logger`
|
308
309
|
|
@@ -477,6 +478,8 @@ Shrine.plugin :remote_url
|
|
477
478
|
[shrine-uploadcare]: https://github.com/janko-m/shrine-uploadcare
|
478
479
|
[Attache]: https://github.com/choonkeat/attache
|
479
480
|
[image_processing]: https://github.com/janko-m/image_processing
|
481
|
+
[FineUploader]: https://github.com/FineUploader/fine-uploader
|
482
|
+
[Dropzone]: https://github.com/enyo/dropzone
|
480
483
|
[jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
481
484
|
[Direct Uploads to S3]: http://shrinerb.com/rdoc/files/doc/direct_s3_md.html
|
482
485
|
[demo app]: https://github.com/janko-m/shrine/tree/master/demo
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
While your app is serving uploads in production, you may realize that you want
|
4
4
|
to change how your attachment's versions are generated. This means that, in
|
5
|
-
addition to changing
|
5
|
+
addition to changing your processing code, you also need to reprocess the
|
6
6
|
existing attachments. This guide is aimed to help doing this migration with
|
7
7
|
zero downtime and no unused files left in the main storage.
|
8
8
|
|
data/doc/securing_uploads.md
CHANGED
@@ -50,20 +50,20 @@ being uploaded to cache and the temporary directory.
|
|
50
50
|
|
51
51
|
### Limiting filesize in direct uploads
|
52
52
|
|
53
|
-
If you're doing direct uploads with the `
|
54
|
-
in the `:max_size` option
|
55
|
-
|
53
|
+
If you're doing direct uploads with the `upload_endpoint` plugin, you can pass
|
54
|
+
in the `:max_size` option to reject files that are larger than the specified
|
55
|
+
limit:
|
56
56
|
|
57
57
|
```rb
|
58
|
-
plugin :
|
58
|
+
plugin :upload_endpoint, max_size: 20*1024*1024 # 20 MB
|
59
59
|
```
|
60
60
|
|
61
|
-
|
62
|
-
|
61
|
+
If you're doing direct uploads to Amazon S3 using the `presign_endpoint`
|
62
|
+
plugin, you can pass in the `:content_length_range` presign option:
|
63
63
|
|
64
64
|
```rb
|
65
|
-
plugin :
|
66
|
-
{content_length_range: 0..20*1024*1024}
|
65
|
+
plugin :presign_endpoint, presign_options: -> (request) do
|
66
|
+
{ content_length_range: 0..20*1024*1024 }
|
67
67
|
end
|
68
68
|
```
|
69
69
|
|
data/doc/testing.md
CHANGED
@@ -58,16 +58,8 @@ Shrine.storages = {
|
|
58
58
|
}
|
59
59
|
```
|
60
60
|
|
61
|
-
Alternatively, if you're using Amazon S3 storage, in tests
|
62
|
-
|
63
|
-
`s3.amazonaws.com` it should use the host of your FakeS3 server when generating
|
64
|
-
URLs.
|
65
|
-
|
66
|
-
```rb
|
67
|
-
Shrine::Storage::S3.new(endpoint: "http://localhost:10000")
|
68
|
-
```
|
69
|
-
|
70
|
-
Note that for using FakeS3 you need aws-sdk version 2.2.25 or higher.
|
61
|
+
Alternatively, if you're using Amazon S3 storage, in tests you can use
|
62
|
+
[aws-sdk-ruby stubs].
|
71
63
|
|
72
64
|
## Test data
|
73
65
|
|
@@ -76,14 +68,14 @@ you can have the test file assigned dynamically when the record is created:
|
|
76
68
|
|
77
69
|
```rb
|
78
70
|
factory :photo do
|
79
|
-
image File.open("test/files/image.jpg")
|
71
|
+
image { File.open("test/files/image.jpg") }
|
80
72
|
end
|
81
73
|
```
|
82
74
|
|
83
75
|
On the other hand, if you're setting up test data using Rails' YAML fixtures,
|
84
76
|
you unfortunately won't be able to use them for assigning files. This is
|
85
77
|
because Rails fixtures only allow assigning primitive data types, and don't
|
86
|
-
allow you to specify Shrine attributes
|
78
|
+
allow you to specify Shrine attributes - you can only assign to columns
|
87
79
|
directly.
|
88
80
|
|
89
81
|
## Background jobs
|
@@ -250,15 +242,29 @@ isolation.
|
|
250
242
|
|
251
243
|
## Direct upload
|
252
244
|
|
253
|
-
|
254
|
-
|
255
|
-
|
245
|
+
If you've set up direct uploads to Amazon S3 (using the `presign_endpoint`
|
246
|
+
plugin), in tests you'll probably want to just use filesystem or memory storage
|
247
|
+
to avoid network requests.
|
248
|
+
|
249
|
+
The easiest way to do that is to add an `upload_endpoint`, modify it so that it
|
250
|
+
behaves like S3, and change `presign_endpoint` response to point to the upload
|
251
|
+
endpoint. Here is how one could modify the test helper in a Rails application:
|
256
252
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
253
|
+
```rb
|
254
|
+
# test/test_helper.rb
|
255
|
+
|
256
|
+
# create and mount a fake S3 upload endpoint
|
257
|
+
Shrine.plugin :upload_endpoint
|
258
|
+
fake_s3 = Shrine.upload_endpoint(:cache, upload_context: -> (request) {
|
259
|
+
{ location: request.params["key"].match(/^cache\//).post_match }
|
260
|
+
})
|
261
|
+
Rails.application.routes.prepend { mount fake_s3 => "/s3" }
|
262
|
+
|
263
|
+
# override presigns to return URLs to the fake S3 upload endpoint
|
264
|
+
Shrine.plugin :presign_endpoint, presign: -> (id, options, request) do
|
265
|
+
Struct.new(:url, :fields).new("#{request.base_url}/s3", { "key" => "cache/#{id}" })
|
266
|
+
end
|
267
|
+
```
|
262
268
|
|
263
269
|
[DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
|
264
270
|
[shrine-memory]: https://github.com/janko-m/shrine-memory
|
@@ -267,4 +273,4 @@ provided by the `direct_upload` app mounted in your routes.
|
|
267
273
|
[`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
|
268
274
|
[Rack::Test]: https://github.com/brynary/rack-test
|
269
275
|
[Rack::TestApp]: https://github.com/kwatch/rack-test_app
|
270
|
-
[
|
276
|
+
[aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
|
data/lib/shrine.rb
CHANGED
@@ -154,8 +154,8 @@ class Shrine
|
|
154
154
|
# class Photo
|
155
155
|
# include Shrine.attachment(:image) # creates a Shrine::Attachment object
|
156
156
|
# end
|
157
|
-
def attachment(name)
|
158
|
-
self::Attachment.new(name)
|
157
|
+
def attachment(name, **options)
|
158
|
+
self::Attachment.new(name, **options)
|
159
159
|
end
|
160
160
|
alias [] attachment
|
161
161
|
|
@@ -167,7 +167,6 @@ class Shrine
|
|
167
167
|
def uploaded_file(object, &block)
|
168
168
|
case object
|
169
169
|
when String
|
170
|
-
deprecation("Giving a string to Shrine.uploaded_file is deprecated and won't be possible in Shrine 3. Use Attacher#uploaded_file instead.")
|
171
170
|
uploaded_file(JSON.parse(object), &block)
|
172
171
|
when Hash
|
173
172
|
uploaded_file(self::UploadedFile.new(object), &block)
|
@@ -390,17 +389,22 @@ class Shrine
|
|
390
389
|
|
391
390
|
module AttachmentMethods
|
392
391
|
# Instantiates an attachment module for a given attribute name, which
|
393
|
-
# can then be included to a model class.
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
# correct attacher instance.
|
399
|
-
class_variable_set(:"@@#{name}_attacher_class", shrine_class::Attacher)
|
392
|
+
# can then be included to a model class. Second argument will be passed
|
393
|
+
# to an attacher module.
|
394
|
+
def initialize(name, **options)
|
395
|
+
@name = name
|
396
|
+
@options = options
|
400
397
|
|
401
398
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
402
399
|
def #{name}_attacher
|
403
|
-
@#{name}_attacher ||=
|
400
|
+
@#{name}_attacher ||= (
|
401
|
+
attachments = self.class.ancestors.grep(Shrine::Attachment)
|
402
|
+
attachment = attachments.find { |mod| mod.attachment_name == :#{name} }
|
403
|
+
attacher_class = attachment.shrine_class::Attacher
|
404
|
+
options = attachment.options
|
405
|
+
|
406
|
+
attacher_class.new(self, :#{name}, options)
|
407
|
+
)
|
404
408
|
end
|
405
409
|
|
406
410
|
def #{name}=(value)
|
@@ -417,18 +421,28 @@ class Shrine
|
|
417
421
|
RUBY
|
418
422
|
end
|
419
423
|
|
420
|
-
#
|
424
|
+
# Returns name of the attachment this module provides.
|
425
|
+
def attachment_name
|
426
|
+
@name
|
427
|
+
end
|
428
|
+
|
429
|
+
# Returns options that are to be passed to the Attacher.
|
430
|
+
def options
|
431
|
+
@options
|
432
|
+
end
|
433
|
+
|
434
|
+
# Returns class name with attachment name included.
|
421
435
|
#
|
422
436
|
# Shrine[:image].to_s #=> "#<Shrine::Attachment(image)>"
|
423
437
|
def to_s
|
424
|
-
"#<#{self.class.inspect}(#{
|
438
|
+
"#<#{self.class.inspect}(#{attachment_name})>"
|
425
439
|
end
|
426
440
|
|
427
|
-
#
|
441
|
+
# Returns class name with attachment name included.
|
428
442
|
#
|
429
443
|
# Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
|
430
444
|
def inspect
|
431
|
-
"#<#{self.class.inspect}(#{
|
445
|
+
"#<#{self.class.inspect}(#{attachment_name})>"
|
432
446
|
end
|
433
447
|
|
434
448
|
# Returns the Shrine class that this attachment's class is namespaced
|
@@ -459,7 +473,8 @@ class Shrine
|
|
459
473
|
# end
|
460
474
|
# end
|
461
475
|
def validate(&block)
|
462
|
-
|
476
|
+
define_method(:validate_block, &block)
|
477
|
+
private :validate_block
|
463
478
|
end
|
464
479
|
end
|
465
480
|
|
@@ -519,7 +534,7 @@ class Shrine
|
|
519
534
|
# Runs the validations defined by `Attacher.validate`.
|
520
535
|
def validate
|
521
536
|
errors.clear
|
522
|
-
|
537
|
+
validate_block if get
|
523
538
|
end
|
524
539
|
|
525
540
|
# Returns true if a new file has been attached.
|
@@ -629,11 +644,7 @@ class Shrine
|
|
629
644
|
# Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
|
630
645
|
# files as JSON strings.
|
631
646
|
def uploaded_file(object, &block)
|
632
|
-
|
633
|
-
uploaded_file(JSON.parse(object), &block)
|
634
|
-
else
|
635
|
-
shrine_class.uploaded_file(object, &block)
|
636
|
-
end
|
647
|
+
shrine_class.uploaded_file(object, &block)
|
637
648
|
end
|
638
649
|
|
639
650
|
# The name of the attribute on the model instance that is used to store
|
@@ -661,9 +672,9 @@ class Shrine
|
|
661
672
|
_set(uploaded_file)
|
662
673
|
end
|
663
674
|
|
664
|
-
#
|
675
|
+
# Performs validation actually.
|
676
|
+
# This method is redefined with `Attacher.validate`.
|
665
677
|
def validate_block
|
666
|
-
shrine_class.opts[:validate]
|
667
678
|
end
|
668
679
|
|
669
680
|
# Converts the UploadedFile to a data hash and writes it to the
|
@@ -40,6 +40,10 @@ class Shrine
|
|
40
40
|
# end
|
41
41
|
# end
|
42
42
|
#
|
43
|
+
# Note that ActiveRecord currently has a [bug with transaction callbacks],
|
44
|
+
# so if you have any "after commit" callbacks, make sure to include Shrine's
|
45
|
+
# attachment module *after* they have all been defined.
|
46
|
+
#
|
43
47
|
# If you don't want the attachment module to add any callbacks to the
|
44
48
|
# model, and would instead prefer to call these actions manually, you can
|
45
49
|
# disable callbacks:
|
@@ -49,8 +53,21 @@ class Shrine
|
|
49
53
|
# ## Validations
|
50
54
|
#
|
51
55
|
# Additionally, any Shrine validation errors will be added to
|
52
|
-
# ActiveRecord's errors upon validation.
|
53
|
-
#
|
56
|
+
# ActiveRecord's errors upon validation. Note that Shrine validation
|
57
|
+
# messages don't have to be strings, they can also be symbols or symbols
|
58
|
+
# and options, which allows them to be internationalized together with
|
59
|
+
# other ActiveRecord validation messages.
|
60
|
+
#
|
61
|
+
# class MyUploader < Shrine
|
62
|
+
# plugin :validation_helpers
|
63
|
+
#
|
64
|
+
# Attacher.validate do
|
65
|
+
# validate_max_size 256 * 1024**2, message: ->(max) { [:max_size, max: max] }
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# If you want to validate presence of the attachment, you can do it
|
70
|
+
# directly on the model.
|
54
71
|
#
|
55
72
|
# class User < ActiveRecord::Base
|
56
73
|
# include ImageUploader::Attachment.new(:avatar)
|
@@ -61,6 +78,8 @@ class Shrine
|
|
61
78
|
# model errors, you can disable it:
|
62
79
|
#
|
63
80
|
# plugin :activerecord, validations: false
|
81
|
+
#
|
82
|
+
# [bug with transaction callbacks]: https://github.com/rails/rails/issues/14493
|
64
83
|
module Activerecord
|
65
84
|
def self.configure(uploader, opts = {})
|
66
85
|
uploader.opts[:activerecord_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:activerecord_callbacks, true))
|
@@ -78,7 +97,7 @@ class Shrine
|
|
78
97
|
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1 if opts[:activerecord_validations]
|
79
98
|
validate do
|
80
99
|
#{@name}_attacher.errors.each do |message|
|
81
|
-
errors.add(:#{@name}, message)
|
100
|
+
errors.add(:#{@name}, *message)
|
82
101
|
end
|
83
102
|
end
|
84
103
|
RUBY
|
data/lib/shrine/plugins/copy.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "base64"
|
2
2
|
require "strscan"
|
3
|
-
require "cgi
|
3
|
+
require "cgi"
|
4
4
|
require "stringio"
|
5
5
|
require "forwardable"
|
6
6
|
|
@@ -169,7 +169,7 @@ class Shrine
|
|
169
169
|
# Returns contents of the file base64-encoded.
|
170
170
|
def base64
|
171
171
|
binary = open { |io| io.read }
|
172
|
-
result = Base64.
|
172
|
+
result = Base64.strict_encode64(binary)
|
173
173
|
binary.clear # deallocate string
|
174
174
|
result
|
175
175
|
end
|
@@ -189,7 +189,7 @@ class Shrine
|
|
189
189
|
end
|
190
190
|
|
191
191
|
extend Forwardable
|
192
|
-
delegate
|
192
|
+
delegate [:read, :size, :rewind, :eof?] => :@io
|
193
193
|
|
194
194
|
def close
|
195
195
|
@io.close
|
@@ -30,7 +30,12 @@ class Shrine
|
|
30
30
|
#
|
31
31
|
# :mime_types
|
32
32
|
# : Uses the [mime-types] gem to determine the MIME type from the file
|
33
|
-
#
|
33
|
+
# extension. Note that unlike other solutions, this analyzer is not
|
34
|
+
# guaranteed to return the actual MIME type of the file.
|
35
|
+
#
|
36
|
+
# :mini_mime
|
37
|
+
# : Uses the [mini_mime] gem to determine the MIME type from the file
|
38
|
+
# extension. Note that unlike other solutions, this analyzer is not
|
34
39
|
# guaranteed to return the actual MIME type of the file.
|
35
40
|
#
|
36
41
|
# :default
|
@@ -65,6 +70,7 @@ class Shrine
|
|
65
70
|
# [ruby-filemagic]: https://github.com/blackwinter/ruby-filemagic
|
66
71
|
# [mimemagic]: https://github.com/minad/mimemagic
|
67
72
|
# [mime-types]: https://github.com/mime-types/ruby-mime-types
|
73
|
+
# [mini_mime]: https://github.com/discourse/mini_mime
|
68
74
|
module DetermineMimeType
|
69
75
|
def self.configure(uploader, opts = {})
|
70
76
|
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
|
@@ -115,7 +121,7 @@ class Shrine
|
|
115
121
|
end
|
116
122
|
|
117
123
|
class MimeTypeAnalyzer
|
118
|
-
SUPPORTED_TOOLS = [:file, :filemagic, :mimemagic, :mime_types]
|
124
|
+
SUPPORTED_TOOLS = [:file, :filemagic, :mimemagic, :mime_types, :mini_mime]
|
119
125
|
MAGIC_NUMBER = 256 * 1024
|
120
126
|
|
121
127
|
def initialize(tool)
|
@@ -125,8 +131,11 @@ class Shrine
|
|
125
131
|
end
|
126
132
|
|
127
133
|
def call(io)
|
134
|
+
return nil if io.eof? # empty file doesn't have a MIME type
|
135
|
+
|
128
136
|
mime_type = send(:"extract_with_#{@tool}", io)
|
129
137
|
io.rewind
|
138
|
+
|
130
139
|
mime_type
|
131
140
|
end
|
132
141
|
|
@@ -135,8 +144,8 @@ class Shrine
|
|
135
144
|
def extract_with_file(io)
|
136
145
|
require "open3"
|
137
146
|
|
138
|
-
cmd
|
139
|
-
options = {stdin_data: io.read(MAGIC_NUMBER), binmode: true}
|
147
|
+
cmd = %W[file --mime-type --brief -]
|
148
|
+
options = { stdin_data: io.read(MAGIC_NUMBER), binmode: true }
|
140
149
|
|
141
150
|
begin
|
142
151
|
stdout, stderr, status = Open3.capture3(*cmd, options)
|
@@ -168,15 +177,20 @@ class Shrine
|
|
168
177
|
end
|
169
178
|
|
170
179
|
def extract_with_mime_types(io)
|
171
|
-
|
172
|
-
require "mime/types/columnar"
|
173
|
-
rescue LoadError
|
174
|
-
require "mime/types"
|
175
|
-
end
|
180
|
+
require "mime/types"
|
176
181
|
|
177
182
|
if filename = extract_filename(io)
|
178
183
|
mime_type = MIME::Types.of(filename).first
|
179
|
-
mime_type.
|
184
|
+
mime_type.content_type if mime_type
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def extract_with_mini_mime(io)
|
189
|
+
require "mini_mime"
|
190
|
+
|
191
|
+
if filename = extract_filename(io)
|
192
|
+
info = MiniMime.lookup_by_filename(filename)
|
193
|
+
info.content_type if info
|
180
194
|
end
|
181
195
|
end
|
182
196
|
|