paperclip 2.1.2 → 2.1.5

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.

@@ -1,15 +1,13 @@
1
1
  module Paperclip
2
-
2
+
3
3
  # Defines the geometry of an image.
4
4
  class Geometry
5
5
  attr_accessor :height, :width, :modifier
6
6
 
7
7
  # Gives a Geometry representing the given height and width
8
8
  def initialize width = nil, height = nil, modifier = nil
9
- height = nil if height == ""
10
- width = nil if width == ""
11
- @height = (height || width).to_f
12
- @width = (width || height).to_f
9
+ @height = height.to_f
10
+ @width = width.to_f
13
11
  @modifier = modifier
14
12
  end
15
13
 
@@ -17,13 +15,18 @@ module Paperclip
17
15
  # File or path.
18
16
  def self.from_file file
19
17
  file = file.path if file.respond_to? "path"
20
- parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
18
+ geometry = begin
19
+ Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"])
20
+ rescue PaperclipCommandLineError
21
+ ""
22
+ end
23
+ parse(geometry) ||
21
24
  raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
25
  end
23
26
 
24
27
  # Parses a "WxH" formatted string, where W is the width and H is the height.
25
28
  def self.parse string
26
- if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
29
+ if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/))
27
30
  Geometry.new(*match[1,3])
28
31
  end
29
32
  end
@@ -60,7 +63,11 @@ module Paperclip
60
63
 
61
64
  # Returns the width and height in a format suitable to be passed to Geometry.parse
62
65
  def to_s
63
- "%dx%d%s" % [width, height, modifier]
66
+ s = ""
67
+ s << width.to_i.to_s if width > 0
68
+ s << "x#{height.to_i}" if height > 0
69
+ s << modifier.to_s
70
+ s
64
71
  end
65
72
 
66
73
  # Same as to_s
@@ -76,15 +83,14 @@ module Paperclip
76
83
  # overhanging image would be cropped. Useful for square thumbnail images. The cropping
77
84
  # is weighted at the center of the Geometry.
78
85
  def transformation_to dst, crop = false
79
- ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
80
-
81
86
  if crop
87
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
82
88
  scale_geometry, scale = scaling(dst, ratio)
83
89
  crop_geometry = cropping(dst, ratio, scale)
84
90
  else
85
91
  scale_geometry = dst.to_s
86
92
  end
87
-
93
+
88
94
  [ scale_geometry, crop_geometry ]
89
95
  end
90
96
 
@@ -11,7 +11,7 @@ module IOStream
11
11
 
12
12
  # Copies one read-able object from one place to another in blocks, obviating the need to load
13
13
  # the whole thing into memory. Defaults to 8k blocks. If this module is included in both
14
- # both StringIO and Tempfile, then either can have its data copied anywhere else without typing
14
+ # StringIO and Tempfile, then either can have its data copied anywhere else without typing
15
15
  # worries or memory overhead worries. Returns a File if a String is passed in as the destination
16
16
  # and returns the IO or Tempfile as passed in if one is sent as the destination.
17
17
  def stream_to path_or_file, in_blocks_of = 8192
@@ -41,3 +41,18 @@ end
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ # Corrects a bug in Windows when asking for Tempfile size.
46
+ if defined? Tempfile
47
+ class Tempfile
48
+ def size
49
+ if @tmpfile
50
+ @tmpfile.fsync
51
+ @tmpfile.flush
52
+ @tmpfile.stat.size
53
+ else
54
+ 0
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,6 +1,21 @@
1
1
  module Paperclip
2
2
  module Storage
3
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"
4
19
  module Filesystem
5
20
  def self.extended base
6
21
  end
@@ -16,23 +31,27 @@ module Paperclip
16
31
  # Returns representation of the data of the file assigned to the given
17
32
  # style, in the format most representative of the current storage.
18
33
  def to_file style = default_style
19
- @queued_for_write[style] || (File.new(path(style)) if exists?(style))
34
+ @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
20
35
  end
21
36
  alias_method :to_io, :to_file
22
37
 
23
38
  def flush_writes #:nodoc:
39
+ logger.info("[paperclip] Writing files for #{name}")
24
40
  @queued_for_write.each do |style, file|
25
41
  FileUtils.mkdir_p(File.dirname(path(style)))
26
- result = file.stream_to(path(style))
42
+ logger.info("[paperclip] -> #{path(style)}")
43
+ FileUtils.mv(file.path, path(style))
44
+ FileUtils.chmod(0644, path(style))
27
45
  file.close
28
- result.close
29
46
  end
30
47
  @queued_for_write = {}
31
48
  end
32
49
 
33
50
  def flush_deletes #:nodoc:
51
+ logger.info("[paperclip] Deleting files for #{name}")
34
52
  @queued_for_delete.each do |path|
35
53
  begin
54
+ logger.info("[paperclip] -> #{path}")
36
55
  FileUtils.rm(path) if File.exist?(path)
37
56
  rescue Errno::ENOENT => e
38
57
  # ignore file-not-found, let everything else pass
@@ -42,19 +61,67 @@ module Paperclip
42
61
  end
