rsturim-paperclip 2.2.9.2

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 (47) hide show
  1. data/LICENSE +26 -0
  2. data/README.rdoc +174 -0
  3. data/Rakefile +99 -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/paperclip.rb +350 -0
  9. data/lib/paperclip/attachment.rb +413 -0
  10. data/lib/paperclip/callback_compatability.rb +33 -0
  11. data/lib/paperclip/geometry.rb +115 -0
  12. data/lib/paperclip/interpolations.rb +105 -0
  13. data/lib/paperclip/iostream.rb +58 -0
  14. data/lib/paperclip/matchers.rb +4 -0
  15. data/lib/paperclip/matchers/have_attached_file_matcher.rb +49 -0
  16. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +66 -0
  17. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +48 -0
  18. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +83 -0
  19. data/lib/paperclip/processor.rb +49 -0
  20. data/lib/paperclip/storage.rb +241 -0
  21. data/lib/paperclip/thumbnail.rb +70 -0
  22. data/lib/paperclip/upfile.rb +48 -0
  23. data/shoulda_macros/paperclip.rb +68 -0
  24. data/tasks/paperclip_tasks.rake +79 -0
  25. data/test/attachment_test.rb +768 -0
  26. data/test/database.yml +4 -0
  27. data/test/fixtures/12k.png +0 -0
  28. data/test/fixtures/50x50.png +0 -0
  29. data/test/fixtures/5k.png +0 -0
  30. data/test/fixtures/bad.png +1 -0
  31. data/test/fixtures/s3.yml +4 -0
  32. data/test/fixtures/text.txt +0 -0
  33. data/test/fixtures/twopage.pdf +0 -0
  34. data/test/geometry_test.rb +177 -0
  35. data/test/helper.rb +100 -0
  36. data/test/integration_test.rb +481 -0
  37. data/test/interpolations_test.rb +120 -0
  38. data/test/iostream_test.rb +71 -0
  39. data/test/matchers/have_attached_file_matcher_test.rb +21 -0
  40. data/test/matchers/validate_attachment_content_type_matcher_test.rb +30 -0
  41. data/test/matchers/validate_attachment_presence_matcher_test.rb +21 -0
  42. data/test/matchers/validate_attachment_size_matcher_test.rb +50 -0
  43. data/test/paperclip_test.rb +291 -0
  44. data/test/processor_test.rb +10 -0
  45. data/test/storage_test.rb +293 -0
  46. data/test/thumbnail_test.rb +177 -0
  47. metadata +124 -0
