path-paperclip 2.3.3

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.
Files changed (57) hide show
  1. data/LICENSE +26 -0
  2. data/README.rdoc +198 -0
  3. data/Rakefile +76 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +1 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +440 -0
  12. data/lib/paperclip/attachment.rb +401 -0
  13. data/lib/paperclip/callback_compatability.rb +61 -0
  14. data/lib/paperclip/geometry.rb +150 -0
  15. data/lib/paperclip/interpolations.rb +113 -0
  16. data/lib/paperclip/iostream.rb +59 -0
  17. data/lib/paperclip/matchers.rb +33 -0
  18. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  19. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +75 -0
  20. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  21. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  22. data/lib/paperclip/processor.rb +49 -0
  23. data/lib/paperclip/railtie.rb +20 -0
  24. data/lib/paperclip/storage.rb +258 -0
  25. data/lib/paperclip/style.rb +90 -0
  26. data/lib/paperclip/thumbnail.rb +78 -0
  27. data/lib/paperclip/upfile.rb +52 -0
  28. data/lib/paperclip/version.rb +3 -0
  29. data/lib/tasks/paperclip.rake +95 -0
  30. data/rails/init.rb +2 -0
  31. data/shoulda_macros/paperclip.rb +119 -0
  32. data/test/attachment_test.rb +796 -0
  33. data/test/database.yml +4 -0
  34. data/test/fixtures/12k.png +0 -0
  35. data/test/fixtures/50x50.png +0 -0
  36. data/test/fixtures/5k.png +0 -0
  37. data/test/fixtures/bad.png +1 -0
  38. data/test/fixtures/ceedub.gif +0 -0
  39. data/test/fixtures/s3.yml +8 -0
  40. data/test/fixtures/text.txt +1 -0
  41. data/test/fixtures/twopage.pdf +0 -0
  42. data/test/geometry_test.rb +177 -0
  43. data/test/helper.rb +152 -0
  44. data/test/integration_test.rb +610 -0
  45. data/test/interpolations_test.rb +135 -0
  46. data/test/iostream_test.rb +78 -0
  47. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  48. data/test/matchers/validate_attachment_content_type_matcher_test.rb +54 -0
  49. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  50. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  51. data/test/paperclip_test.rb +389 -0
  52. data/test/processor_test.rb +10 -0
  53. data/test/storage_test.rb +407 -0
  54. data/test/style_test.rb +141 -0
  55. data/test/thumbnail_test.rb +227 -0
  56. data/test/upfile_test.rb +36 -0
  57. metadata +221 -0
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @other_args = definition
22
+ else
23
+ @geometry, @format = [definition, nil].flatten[0..1]
24
+ @other_args = {}
25
+ end
26
+ @format = nil if @format.blank?
27
+ end
28
+
29
+ # retrieves from the attachment the processors defined in the has_attached_file call
30
+ # (which method (in the attachment) will call any supplied procs)
31
+ # There is an important change of interface here: a style rule can set its own processors
32
+ # by default we behave as before, though.
33
+ def processors
34
+ @processors || attachment.processors
35
+ end
36
+
37
+ # retrieves from the attachment the whiny setting
38
+ def whiny
39
+ attachment.whiny
40
+ end
41
+
42
+ # returns true if we're inclined to grumble
43
+ def whiny?
44
+ !!whiny
45
+ end
46
+
47
+ def convert_options
48
+ attachment.send(:extra_options_for, name)
49
+ end
50
+
51
+ # returns the geometry string for this style
52
+ # if a proc has been supplied, we call it here
53
+ def geometry
54
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
55
+ end
56
+
57
+ # Supplies the hash of options that processors expect to receive as their second argument
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
+ # initialization and any procs are called here, just before post-processing.
60
+ def processor_options
61
+ args = {}
62
+ @other_args.each do |k,v|
63
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
+ end
65
+ [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
+ (arg = send(k)) && args[k] = arg
67
+ end
68
+ args
69
+ end
70
+
71
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
+ # eg. @attachment.styles[:large][:geometry]@ will still work
73
+ def [](key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
+ send(key)
76
+ elsif defined? @other_args[key]
77
+ @other_args[key]
78
+ end
79
+ end
80
+
81
+ def []=(key, value)
82
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
+ send("#{key}=".intern, value)
84
+ else
85
+ @other_args[key] = value
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,78 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, options = {}, attachment = nil
14
+ super
15
+
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
27
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
28
+
29
+ @current_format = File.extname(@file.path)
30
+ @basename = File.basename(@file.path, @current_format)
31
+
32
+ end
33
+
34
+ # Returns true if the +target_geometry+ is meant to crop.
35
+ def crop?
36
+ @crop
37
+ end
38
+
39
+ # Returns true if the image is meant to make use of additional convert options.
40
+ def convert_options?
41
+ !@convert_options.nil? && !@convert_options.empty?
42
+ end
43
+
44
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
45
+ # that contains the new image.
46
+ def make
47
+ src = @file
48
+ dst = Tempfile.new([@basename, @format].compact.join("."))
49
+ dst.binmode
50
+
51
+ begin
52
+ options = [
53
+ source_file_options,
54
+ "#{ File.expand_path(src.path) }[0]",
55
+ transformation_command,
56
+ convert_options,
57
+ "#{ File.expand_path(dst.path) }"
58
+ ].flatten.compact
59
+
60
+ success = Paperclip.run("convert", *options)
61
+ rescue PaperclipCommandLineError => e
62
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
63
+ end
64
+
65
+ dst
66
+ end
67
+
68
+ # Returns the command ImageMagick's +convert+ needs to transform the image
69
+ # into the thumbnail.
70
+ def transformation_command
71
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
72
+ trans = []
73
+ trans << "-resize" << scale unless scale.nil? || scale.empty?
74
+ trans << "-crop" << crop << "+repage" if crop
75
+ trans
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,52 @@
1
+ module Paperclip
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case type
11
+ when %r"jp(e|g|eg)" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "js" then "application/js"
17
+ when "csv", "xml", "css" then "text/#{type}"
18
+ else
19
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
20
+ content_type = (Paperclip.run("file", "--mime-type", self.path).split(':').last.strip rescue "application/x-#{type}")
21
+ content_type = "application/x-#{type}" if content_type.match(/\(.*?\)/)
22
+ content_type
23
+ end
24
+ end
25
+
26
+ # Returns the file's normal name.
27
+ def original_filename
28
+ File.basename(self.path)
29
+ end
30
+
31
+ # Returns the size of the file.
32
+ def size
33
+ File.size(self)
34
+ end
35
+ end
36
+ end
37
+
38
+ if defined? StringIO
39
+ class StringIO
40
+ attr_accessor :original_filename, :content_type
41
+ def original_filename
42
+ @original_filename ||= "stringio.txt"
43
+ end
44
+ def content_type
45
+ @content_type ||= "text/plain"
46
+ end
47
+ end
48
+ end
49
+
50
+ class File #:nodoc:
51
+ include Paperclip::Upfile
52
+ end
@@ -0,0 +1,3 @@
1
+ module Paperclip
2
+ VERSION = "2.3.3" unless defined? Paperclip::VERSION
3
+ end
@@ -0,0 +1,95 @@
1
+ def obtain_class
2
+ class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
4
+ @klass = Object.const_get(class_name)
5
+ end
6
+
7
+ def obtain_attachments
8
+ name = ENV['ATTACHMENT'] || ENV['attachment']
9
+ raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
10
+ @attachments = if !name.blank? && @klass.attachment_definitions.keys.include?(name)
11
+ [ name ]
12
+ else
13
+ @klass.attachment_definitions.keys
14
+ end
15
+ end
16
+
17
+ def obtain_styles(instance)
18
+ style_names = (ENV['STYLES'] || ENV['styles']).to_s.split(/\s*,\s*/).map {|s| s.to_sym }
19
+ @attachments.inject({}) do |memo, attachment_name|
20
+ attachment = instance.attachment_for(attachment_name)
21
+ if style_names.empty?
22
+ memo[attachment_name] = attachment.styles.keys
23
+ else
24
+ style_names.each do |style_name|
25
+ raise "Unknown %s style: %s" % [attachment_name, style_name] unless attachment.styles[style_name]
26
+ end
27
+ memo[attachment_name] = style_names
28
+ end
29
+ memo
30
+ end
31
+ end
32
+
33
+ def for_all_attachments
34
+ klass = obtain_class
35
+ names = obtain_attachments
36
+
37
+ klass.all.each do |instance|
38
+ styles = obtain_styles(instance)
39
+ styles.each do |attachment_name, style_names|
40
+ result = if instance.send("#{ attachment_name }?")
41
+ yield(instance, attachment_name, style_names)
42
+ else
43
+ true
44
+ end
45
+ print result ? "." : "x"; $stdout.flush
46
+ end
47
+ end
48
+
49
+ puts " Done."
50
+ end
51
+
52
+ namespace :paperclip do
53
+ desc "Refreshes both metadata and thumbnails."
54
+ task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
55
+
56
+ namespace :refresh do
57
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT and comma-separated STYLES)."
58
+ task :thumbnails => :environment do
59
+ errors = []
60
+ for_all_attachments do |instance, name, styles|
61
+ result = instance.send(name).reprocess!(styles)
62
+ errors << [instance.id, instance.errors] unless instance.errors.blank?
63
+ result
64
+ end
65
+ errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
66
+ end
67
+
68
+ desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
69
+ task :metadata => :environment do
70
+ for_all_attachments do |instance, name, styles|
71
+ if file = instance.send(name).to_file
72
+ instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
73
+ instance.send("#{name}_content_type=", file.content_type.strip)
74
+ instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
75
+ instance.save(false)
76
+ else
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ desc "Cleans out invalid attachments. Useful after you've added new validations."
84
+ task :clean => :environment do
85
+ for_all_attachments do |instance, name, styles|
86
+ instance.send(name).send(:validate)
87
+ if instance.send(name).valid?
88
+ true
89
+ else
90
+ instance.send("#{name}=", nil)
91
+ instance.save
92
+ end
93
+ end
94
+ end
95
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'paperclip/railtie'
2
+ Paperclip::Railtie.insert
@@ -0,0 +1,119 @@
1
+ require 'paperclip/matchers'
2
+ require 'action_controller'
3
+
4
+ module Paperclip
5
+ # =Paperclip Shoulda Macros
6
+ #
7
+ # These macros are intended for use with shoulda, and will be included into
8
+ # your tests automatically. All of the macros use the standard shoulda
9
+ # assumption that the name of the test is based on the name of the model
10
+ # you're testing (that is, UserTest is the test for the User model), and
11
+ # will load that class for testing purposes.
12
+ module Shoulda
13
+ include Matchers
14
+ # This will test whether you have defined your attachment correctly by
15
+ # checking for all the required fields exist after the definition of the
16
+ # attachment.
17
+ def should_have_attached_file name
18
+ klass = self.name.gsub(/Test$/, '').constantize
19
+ matcher = have_attached_file name
20
+ should matcher.description do
21
+ assert_accepts(matcher, klass)
22
+ end
23
+ end
24
+
25
+ # Tests for validations on the presence of the attachment.
26
+ def should_validate_attachment_presence name
27
+ klass = self.name.gsub(/Test$/, '').constantize
28
+ matcher = validate_attachment_presence name
29
+ should matcher.description do
30
+ assert_accepts(matcher, klass)
31
+ end
32
+ end
33
+
34
+ # Tests that you have content_type validations specified. There are two
35
+ # options, :valid and :invalid. Both accept an array of strings. The
36
+ # strings should be a list of content types which will pass and fail
37
+ # validation, respectively.
38
+ def should_validate_attachment_content_type name, options = {}
39
+ klass = self.name.gsub(/Test$/, '').constantize
40
+ valid = [options[:valid]].flatten
41
+ invalid = [options[:invalid]].flatten
42
+ matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
43
+ should matcher.description do
44
+ assert_accepts(matcher, klass)
45
+ end
46
+ end
47
+
48
+ # Tests to ensure that you have file size validations turned on. You
49
+ # can pass the same options to this that you can to
50
+ # validate_attachment_file_size - :less_than, :greater_than, and :in.
51
+ # :less_than checks that a file is less than a certain size, :greater_than
52
+ # checks that a file is more than a certain size, and :in takes a Range or
53
+ # Array which specifies the lower and upper limits of the file size.
54
+ def should_validate_attachment_size name, options = {}
55
+ klass = self.name.gsub(/Test$/, '').constantize
56
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
57
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
58
+ range = (min..max)
59
+ matcher = validate_attachment_size(name).in(range)
60
+ should matcher.description do
61
+ assert_accepts(matcher, klass)
62
+ end
63
+ end
64
+
65
+ # Stubs the HTTP PUT for an attachment using S3 storage.
66
+ #
67
+ # @example
68
+ # stub_paperclip_s3('user', 'avatar', 'png')
69
+ def stub_paperclip_s3(model, attachment, extension)
70
+ definition = model.gsub(" ", "_").classify.constantize.
71
+ attachment_definitions[attachment.to_sym]
72
+
73
+ path = "http://s3.amazonaws.com/:id/#{definition[:path]}"
74
+ path.gsub!(/:([^\/\.]+)/) do |match|
75
+ "([^\/\.]+)"
76
+ end
77
+
78
+ begin
79
+ FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
80
+ rescue NameError
81
+ raise NameError, "the stub_paperclip_s3 shoulda macro requires the fakeweb gem."
82
+ end
83
+ end
84
+
85
+ # Stub S3 and return a file for attachment. Best with Factory Girl.
86
+ # Uses a strict directory convention:
87
+ #
88
+ # features/support/paperclip
89
+ #
90
+ # This method is used by the Paperclip-provided Cucumber step:
91
+ #
92
+ # When I attach a "demo_tape" "mp3" file to a "band" on S3
93
+ #
94
+ # @example
95
+ # Factory.define :band_with_demo_tape, :parent => :band do |band|
96
+ # band.demo_tape { band.paperclip_fixture("band", "demo_tape", "png") }
97
+ # end
98
+ def paperclip_fixture(model, attachment, extension)
99
+ stub_paperclip_s3(model, attachment, extension)
100
+ base_path = File.join(File.dirname(__FILE__), "..", "..",
101
+ "features", "support", "paperclip")
102
+ File.new(File.join(base_path, model, "#{attachment}.#{extension}"))
103
+ end
104
+ end
105
+ end
106
+
107
+ if defined?(ActionController::Integration::Session)
108
+ class ActionController::Integration::Session #:nodoc:
109
+ include Paperclip::Shoulda
110
+ end
111
+ end
112
+
113
+ class Factory
114
+ include Paperclip::Shoulda #:nodoc:
115
+ end
116
+
117
+ class Test::Unit::TestCase #:nodoc:
118
+ extend Paperclip::Shoulda
119
+ end