43
62
  end
44
63
 
64
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
65
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
66
+ # There are a few S3-specific options for has_attached_file:
67
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
68
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
69
+ # gives you. You can 'environment-space' this just like you do to your
70
+ # database.yml file, so different environments can use different accounts:
71
+ # development:
72
+ # access_key_id: 123...
73
+ # secret_access_key: 123...
74
+ # test:
75
+ # access_key_id: abc...
76
+ # secret_access_key: abc...
77
+ # production:
78
+ # access_key_id: 456...
79
+ # secret_access_key: 456...
80
+ # This is not required, however, and the file may simply look like this:
81
+ # access_key_id: 456...
82
+ # secret_access_key: 456...
83
+ # In which case, those access keys will be used in all environments. You can also
84
+ # put your bucket name in this file, instead of adding it to the code directly.
85
+ # This is useful when you want the same account but a different bucket for
86
+ # development versus production.
87
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
88
+ # policies that S3 provides (more information can be found here:
89
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
90
+ # The default for Paperclip is "public-read".
91
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
92
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
93
+ # default), and 'https' when your :s3_permissions are anything else.
94
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
95
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
96
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
97
+ # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
98
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
99
+ # Normally, this won't matter in the slightest and you can leave the default (which is
100
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
101
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
102
+ # * +path+: This is the key under the bucket in which the file will be stored. The
103
+ # URL will be constructed from the bucket and the path. This is what you will want
104
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
105
+ # S3 (strictly speaking) does not support directories, you can still use a / to
106
+ # separate parts of your file name.
45
107
  module S3
46
108
  def self.extended base
47
109
  require 'right_aws'
48
110
  base.instance_eval do
49
- @bucket = @options[:bucket]
50
- @s3_credentials = parse_credentials(@options[:s3_credentials])
51
- @s3_options = @options[:s3_options] || {}
52
- @s3_permissions = @options[:s3_permissions] || 'public-read'
53
- @url = ":s3_url"
111
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
112
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
113
+ @s3_options = @options[:s3_options] || {}
114
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
115
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
116
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
54
117
  end
55
- base.class.interpolations[:s3_url] = lambda do |attachment, style|
56
- "https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
118
+ base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
119
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
57
120
  end
121
+ base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
122
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
123
+ end
124
+ ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
58
125
  end
59
126
 
60
127
  def s3
@@ -80,6 +147,10 @@ module Paperclip
80
147
  s3_bucket.key(path(style)) ? true : false
81
148
  end
82
149
 
150
+ def s3_protocol
151
+ @s3_protocol
152
+ end
153
+
83
154
  # Returns representation of the data of the file assigned to the given
84
155
  # style, in the format most representative of the current storage.
85
156
  def to_file style = default_style
@@ -88,11 +159,13 @@ module Paperclip
88
159
  alias_method :to_io, :to_file
89
160
 
90
161
  def flush_writes #:nodoc:
162
+ logger.info("[paperclip] Writing files for #{name}")
91
163
  @queued_for_write.each do |style, file|
92
164
  begin
165
+ logger.info("[paperclip] -> #{path(style)}")
93
166
  key = s3_bucket.key(path(style))
94
167
  key.data = file
95
- key.put(nil, @s3_permissions)
168
+ key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)})
96
169
  rescue RightAws::AwsError => e
97
170
  raise
98
171
  end
@@ -101,8 +174,10 @@ module Paperclip
101
174
  end
102
175
 
103
176
  def flush_deletes #:nodoc:
177
+ logger.info("[paperclip] Writing files for #{name}")
104
178
  @queued_for_delete.each do |path|
105
179
  begin
180
+ logger.info("[paperclip] -> #{path}")
106
181
  if file = s3_bucket.key(path)
107
182
  file.delete
108
183
  end
@@ -2,18 +2,20 @@ module Paperclip
2
2
  # Handles thumbnailing images that are uploaded.
3
3
  class Thumbnail
4
4
 
5
- attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails
5
+ attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails, :convert_options
6
6
 
7
7
  # Creates a Thumbnail object set to work on the +file+ given. It
8
8
  # will attempt to transform the image into one defined by +target_geometry+
9
9
  # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
10
  # unless specified. Thumbnail creation will raise no errors unless
11
- # +whiny_thumbnails+ is true (which it is, by default.
12
- def initialize file, target_geometry, format = nil, whiny_thumbnails = true
11
+ # +whiny_thumbnails+ 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, target_geometry, format = nil, convert_options = nil, whiny_thumbnails = true
13
14
  @file = file
14
15
  @crop = target_geometry[-1,1] == '#'
15
16
  @target_geometry = Geometry.parse target_geometry
16
17
  @current_geometry = Geometry.from_file file
18
+ @convert_options = convert_options
17
19
  @whiny_thumbnails = whiny_thumbnails
18
20
 
19
21
  @current_format = File.extname(@file.path)
@@ -24,14 +26,19 @@ module Paperclip
24
26
 
25
27
  # Creates a thumbnail, as specified in +initialize+, +make+s it, and returns the
