paperclip 3.0.3 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of paperclip might be problematic. Click here for more details.
- checksums.yaml +15 -0
- data/.gitignore +2 -1
- data/.travis.yml +3 -0
- data/Appraisals +8 -3
- data/Gemfile +1 -1
- data/LICENSE +1 -1
- data/NEWS +198 -35
- data/README.md +332 -113
- data/features/basic_integration.feature +24 -12
- data/features/migration.feature +94 -0
- data/features/rake_tasks.feature +2 -3
- data/features/step_definitions/attachment_steps.rb +28 -0
- data/features/step_definitions/rails_steps.rb +94 -8
- data/features/step_definitions/s3_steps.rb +1 -1
- data/features/step_definitions/web_steps.rb +3 -3
- data/features/support/fakeweb.rb +4 -1
- data/features/support/file_helpers.rb +10 -0
- data/features/support/rails.rb +18 -2
- data/gemfiles/3.0.gemfile +2 -2
- data/gemfiles/3.1.gemfile +2 -2
- data/gemfiles/3.2.gemfile +2 -2
- data/gemfiles/4.0.gemfile +11 -0
- data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +4 -8
- data/lib/paperclip/attachment.rb +96 -43
- data/lib/paperclip/attachment_registry.rb +57 -0
- data/lib/paperclip/callbacks.rb +2 -2
- data/lib/paperclip/content_type_detector.rb +78 -0
- data/lib/paperclip/file_command_content_type_detector.rb +32 -0
- data/lib/paperclip/filename_cleaner.rb +16 -0
- data/lib/paperclip/geometry.rb +66 -30
- data/lib/paperclip/geometry_detector_factory.rb +41 -0
- data/lib/paperclip/geometry_parser_factory.rb +31 -0
- data/lib/paperclip/glue.rb +2 -8
- data/lib/paperclip/has_attached_file.rb +99 -0
- data/lib/paperclip/helpers.rb +12 -15
- data/lib/paperclip/interpolations/plural_cache.rb +17 -0
- data/lib/paperclip/interpolations.rb +15 -5
- data/lib/paperclip/io_adapters/abstract_adapter.rb +45 -0
- data/lib/paperclip/io_adapters/attachment_adapter.rb +14 -49
- data/lib/paperclip/io_adapters/data_uri_adapter.rb +27 -0
- data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
- data/lib/paperclip/io_adapters/file_adapter.rb +8 -69
- data/lib/paperclip/io_adapters/identity_adapter.rb +1 -1
- data/lib/paperclip/io_adapters/nil_adapter.rb +2 -2
- data/lib/paperclip/io_adapters/stringio_adapter.rb +16 -45
- data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +17 -40
- data/lib/paperclip/io_adapters/uri_adapter.rb +44 -0
- data/lib/paperclip/matchers/have_attached_file_matcher.rb +1 -5
- data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +36 -17
- data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +5 -1
- data/lib/paperclip/matchers.rb +3 -3
- data/lib/paperclip/missing_attachment_styles.rb +11 -16
- data/lib/paperclip/processor.rb +12 -0
- data/lib/paperclip/railtie.rb +5 -1
- data/lib/paperclip/schema.rb +59 -23
- data/lib/paperclip/storage/filesystem.rb +23 -5
- data/lib/paperclip/storage/fog.rb +64 -25
- data/lib/paperclip/storage/s3.rb +93 -52
- data/lib/paperclip/style.rb +2 -2
- data/lib/paperclip/tempfile_factory.rb +21 -0
- data/lib/paperclip/thumbnail.rb +18 -3
- data/lib/paperclip/validators/attachment_content_type_validator.rb +38 -10
- data/lib/paperclip/validators/attachment_presence_validator.rb +8 -8
- data/lib/paperclip/validators/attachment_size_validator.rb +12 -7
- data/lib/paperclip/validators.rb +21 -2
- data/lib/paperclip/version.rb +1 -1
- data/lib/paperclip.rb +15 -44
- data/lib/tasks/paperclip.rake +26 -7
- data/paperclip.gemspec +11 -7
- data/test/attachment_definitions_test.rb +12 -0
- data/test/attachment_processing_test.rb +83 -0
- data/test/attachment_registry_test.rb +77 -0
- data/test/attachment_test.rb +253 -44
- data/test/content_type_detector_test.rb +50 -0
- data/test/file_command_content_type_detector_test.rb +25 -0
- data/test/filename_cleaner_test.rb +14 -0
- data/test/fixtures/animated +0 -0
- data/test/fixtures/animated.unknown +0 -0
- data/test/fixtures/rotated.jpg +0 -0
- data/test/generator_test.rb +26 -24
- data/test/geometry_detector_test.rb +24 -0
- data/test/geometry_parser_test.rb +73 -0
- data/test/geometry_test.rb +55 -4
- data/test/has_attached_file_test.rb +125 -0
- data/test/helper.rb +38 -7
- data/test/integration_test.rb +105 -89
- data/test/interpolations_test.rb +12 -0
- data/test/io_adapters/abstract_adapter_test.rb +58 -0
- data/test/io_adapters/attachment_adapter_test.rb +120 -33
- data/test/io_adapters/data_uri_adapter_test.rb +60 -0
- data/test/io_adapters/empty_string_adapter_test.rb +17 -0
- data/test/io_adapters/file_adapter_test.rb +32 -1
- data/test/io_adapters/stringio_adapter_test.rb +29 -10
- data/test/io_adapters/uploaded_file_adapter_test.rb +53 -5
- data/test/io_adapters/uri_adapter_test.rb +102 -0
- data/test/matchers/validate_attachment_presence_matcher_test.rb +22 -0
- data/test/meta_class_test.rb +32 -0
- data/test/paperclip_missing_attachment_styles_test.rb +4 -8
- data/test/paperclip_test.rb +27 -51
- data/test/plural_cache_test.rb +36 -0
- data/test/processor_test.rb +16 -0
- data/test/rake_test.rb +103 -0
- data/test/schema_test.rb +179 -77
- data/test/storage/filesystem_test.rb +26 -3
- data/test/storage/fog_test.rb +181 -3
- data/test/storage/s3_test.rb +239 -4
- data/test/style_test.rb +18 -14
- data/test/tempfile_factory_test.rb +13 -0
- data/test/thumbnail_test.rb +96 -16
- data/test/validators/attachment_content_type_validator_test.rb +181 -55
- data/test/validators/attachment_size_validator_test.rb +10 -0
- data/test/validators_test.rb +8 -1
- metadata +126 -92
- data/Gemfile.lock +0 -157
- data/features/support/fixtures/.boot_config.rb.swo +0 -0
- data/images.rake +0 -21
- data/lib/.DS_Store +0 -0
- data/lib/paperclip/.DS_Store +0 -0
- data/lib/paperclip/attachment_options.rb +0 -9
- data/lib/paperclip/instance_methods.rb +0 -35
- data/test/attachment_options_test.rb +0 -27
data/lib/paperclip/attachment.rb
CHANGED
@@ -12,7 +12,9 @@ module Paperclip
|
|
12
12
|
:convert_options => {},
|
13
13
|
:default_style => :original,
|
14
14
|
:default_url => "/:attachment/:style/missing.png",
|
15
|
+
:escape_url => true,
|
15
16
|
:restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
|
17
|
+
:filename_cleaner => nil,
|
16
18
|
:hash_data => ":class/:attachment/:id/:style/:updated_at",
|
17
19
|
:hash_digest => "SHA1",
|
18
20
|
:interpolator => Paperclip::Interpolations,
|
@@ -27,7 +29,8 @@ module Paperclip
|
|
27
29
|
:url_generator => Paperclip::UrlGenerator,
|
28
30
|
:use_default_time_zone => true,
|
29
31
|
:use_timestamp => true,
|
30
|
-
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
|
32
|
+
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
|
33
|
+
:check_validity_before_processing => true
|
31
34
|
}
|
32
35
|
end
|
33
36
|
|
@@ -36,7 +39,7 @@ module Paperclip
|
|
36
39
|
attr_accessor :post_processing
|
37
40
|
|
38
41
|
# Creates an Attachment object. +name+ is the name of the attachment,
|
39
|
-
# +instance+ is the
|
42
|
+
# +instance+ is the model object instance it's attached to, and
|
40
43
|
# +options+ is the same as the hash passed to +has_attached_file+.
|
41
44
|
#
|
42
45
|
# Options include:
|
@@ -46,7 +49,7 @@ module Paperclip
|
|
46
49
|
# +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
|
47
50
|
# +only_process+ - style args to be run through the post-processor. This defaults to the empty list
|
48
51
|
# +default_url+ - a URL for the missing image
|
49
|
-
# +default_style+ - the style to use when
|
52
|
+
# +default_style+ - the style to use when an argument is not specified e.g. #url, #path
|
50
53
|
# +storage+ - the storage mechanism. Defaults to :filesystem
|
51
54
|
# +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
|
52
55
|
# +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
|
@@ -57,9 +60,11 @@ module Paperclip
|
|
57
60
|
# +convert_options+ - flags passed to the +convert+ command for processing
|
58
61
|
# +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
|
59
62
|
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
|
60
|
-
# +preserve_files+ - whether to keep files on the filesystem when deleting
|
63
|
+
# +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
|
64
|
+
# +filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
|
61
65
|
# +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
|
62
66
|
# +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
|
67
|
+
# +escape_url+ - Perform URI escaping to URLs. Defaults to true
|
63
68
|
def initialize(name, instance, options = {})
|
64
69
|
@name = name
|
65
70
|
@instance = instance
|
@@ -90,8 +95,8 @@ module Paperclip
|
|
90
95
|
ensure_required_accessors!
|
91
96
|
file = Paperclip.io_adapters.for(uploaded_file)
|
92
97
|
|
93
|
-
|
94
|
-
self.clear(
|
98
|
+
return nil if not file.assignment?
|
99
|
+
self.clear(*only_process)
|
95
100
|
return nil if file.nil?
|
96
101
|
|
97
102
|
@queued_for_write[:original] = file
|
@@ -99,15 +104,18 @@ module Paperclip
|
|
99
104
|
instance_write(:content_type, file.content_type.to_s.strip)
|
100
105
|
instance_write(:file_size, file.size)
|
101
106
|
instance_write(:fingerprint, file.fingerprint) if instance_respond_to?(:fingerprint)
|
107
|
+
instance_write(:created_at, Time.now) if has_enabled_but_unset_created_at?
|
102
108
|
instance_write(:updated_at, Time.now)
|
103
109
|
|
104
110
|
@dirty = true
|
105
111
|
|
106
|
-
post_process(
|
112
|
+
post_process(*only_process) if post_processing
|
107
113
|
|
108
114
|
# Reset the file size if the original file was reprocessed.
|
109
115
|
instance_write(:file_size, @queued_for_write[:original].size)
|
110
116
|
instance_write(:fingerprint, @queued_for_write[:original].fingerprint) if instance_respond_to?(:fingerprint)
|
117
|
+
updater = :"#{name}_file_name_will_change!"
|
118
|
+
instance.send updater if instance.respond_to? updater
|
111
119
|
end
|
112
120
|
|
113
121
|
# Returns the public URL of the attachment with a given style. This does
|
@@ -133,7 +141,7 @@ module Paperclip
|
|
133
141
|
# +#new(Paperclip::Attachment, options_hash)+
|
134
142
|
# +#for(style_name, options_hash)+
|
135
143
|
def url(style_name = default_style, options = {})
|
136
|
-
default_options = {:timestamp => @options[:use_timestamp], :escape =>
|
144
|
+
default_options = {:timestamp => @options[:use_timestamp], :escape => @options[:escape_url]}
|
137
145
|
|
138
146
|
if options == true || options == false # Backwards compatibility.
|
139
147
|
@url_generator.for(style_name, default_options.merge(:timestamp => options))
|
@@ -142,6 +150,13 @@ module Paperclip
|
|
142
150
|
end
|
143
151
|
end
|
144
152
|
|
153
|
+
# Alias to +url+ that allows using the expiring_url method provided by the cloud
|
154
|
+
# storage implementations, but keep using filesystem storage for development and
|
155
|
+
# testing.
|
156
|
+
def expiring_url(time = 3600, style_name = default_style)
|
157
|
+
url(style_name)
|
158
|
+
end
|
159
|
+
|
145
160
|
# Returns the path of the attachment as defined by the :path option. If the
|
146
161
|
# file is stored in the filesystem the path refers to the path of the file
|
147
162
|
# on disk. If the file is stored in S3, the path is the "key" part of the
|
@@ -156,6 +171,10 @@ module Paperclip
|
|
156
171
|
url(style_name)
|
157
172
|
end
|
158
173
|
|
174
|
+
def as_json(options = nil)
|
175
|
+
to_s((options && options[:style]) || default_style)
|
176
|
+
end
|
177
|
+
|
159
178
|
def default_style
|
160
179
|
@options[:default_style]
|
161
180
|
end
|
@@ -166,13 +185,19 @@ module Paperclip
|
|
166
185
|
styles = styles.call(self) if styles.respond_to?(:call)
|
167
186
|
|
168
187
|
@normalized_styles = styles.dup
|
169
|
-
|
188
|
+
styles.each_pair do |name, options|
|
170
189
|
@normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
|
171
190
|
end
|
172
191
|
end
|
173
192
|
@normalized_styles
|
174
193
|
end
|
175
194
|
|
195
|
+
def only_process
|
196
|
+
only_process = @options[:only_process].dup
|
197
|
+
only_process = only_process.call(self) if only_process.respond_to?(:call)
|
198
|
+
only_process.map(&:to_sym)
|
199
|
+
end
|
200
|
+
|
176
201
|
def processors
|
177
202
|
processing_option = @options[:processors]
|
178
203
|
|
@@ -219,10 +244,8 @@ module Paperclip
|
|
219
244
|
# nil to the attachment *and saving*. This is permanent. If you wish to
|
220
245
|
# wipe out the existing attachment but not save, use #clear.
|
221
246
|
def destroy
|
222
|
-
|
223
|
-
|
224
|
-
save
|
225
|
-
end
|
247
|
+
clear
|
248
|
+
save
|
226
249
|
end
|
227
250
|
|
228
251
|
# Returns the uploaded file if present.
|
@@ -243,7 +266,7 @@ module Paperclip
|
|
243
266
|
end
|
244
267
|
|
245
268
|
# Returns the fingerprint of the file, if one's defined. The fingerprint is
|
246
|
-
# stored in the <attachment>
|
269
|
+
# stored in the <attachment>_fingerprint attribute of the model.
|
247
270
|
def fingerprint
|
248
271
|
instance_read(:fingerprint)
|
249
272
|
end
|
@@ -254,6 +277,15 @@ module Paperclip
|
|
254
277
|
instance_read(:content_type)
|
255
278
|
end
|
256
279
|
|
280
|
+
# Returns the creation time of the file as originally assigned, and
|
281
|
+
# lives in the <attachment>_created_at attribute of the model.
|
282
|
+
def created_at
|
283
|
+
if able_to_store_created_at?
|
284
|
+
time = instance_read(:created_at)
|
285
|
+
time && time.to_f.to_i
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
257
289
|
# Returns the last modified time of the file as originally assigned, and
|
258
290
|
# lives in the <attachment>_updated_at attribute of the model.
|
259
291
|
def updated_at
|
@@ -285,6 +317,7 @@ module Paperclip
|
|
285
317
|
begin
|
286
318
|
assign(self)
|
287
319
|
save
|
320
|
+
instance.save
|
288
321
|
rescue Errno::EACCES => e
|
289
322
|
warn "#{e} - skipping file."
|
290
323
|
false
|
@@ -300,6 +333,10 @@ module Paperclip
|
|
300
333
|
|
301
334
|
alias :present? :file?
|
302
335
|
|
336
|
+
def blank?
|
337
|
+
not present?
|
338
|
+
end
|
339
|
+
|
303
340
|
# Determines whether the instance responds to this attribute. Used to prevent
|
304
341
|
# calculations on fields we won't even store.
|
305
342
|
def instance_respond_to?(attr)
|
@@ -311,19 +348,18 @@ module Paperclip
|
|
311
348
|
# "avatar_file_name" field (assuming the attachment is called avatar).
|
312
349
|
def instance_write(attr, value)
|
313
350
|
setter = :"#{name}_#{attr}="
|
314
|
-
|
315
|
-
|
316
|
-
|
351
|
+
if instance.respond_to?(setter)
|
352
|
+
instance.send(setter, value)
|
353
|
+
end
|
317
354
|
end
|
318
355
|
|
319
356
|
# Reads the attachment-specific attribute on the instance. See instance_write
|
320
357
|
# for more details.
|
321
358
|
def instance_read(attr)
|
322
359
|
getter = :"#{name}_#{attr}"
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
instance.send(getter) if responds || attr.to_s == "file_name"
|
360
|
+
if instance.respond_to?(getter)
|
361
|
+
instance.send(getter)
|
362
|
+
end
|
327
363
|
end
|
328
364
|
|
329
365
|
private
|
@@ -344,10 +380,6 @@ module Paperclip
|
|
344
380
|
Paperclip.log(message)
|
345
381
|
end
|
346
382
|
|
347
|
-
def valid_assignment? file #:nodoc:
|
348
|
-
file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
|
349
|
-
end
|
350
|
-
|
351
383
|
def initialize_storage #:nodoc:
|
352
384
|
storage_class_name = @options[:storage].to_s.downcase.camelize
|
353
385
|
begin
|
@@ -359,18 +391,17 @@ module Paperclip
|
|
359
391
|
end
|
360
392
|
|
361
393
|
def extra_options_for(style) #:nodoc:
|
362
|
-
|
363
|
-
all_options = all_options.call(instance) if all_options.respond_to?(:call)
|
364
|
-
style_options = @options[:convert_options][style]
|
365
|
-
style_options = style_options.call(instance) if style_options.respond_to?(:call)
|
366
|
-
|
367
|
-
[ style_options, all_options ].compact.join(" ")
|
394
|
+
process_options(:convert_options, style)
|
368
395
|
end
|
369
396
|
|
370
397
|
def extra_source_file_options_for(style) #:nodoc:
|
371
|
-
|
398
|
+
process_options(:source_file_options, style)
|
399
|
+
end
|
400
|
+
|
401
|
+
def process_options(options_type, style) #:nodoc:
|
402
|
+
all_options = @options[options_type][:all]
|
372
403
|
all_options = all_options.call(instance) if all_options.respond_to?(:call)
|
373
|
-
style_options = @options[
|
404
|
+
style_options = @options[options_type][style]
|
374
405
|
style_options = style_options.call(instance) if style_options.respond_to?(:call)
|
375
406
|
|
376
407
|
[ style_options, all_options ].compact.join(" ")
|
@@ -381,7 +412,9 @@ module Paperclip
|
|
381
412
|
|
382
413
|
instance.run_paperclip_callbacks(:post_process) do
|
383
414
|
instance.run_paperclip_callbacks(:"#{name}_post_process") do
|
384
|
-
|
415
|
+
unless @options[:check_validity_before_processing] && instance.errors.any?
|
416
|
+
post_process_styles(*style_args)
|
417
|
+
end
|
385
418
|
end
|
386
419
|
end
|
387
420
|
end
|
@@ -421,14 +454,17 @@ module Paperclip
|
|
421
454
|
end
|
422
455
|
|
423
456
|
def queue_all_for_delete #:nodoc:
|
424
|
-
return if
|
425
|
-
@
|
426
|
-
|
427
|
-
|
457
|
+
return if !file?
|
458
|
+
unless @options[:preserve_files]
|
459
|
+
@queued_for_delete += [:original, *styles.keys].uniq.map do |style|
|
460
|
+
path(style) if exists?(style)
|
461
|
+
end.compact
|
462
|
+
end
|
428
463
|
instance_write(:file_name, nil)
|
429
464
|
instance_write(:content_type, nil)
|
430
465
|
instance_write(:file_size, nil)
|
431
466
|
instance_write(:fingerprint, nil)
|
467
|
+
instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
|
432
468
|
instance_write(:updated_at, nil)
|
433
469
|
end
|
434
470
|
|
@@ -440,14 +476,31 @@ module Paperclip
|
|
440
476
|
|
441
477
|
# called by storage after the writes are flushed and before @queued_for_writes is cleared
|
442
478
|
def after_flush_writes
|
479
|
+
@queued_for_write.each do |style, file|
|
480
|
+
file.close unless file.closed?
|
481
|
+
file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# You can either specifiy :restricted_characters or you can define your own
|
486
|
+
# :filename_cleaner object. This object needs to respond to #call and takes
|
487
|
+
# the filename that will be cleaned. It should return the cleaned filenme.
|
488
|
+
def filename_cleaner
|
489
|
+
@options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
|
443
490
|
end
|
444
491
|
|
445
492
|
def cleanup_filename(filename)
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
493
|
+
filename_cleaner.call(filename)
|
494
|
+
end
|
495
|
+
|
496
|
+
# Check if attachment database table has a created_at field
|
497
|
+
def able_to_store_created_at?
|
498
|
+
@instance.respond_to?("#{name}_created_at".to_sym)
|
499
|
+
end
|
500
|
+
|
501
|
+
# Check if attachment database table has a created_at field which is not yet set
|
502
|
+
def has_enabled_but_unset_created_at?
|
503
|
+
able_to_store_created_at? && !instance_read(:created_at)
|
451
504
|
end
|
452
505
|
end
|
453
506
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Paperclip
|
4
|
+
class AttachmentRegistry
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
def self.register(klass, attachment_name, attachment_options)
|
8
|
+
instance.register(klass, attachment_name, attachment_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.clear
|
12
|
+
instance.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.names_for(klass)
|
16
|
+
instance.names_for(klass)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.each_definition(&block)
|
20
|
+
instance.each_definition(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.definitions_for(klass)
|
24
|
+
instance.definitions_for(klass)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
clear
|
29
|
+
end
|
30
|
+
|
31
|
+
def register(klass, attachment_name, attachment_options)
|
32
|
+
@attachments ||= {}
|
33
|
+
@attachments[klass] ||= {}
|
34
|
+
@attachments[klass][attachment_name] = attachment_options
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear
|
38
|
+
@attachments = Hash.new { |h,k| h[k] = {} }
|
39
|
+
end
|
40
|
+
|
41
|
+
def names_for(klass)
|
42
|
+
@attachments[klass].keys
|
43
|
+
end
|
44
|
+
|
45
|
+
def each_definition
|
46
|
+
@attachments.each do |klass, attachments|
|
47
|
+
attachments.each do |name, options|
|
48
|
+
yield klass, name, options
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def definitions_for(klass)
|
54
|
+
@attachments[klass]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/paperclip/callbacks.rb
CHANGED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Paperclip
|
2
|
+
class ContentTypeDetector
|
3
|
+
# The content-type detection strategy is as follows:
|
4
|
+
#
|
5
|
+
# 1. Blank/Empty files: If there's no filename or the file is empty,
|
6
|
+
# provide a sensible default (application/octet-stream or inode/x-empty)
|
7
|
+
#
|
8
|
+
# 2. Calculated match: Return the first result that is found by both the
|
9
|
+
# `file` command and MIME::Types.
|
10
|
+
#
|
11
|
+
# 3. Standard types: Return the first standard (without an x- prefix) entry
|
12
|
+
# in MIME::Types
|
13
|
+
#
|
14
|
+
# 4. Experimental types: If there were no standard types in MIME::Types
|
15
|
+
# list, try to return the first experimental one
|
16
|
+
#
|
17
|
+
# 5. Raw `file` command: Just use the output of the `file` command raw, or
|
18
|
+
# a sensible default. This is cached from Step 2.
|
19
|
+
|
20
|
+
EMPTY_TYPE = "inode/x-empty"
|
21
|
+
SENSIBLE_DEFAULT = "application/octet-stream"
|
22
|
+
|
23
|
+
def initialize(filename)
|
24
|
+
@filename = filename
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a String describing the file's content type
|
28
|
+
def detect
|
29
|
+
if blank_name?
|
30
|
+
SENSIBLE_DEFAULT
|
31
|
+
elsif empty_file?
|
32
|
+
EMPTY_TYPE
|
33
|
+
elsif calculated_type_matches.any?
|
34
|
+
calculated_type_matches.first
|
35
|
+
elsif official_type_matches.any?
|
36
|
+
official_type_matches.first
|
37
|
+
elsif unofficial_type_matches.any?
|
38
|
+
unofficial_type_matches.first
|
39
|
+
else
|
40
|
+
type_from_file_command || SENSIBLE_DEFAULT
|
41
|
+
end.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def empty_file?
|
47
|
+
File.exists?(@filename) && File.size(@filename) == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def blank_name?
|
51
|
+
@filename.nil? || @filename.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty?
|
55
|
+
File.exists?(@filename) && File.size(@filename) == 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def possible_types
|
59
|
+
MIME::Types.type_for(@filename).collect(&:content_type)
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculated_type_matches
|
63
|
+
possible_types.select{|content_type| content_type == type_from_file_command }
|
64
|
+
end
|
65
|
+
|
66
|
+
def official_type_matches
|
67
|
+
possible_types.reject{|content_type| content_type.match(/\/x-/) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def unofficial_type_matches
|
71
|
+
possible_types.select{|content_type| content_type.match(/\/x-/) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def type_from_file_command
|
75
|
+
@type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Paperclip
|
2
|
+
class FileCommandContentTypeDetector
|
3
|
+
SENSIBLE_DEFAULT = "application/octet-stream"
|
4
|
+
|
5
|
+
def initialize(filename)
|
6
|
+
@filename = filename
|
7
|
+
end
|
8
|
+
|
9
|
+
def detect
|
10
|
+
type_from_file_command
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def type_from_file_command
|
16
|
+
type = begin
|
17
|
+
# On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
|
18
|
+
Paperclip.run("file", "-b --mime :file", :file => @filename)
|
19
|
+
rescue Cocaine::CommandLineError => e
|
20
|
+
Paperclip.log("Error while determining content type: #{e}")
|
21
|
+
SENSIBLE_DEFAULT
|
22
|
+
end
|
23
|
+
|
24
|
+
if type.nil? || type.match(/\(.*?\)/)
|
25
|
+
type = SENSIBLE_DEFAULT
|
26
|
+
end
|
27
|
+
type.split(/[:;\s]+/)[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Paperclip
|
3
|
+
class FilenameCleaner
|
4
|
+
def initialize(invalid_character_regex)
|
5
|
+
@invalid_character_regex = invalid_character_regex
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(filename)
|
9
|
+
if @invalid_character_regex
|
10
|
+
filename.gsub(@invalid_character_regex, "_")
|
11
|
+
else
|
12
|
+
filename
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/paperclip/geometry.rb
CHANGED
@@ -4,37 +4,40 @@ module Paperclip
|
|
4
4
|
class Geometry
|
5
5
|
attr_accessor :height, :width, :modifier
|
6
6
|
|
7
|
+
EXIF_ROTATED_ORIENTATION_VALUES = [5, 6, 7, 8]
|
8
|
+
|
7
9
|
# Gives a Geometry representing the given height and width
|
8
|
-
def initialize
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
#
|
35
|
-
def
|
36
|
-
if
|
37
|
-
|
10
|
+
def initialize(width = nil, height = nil, modifier = nil)
|
11
|
+
if width.is_a?(Hash)
|
12
|
+
options = width
|
13
|
+
@height = options[:height].to_f
|
14
|
+
@width = options[:width].to_f
|
15
|
+
@modifier = options[:modifier]
|
16
|
+
@orientation = options[:orientation].to_i
|
17
|
+
else
|
18
|
+
@height = height.to_f
|
19
|
+
@width = width.to_f
|
20
|
+
@modifier = modifier
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Extracts the Geometry from a file (or path to a file)
|
25
|
+
def self.from_file(file)
|
26
|
+
GeometryDetector.new(file).make
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extracts the Geometry from a "WxH,O" string
|
30
|
+
# Where W is the width, H is the height,
|
31
|
+
# and O is the EXIF orientation
|
32
|
+
def self.parse(string)
|
33
|
+
GeometryParser.new(string).make
|
34
|
+
end
|
35
|
+
|
36
|
+
# Swaps the height and width if necessary
|
37
|
+
def auto_orient
|
38
|
+
if EXIF_ROTATED_ORIENTATION_VALUES.include?(@orientation)
|
39
|
+
@height, @width = @width, @height
|
40
|
+
@orientation -= 4
|
38
41
|
end
|
39
42
|
end
|
40
43
|
|
@@ -101,6 +104,33 @@ module Paperclip
|
|
101
104
|
[ scale_geometry, crop_geometry ]
|
102
105
|
end
|
103
106
|
|
107
|
+
# resize to a new geometry
|
108
|
+
# @param geometry [String] the Paperclip geometry definition to resize to
|
109
|
+
# @example
|
110
|
+
# Paperclip::Geometry.new(150, 150).resize_to('50x50!')
|
111
|
+
# #=> Paperclip::Geometry(50, 50)
|
112
|
+
def resize_to(geometry)
|
113
|
+
new_geometry = Paperclip::Geometry.parse geometry
|
114
|
+
case new_geometry.modifier
|
115
|
+
when '!', '#'
|
116
|
+
new_geometry
|
117
|
+
when '>'
|
118
|
+
if new_geometry.width >= self.width && new_geometry.height >= self.height
|
119
|
+
self
|
120
|
+
else
|
121
|
+
scale_to new_geometry
|
122
|
+
end
|
123
|
+
when '<'
|
124
|
+
if new_geometry.width <= self.width || new_geometry.height <= self.height
|
125
|
+
self
|
126
|
+
else
|
127
|
+
scale_to new_geometry
|
128
|
+
end
|
129
|
+
else
|
130
|
+
scale_to new_geometry
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
104
134
|
private
|
105
135
|
|
106
136
|
def scaling dst, ratio
|
@@ -118,5 +148,11 @@ module Paperclip
|
|
118
148
|
"%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
|
119
149
|
end
|
120
150
|
end
|
151
|
+
|
152
|
+
# scale to the requested geometry and preserve the aspect ratio
|
153
|
+
def scale_to(new_geometry)
|
154
|
+
scale = [new_geometry.width.to_f / self.width.to_f , new_geometry.height.to_f / self.height.to_f].min
|
155
|
+
Paperclip::Geometry.new((self.width * scale).round, (self.height * scale).round)
|
156
|
+
end
|
121
157
|
end
|
122
158
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Paperclip
|
2
|
+
class GeometryDetector
|
3
|
+
def initialize(file)
|
4
|
+
@file = file
|
5
|
+
raise_if_blank_file
|
6
|
+
end
|
7
|
+
|
8
|
+
def make
|
9
|
+
geometry = GeometryParser.new(geometry_string.strip).make
|
10
|
+
geometry || raise(Errors::NotIdentifiedByImageMagickError.new)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def geometry_string
|
16
|
+
begin
|
17
|
+
silence_stream(STDERR) do
|
18
|
+
Paperclip.run("identify", "-format '%wx%h,%[exif:orientation]' :file", :file => "#{path}[0]")
|
19
|
+
end
|
20
|
+
rescue Cocaine::ExitStatusError
|
21
|
+
""
|
22
|
+
rescue Cocaine::CommandNotFoundError => e
|
23
|
+
raise_because_imagemagick_missing
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def path
|
28
|
+
@file.respond_to?(:path) ? @file.path : @file
|
29
|
+
end
|
30
|
+
|
31
|
+
def raise_if_blank_file
|
32
|
+
if path.blank?
|
33
|
+
raise Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def raise_because_imagemagick_missing
|
38
|
+
raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|