@@ -0,0 +1,241 @@
1
+ module Paperclip
2
+ module Storage
3
+
4
+ # The default place to store attachments is in the filesystem. Files on the local
5
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
6
+ # They also can be processed more easily after they've been saved, as they're just
7
+ # normal files. There is one Filesystem-specific option for has_attached_file.
8
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
9
+ # almost all cases, should) be coordinated with the value of the +url+ option to
10
+ # allow files to be saved into a place where Apache can serve them without
11
+ # hitting your app. Defaults to
12
+ # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
+ # public directory.
17
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
+ module Filesystem
20
+ def self.extended base
21
+ end
22
+
23
+ def exists?(style = default_style)
24
+ if original_filename
25
+ File.exist?(path(style))
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ # Returns representation of the data of the file assigned to the given
32
+ # style, in the format most representative of the current storage.
33
+ def to_file style = default_style
34
+ @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
35
+ end
36
+ alias_method :to_io, :to_file
37
+
38
+ def flush_writes #:nodoc:
39
+ @queued_for_write.each do |style, file|
40
+ file.close
41
+ FileUtils.mkdir_p(File.dirname(path(style)))
42
+ log("saving #{path(style)}")
43
+ FileUtils.mv(file.path, path(style))
44
+ FileUtils.chmod(0644, path(style))
45
+ end
46
+ @queued_for_write = {}
47
+ end
48
+
49
+ def flush_deletes #:nodoc:
50
+ @queued_for_delete.each do |path|
51
+ begin
52
+ log("deleting #{path}")
53
+ FileUtils.rm(path) if File.exist?(path)
54
+ rescue Errno::ENOENT => e
55
+ # ignore file-not-found, let everything else pass
56
+ end
57
+ begin
58
+ while(true)
59
+ path = File.dirname(path)
60
+ FileUtils.rmdir(path)
61
+ end
62
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
63
+ # Stop trying to remove parent directories
64
+ rescue SystemCallError => e
65
+ log("There was an unexpected error while deleting directories: #{e.class}")
66
+ # Ignore it
67
+ end
68
+ end
69
+ @queued_for_delete = []
70
+ end
71
+ end
72
+
73
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
74
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
75
+ # There are a few S3-specific options for has_attached_file:
76
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
77
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
78
+ # gives you. You can 'environment-space' this just like you do to your
79
+ # database.yml file, so different environments can use different accounts:
80
+ # development:
81
+ # access_key_id: 123...
82
+ # secret_access_key: 123...
83
+ # test:
84
+ # access_key_id: abc...
85
+ # secret_access_key: abc...
86
+ # production:
87
+ # access_key_id: 456...
88
+ # secret_access_key: 456...
89
+ # This is not required, however, and the file may simply look like this:
90
+ # access_key_id: 456...
91
+ # secret_access_key: 456...
92
+ # In which case, those access keys will be used in all environments. You can also
93
+ # put your bucket name in this file, instead of adding it to the code directly.
94
+ # This is useful when you want the same account but a different bucket for
95
+ # development versus production.
96
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
97
+ # policies that S3 provides (more information can be found here:
98
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
99
+ # The default for Paperclip is "public-read".
100
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
101
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
102
+ # default), and 'https' when your :s3_permissions are anything else.
103
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
104
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
105
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
106
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
107
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
108
+ # Paperclip will call that Proc with attachment as the only argument.
109
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
110
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
111
+ # link in the +url+ entry for more information about S3 domains and buckets.
112
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
113
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
114
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
115
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
116
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
117
+ # Normally, this won't matter in the slightest and you can leave the default (which is
118
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
119
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
120
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
121
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
122
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
123
+ # by S3.
124
+ # * +path+: This is the key under the bucket in which the file will be stored. The
125
+ # URL will be constructed from the bucket and the path. This is what you will want
126
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
127
+ # S3 (strictly speaking) does not support directories, you can still use a / to
128
+ # separate parts of your file name.
129
+ module S3
130
+ def self.extended base
131
+ warn('[DEPRECATION] S3 support through RightAWS is deprecated. S3 support will ' +
132
+ 'be changed to AWS::S3 in a future version.')
133
+ require 'right_aws'
134
+ base.instance_eval do
135
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
136
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
137
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
138
+ @s3_options = @options[:s3_options] || {}
139
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
140
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
141
+ @s3_headers = @options[:s3_headers] || {}
142
+ @s3_host_alias = @options[:s3_host_alias]
143
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
144
+ end
145
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
146
+ "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
147
+ end
148
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
149
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
150
+ end
151
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
152
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
153
+ end
154
+ end
155
+
156
+ def s3
157
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
158
+ @s3_credentials[:secret_access_key],
159
+ @s3_options)
160
+ end
161
+
162
+ def s3_bucket
163
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
164
+ end
165
+
166
+ def bucket_name
167
+ @bucket
168
+ end
169
+
170
+ def s3_host_alias
171
+ @s3_host_alias
172
+ end
173
+
174
+ def parse_credentials creds
175
+ creds = find_credentials(creds).stringify_keys
176
+ (creds[RAILS_ENV] || creds).symbolize_keys
177
+ end
178
+
179
+ def exists?(style = default_style)
180
+ s3_bucket.key(path(style)) ? true : false
181
+ end
182
+
183
+ def s3_protocol
184
+ @s3_protocol
185
+ end
186
+
187
+ # Returns representation of the data of the file assigned to the given
188
+ # style, in the format most representative of the current storage.
189
+ def to_file style = default_style
190
+ tmp_file = Tempfile.new("s3_data")
191
+ tmp_file.write(s3_bucket.key(path(style)).data)
192
+ tmp_file.rewind
193
+ @queued_for_write[style] || tmp_file
194
+ end
195
+ alias_method :to_io, :to_file
196
+
197
+ def flush_writes #:nodoc:
198
+ @queued_for_write.each do |style, file|
199
+ begin
200
+ log("saving #{path(style)}")
201
+ key = s3_bucket.key(path(style))
202
+ key.data = file
203
+ key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
204
+ rescue RightAws::AwsError => e
205
+ raise
206
+ end
207
+ end
208
+ @queued_for_write = {}
209
+ end
210
+
211
+ def flush_deletes #:nodoc:
212
+ @queued_for_delete.each do |path|
213
+ begin
214
+ log("deleting #{path}")
215
+ if file = s3_bucket.key(path)
216
+ file.delete
217
+ end
218
+ rescue RightAws::AwsError
219
+ # Ignore this.
220
+ end
221
+ end
222
+ @queued_for_delete = []
223
+ end
224
+
225
+ def find_credentials creds
226
+ case creds
227
+ when File
228
+ YAML.load_file(creds.path)
229
+ when String
230
+ YAML.load_file(creds)
231
+ when Hash
232
+ creds
233
+ else
234
+ raise ArgumentError, "Credentials are not a path, file, or hash."
235
+ end
236
+ end
237
+ private :find_credentials
238
+
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,70 @@
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
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
+ geometry = options[:geometry]
16
+ @file = file
17
+ @crop = geometry[-1,1] == '#'
18
+ @target_geometry = Geometry.parse geometry
19
+ @current_geometry = Geometry.from_file @file
20
+ @convert_options = options[:convert_options]
21
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
22
+ @format = options[:format]
23
+
24
+ @current_format = File.extname(@file.path)
25
+ @basename = File.basename(@file.path, @current_format)
26
+ end
27
+
28
+ # Returns true if the +target_geometry+ is meant to crop.
29
+ def crop?
30
+ @crop
31
+ end
32
+
33
+ # Returns true if the image is meant to make use of additional convert options.
34
+ def convert_options?
35
+ not @convert_options.blank?
36
+ end
37
+
38
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
39
+ # that contains the new image.
40
+ def make
41
+ src = @file
42
+ dst = Tempfile.new([@basename, @format].compact.join("."))
43
+ dst.binmode
44
+
45
+ command = <<-end_command
46
+ "#{ File.expand_path(src.path) }[0]"
47
+ #{ transformation_command }
48
+ "#{ File.expand_path(dst.path) }"
49
+ end_command
50
+
51
+ begin
52
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
53
+ rescue PaperclipCommandLineError
54
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
55
+ end
56
+
57
+ dst
58
+ end
59
+
60
+ # Returns the command ImageMagick's +convert+ needs to transform the image
61
+ # into the thumbnail.
62
+ def transformation_command
63
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
64
+ trans = "-resize \"#{scale}\""
65
+ trans << " -crop \"#{crop}\" +repage" if crop
66
+ trans << " #{convert_options}" if convert_options?
67
+ trans
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
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"jpe?g" 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 "csv", "xml", "css", "js" then "text/#{type}"
17
+ else "application/x-#{type}"
18
+ end
19
+ end
20
+
21
+ # Returns the file's normal name.
22
+ def original_filename
23
+ File.basename(self.path)
24
+ end
25
+
26
+ # Returns the size of the file.
27
+ def size
28
+ File.size(self)
29
+ end
30
+ end
31
+ end
32
+
33
+ if defined? StringIO
34
+ class StringIO
35
+ attr_accessor :original_filename, :content_type
36
+ def original_filename
37
+ @original_filename ||= "stringio.txt"
38
+ end
39
+ def content_type
40
+ @content_type ||= "text/plain"
41
+ end
42
+ end
43
+ end
44
+
45
+ class File #:nodoc:
46
+ include Paperclip::Upfile
47
+ end
48
+
@@ -0,0 +1,68 @@
1
+ require 'paperclip/matchers'
2
+
3
+ module Paperclip
4
+ # =Paperclip Shoulda Macros
5
+ #
6
+ # These macros are intended for use with shoulda, and will be included into
7
+ # your tests automatically. All of the macros use the standard shoulda
8
+ # assumption that the name of the test is based on the name of the model
9
+ # you're testing (that is, UserTest is the test for the User model), and
10
+ # will load that class for testing purposes.
11
+ module Shoulda
12
+ include Matchers
13
+ # This will test whether you have defined your attachment correctly by
14
+ # checking for all the required fields exist after the definition of the
15
+ # attachment.
16
+ def should_have_attached_file name
17
+ klass = self.name.gsub(/Test$/, '').constantize
18
+ matcher = have_attached_file name
19
+ should matcher.description do
20
+ assert_accepts(matcher, klass)
21
+ end
22
+ end
23
+
24
+ # Tests for validations on the presence of the attachment.
25
+ def should_validate_attachment_presence name
26
+ klass = self.name.gsub(/Test$/, '').constantize
27
+ matcher = validate_attachment_presence name
28
+ should matcher.description do
29
+ assert_accepts(matcher, klass)
30
+ end
31
+ end
32
+
33
+ # Tests that you have content_type validations specified. There are two
34
+ # options, :valid and :invalid. Both accept an array of strings. The
35
+ # strings should be a list of content types which will pass and fail
36
+ # validation, respectively.
37
+ def should_validate_attachment_content_type name, options = {}
38
+ klass = self.name.gsub(/Test$/, '').constantize
39
+ valid = [options[:valid]].flatten
40
+ invalid = [options[:invalid]].flatten
41
+ matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
42
+ should matcher.description do
43
+ assert_accepts(matcher, klass)
44
+ end
45
+ end
46
+
47
+ # Tests to ensure that you have file size validations turned on. You
48
+ # can pass the same options to this that you can to
49
+ # validate_attachment_file_size - :less_than, :greater_than, and :in.
50
+ # :less_than checks that a file is less than a certain size, :greater_than
51
+ # checks that a file is more than a certain size, and :in takes a Range or
52
+ # Array which specifies the lower and upper limits of the file size.
53
+ def should_validate_attachment_size name, options = {}
54
+ klass = self.name.gsub(/Test$/, '').constantize
55
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
56
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
57
+ range = (min..max)
58
+ matcher = validate_attachment_size(name).in(range)
59
+ should matcher.description do
60
+ assert_accepts(matcher, klass)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ class Test::Unit::TestCase #:nodoc:
67
+ extend Paperclip::Shoulda
68
+ end
@@ -0,0 +1,79 @@
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
+ 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 for_all_attachments
18
+ klass = obtain_class
19
+ names = obtain_attachments
20
+ ids = klass.connection.select_values(klass.send(:construct_finder_sql, :select => 'id'))
21
+
22
+ ids.each do |id|
23
+ instance = klass.find(id)
24
+ names.each do |name|
25
+ result = if instance.send("#{ name }?")
26
+ yield(instance, name)
27
+ else
28
+ true
29
+ end
30
+ print result ? "." : "x"; $stdout.flush
31
+ end
32
+ end
33
+ puts " Done."
34
+ end
35
+
36
+ namespace :paperclip do
37
+ desc "Refreshes both metadata and thumbnails."
38
+ task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
39
+
40
+ namespace :refresh do
41
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
42
+ task :thumbnails => :environment do
43
+ errors = []
44
+ for_all_attachments do |instance, name|
45
+ result = instance.send(name).reprocess!
46
+ errors << [instance.id, instance.errors] unless instance.errors.blank?
47
+ result
48
+ end
49
+ errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
50
+ end
51
+
52
+ desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
53
+ task :metadata => :environment do
54
+ for_all_attachments do |instance, name|
55
+ if file = instance.send(name).to_file
56
+ instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
57
+ instance.send("#{name}_content_type=", file.content_type.strip)
58
+ instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
59
+ instance.save(false)
60
+ else
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ desc "Cleans out invalid attachments. Useful after you've added new validations."
68
+ task :clean => :environment do
69
+ for_all_attachments do |instance, name|
70
+ instance.send(name).send(:validate)
71
+ if instance.send(name).valid?
72
+ true
73
+ else
74
+ instance.send("#{name}=", nil)
75
+ instance.save
76
+ end
77
+ end
78
+ end
79
+ end