26
28
  # resulting Tempfile.
27
- def self.make file, dimensions, format = nil, whiny_thumbnails = true
28
- new(file, dimensions, format, whiny_thumbnails).make
29
+ def self.make file, dimensions, format = nil, convert_options = nil, whiny_thumbnails = true
30
+ new(file, dimensions, format, convert_options, whiny_thumbnails).make
29
31
  end
30
32
 
31
33
  # Returns true if the +target_geometry+ is meant to crop.
32
34
  def crop?
33
35
  @crop
34
36
  end
37
+
38
+ # Returns true if the image is meant to make use of additional convert options.
39
+ def convert_options?
40
+ not @convert_options.blank?
41
+ end
35
42
 
36
43
  # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
37
44
  # that contains the new image.
@@ -41,15 +48,15 @@ module Paperclip
41
48
  dst.binmode
42
49
 
43
50
  command = <<-end_command
44
- #{ Paperclip.path_for_command('convert') }
45
- "#{ File.expand_path(src.path) }"
51
+ "#{ File.expand_path(src.path) }[0]"
46
52
  #{ transformation_command }
47
53
  "#{ File.expand_path(dst.path) }"
48
54
  end_command
49
- success = system(command.gsub(/\s+/, " "))
50
55
 
51
- if success && $?.exitstatus != 0 && @whiny_thumbnails
52
- raise PaperclipError, "There was an error processing this thumbnail"
56
+ begin
57
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
58
+ rescue PaperclipCommandLineError
59
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny_thumbnails
53
60
  end
54
61
 
55
62
  dst
@@ -59,8 +66,9 @@ module Paperclip
59
66
  # into the thumbnail.
60
67
  def transformation_command
61
68
  scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
62
- trans = "-scale \"#{scale}\""
69
+ trans = "-resize \"#{scale}\""
63
70
  trans << " -crop \"#{crop}\" +repage" if crop
71
+ trans << " #{convert_options}" if convert_options?
64
72
  trans
65
73
  end
66
74
  end
@@ -6,12 +6,15 @@ module Paperclip
6
6
 
7
7
  # Infer the MIME-type of the file from the extension.
8
8
  def content_type
9
- type = self.path.match(/\.(\w+)$/)[1] rescue "octet-stream"
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
10
  case type
11
- when "jpg", "png", "gif" then "image/#{type}"
12
- when "txt" then "text/plain"
13
- when "csv", "xml", "html", "htm", "css", "js" then "text/#{type}"
14
- else "x-application/#{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}"
15
18
  end
16
19
  end
17
20
 
@@ -25,9 +28,21 @@ module Paperclip
25
28
  File.size(self)
26
29
  end
27
30
  end
31
+ end
28
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
29
43
  end
30
44
 
31
45
  class File #:nodoc:
32
46
  include Paperclip::Upfile
33
47
  end
48
+
@@ -0,0 +1,32 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ def should_have_attached_file name, options = {}
4
+ klass = self.name.gsub(/Test$/, '').constantize
5
+ context "Class #{klass.name} with attachment #{name}" do
6
+ should "respond to all the right methods" do
7
+ ["#{name}", "#{name}=", "#{name}?"].each do |meth|
8
+ assert klass.instance_methods.include?(meth), "#{klass.name} does not respond to #{name}."
9
+ end
10
+ end
11
+
12
+ should "have the correct definition" do
13
+ expected = options
14
+ actual = klass.attachment_definitions[name]
15
+ expected.delete(:validations) if not options.key?(:validations)
16
+ expected.delete(:whiny_thumbnails) if not options.key?(:whiny_thumbnails)
17
+
18
+ assert_equal expected, actual
19
+ end
20
+
21
+ should "ensure that ImageMagick is available" do
22
+ %w( convert identify ).each do |command|
23
+ `#{Paperclip.path_for_command(command)}`
24
+ assert_equal 0, $?, "ImageMagick's #{command} returned with an error. Make sure that #{command} is available at #{Paperclip.path_for_command(command)}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ Test::Unit::TestCase.extend(Paperclip::Shoulda)
@@ -14,25 +14,66 @@ def obtain_attachments
14
14
  end
15
15
  end
16
16
 
17
+ def for_all_attachments
18
+ klass = obtain_class
19
+ names = obtain_attachments
20
+ ids = klass.connection.select_values("SELECT #{klass.primary_key} FROM #{klass.table_name}")
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
+
17
36
  namespace :paperclip do
18
- desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)"
19
- task :refresh => :environment do
20
- klass = obtain_class
21
- names = obtain_attachments
22
- instances = klass.find(:all)
23
-
24
- puts "Regenerating thumbnails for #{instances.length} instances of #{klass.name}:"
25
- instances.each do |instance|
26
- names.each do |name|
27
- result = if instance.send("#{ name }?")
28
- instance.send(name).reprocess!
29
- instance.send(name).save
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)
30
60
  else
31
61
  true
32
62
  end
33
- print result ? "." : "x"; $stdout.flush
34
63
  end
35
64
  end
36
- puts " Done."
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
37
78
  end
38
79
  end