paperclip 2.1.0 → 2.1.2

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.

@@ -36,7 +36,7 @@ require 'paperclip/attachment'
36
36
  # The base module that gets included in ActiveRecord::Base.
37
37
  module Paperclip
38
38
 
39
- VERSION = "2.1.0"
39
+ VERSION = "2.1.2"
40
40
 
41
41
  class << self
42
42
  # Provides configurability to Paperclip. There are a number of options available, such as:
@@ -65,6 +65,9 @@ module Paperclip
65
65
  class PaperclipError < StandardError #:nodoc:
66
66
  end
67
67
 
68
+ class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
69
+ end
70
+
68
71
  module ClassMethods
69
72
  # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
70
73
  # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
@@ -131,7 +134,7 @@ module Paperclip
131
134
  end
132
135
 
133
136
  define_method "#{name}?" do
134
- ! attachment_for(name).file.nil?
137
+ ! attachment_for(name).original_filename.blank?
135
138
  end
136
139
 
137
140
  validates_each(name) do |record, attr, value|
@@ -144,6 +147,7 @@ module Paperclip
144
147
  # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
145
148
  # * +less_than+: equivalent to :in => 0..options[:less_than]
146
149
  # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
150
+ # * +message+: error message to display, use :min and :max as replacements
147
151
  def validates_attachment_size name, options = {}
148
152
  attachment_definitions[name][:validations] << lambda do |attachment, instance|
149
153
  unless options[:greater_than].nil?
@@ -152,17 +156,48 @@ module Paperclip
152
156
  unless options[:less_than].nil?
153
157
  options[:in] = (0..options[:less_than])
154
158
  end
155
- unless options[:in].include? instance[:"#{name}_file_size"].to_i
156
- "file size is not between #{options[:in].first} and #{options[:in].last} bytes."
159
+ unless attachment.original_filename.blank? || options[:in].include?(instance[:"#{name}_file_size"].to_i)
160
+ min = options[:in].first
161
+ max = options[:in].last
162
+
163
+ if options[:message]
164
+ options[:message].gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
165
+ else
166
+ "file size is not between #{min} and #{max} bytes."
167
+ end
157
168
  end
158
169
  end
159
170
  end
160
171
 
172
+ # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
173
+ def validates_attachment_thumbnails name, options = {}
174
+ attachment_definitions[name][:whiny_thumbnails] = true
175
+ end
176
+
161
177
  # Places ActiveRecord-style validations on the presence of a file.
162
- def validates_attachment_presence name
178
+ def validates_attachment_presence name, options = {}
179
+ attachment_definitions[name][:validations] << lambda do |attachment, instance|
180
+ if attachment.original_filename.blank?
181
+ options[:message] || "must be set."
182
+ end
183
+ end
184
+ end
185
+
186
+ # Places ActiveRecord-style validations on the content type of the file assigned. The
187
+ # possible options are:
188
+ # * +content_type+: Allowed content types. Can be a single content type or an array. Allows all by default.
189
+ # * +message+: The message to display when the uploaded file has an invalid content type.
190
+ def validates_attachment_content_type name, options = {}
163
191
  attachment_definitions[name][:validations] << lambda do |attachment, instance|
164
- if attachment.file.nil? || !File.exist?(attachment.file.path)
165
- "must be set."
192
+ valid_types = [options[:content_type]].flatten
193
+
194
+ unless attachment.original_filename.nil?
195
+ unless options[:content_type].blank?
196
+ content_type = instance[:"#{name}_content_type"]
197
+ unless valid_types.any?{|t| t === content_type }
198
+ options[:message] || ActiveRecord::Errors.default_error_messages[:inclusion]
199
+ end
200
+ end
166
201
  end
167
202
  end
168
203
  end
@@ -15,7 +15,7 @@ module Paperclip
15
15
  }
16
16
  end
17
17
 
18
- attr_reader :name, :instance, :file, :styles, :default_style
18
+ attr_reader :name, :instance, :styles, :default_style
19
19
 
20
20
  # Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
21
21
  # ActiveRecord object instance it's attached to, and +options+ is the same as the hash
@@ -33,21 +33,16 @@ module Paperclip
33
33
  @validations = options[:validations]
34
34
  @default_style = options[:default_style]
35
35
  @storage = options[:storage]
36
+ @whiny_thumbnails = options[:whiny_thumbnails]
36
37
  @options = options
37
38
  @queued_for_delete = []
38
- @processed_files = {}
39
+ @queued_for_write = {}
39
40
  @errors = []
40
- @file = nil
41
41
  @validation_errors = nil
42
42
  @dirty = false
43
43
 
44
44
  normalize_style_definition
45
45
  initialize_storage
46
-
47
- if original_filename
48
- @processed_files = locate_files
49
- @file = @processed_files[@default_style]
50
- end
51
46
  end
52
47
 
53
48
  # What gets called when you call instance.attachment = File. It clears errors,
@@ -58,11 +53,11 @@ module Paperclip
58
53
 
59
54
  queue_existing_for_delete
60
55
  @errors = []
61
- @validation_errors = nil
56
+ @validation_errors = nil
62
57
 
63
58
  return nil if uploaded_file.nil?
64
59
 
65
- @file = uploaded_file.to_tempfile
60
+ @queued_for_write[:original] = uploaded_file.to_tempfile
66
61
  @instance[:"#{@name}_file_name"] = uploaded_file.original_filename
67
62
  @instance[:"#{@name}_content_type"] = uploaded_file.content_type
68
63
  @instance[:"#{@name}_file_size"] = uploaded_file.size
@@ -79,8 +74,8 @@ module Paperclip
79
74
  # and can point to an action in your app, if you need fine grained security.
80
75
  # This is not recommended if you don't need the security, however, for
81
76
  # performance reasons.
82
- def url style = nil
83
- @file ? interpolate(@url, style) : interpolate(@default_url, style)
77
+ def url style = default_style
78
+ original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
84
79
  end
85
80
 
86
81
  # Returns the path of the attachment as defined by the :path optionn. If the
@@ -118,7 +113,6 @@ module Paperclip
118
113
  flush_deletes
119
114
  flush_writes
120
115
  @dirty = false
121
- @file = @processed_files[default_style]
122
116
  true
123
117
  else
124
118
  flush_errors
@@ -126,14 +120,6 @@ module Paperclip
126
120
  end
127
121
  end
128
122
 
129
- # Returns representation of the data of the file assigned to the given
130
- # style, in the format most representative of the current storage.
131
- def to_file style = nil
132
- @processed_files[style || default_style]
133
- end
134
-
135
- alias_method :to_io, :to_file
136
-
137
123
  # Returns the name of the file as originally assigned, and as lives in the
138
124
  # <attachment>_file_name attribute of the model.
139
125
  def original_filename
@@ -149,10 +135,10 @@ module Paperclip
149
135
  @interpolations ||= {
150
136
  :rails_root => lambda{|attachment,style| RAILS_ROOT },
151
137
  :class => lambda do |attachment,style|
152
- attachment.instance.class.to_s.downcase.pluralize
138
+ attachment.instance.class.name.underscore.pluralize
153
139
  end,
154
140
  :basename => lambda do |attachment,style|
155
- attachment.original_filename.gsub(/\.(.*?)$/, "")
141
+ attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
156
142
  end,
157
143
  :extension => lambda do |attachment,style|
158
144
  ((style = attachment.styles[style]) && style.last) ||
@@ -167,6 +153,22 @@ module Paperclip
167
153
  }
168
154
  end
169
155
 
156
+ # This method really shouldn't be called that often. It's expected use is in the
157
+ # paperclip:refresh rake task and that's it. It will regenerate all thumbnails
158
+ # forcefully, by reobtaining the original file and going through the post-process
159
+ # again.
160
+ def reprocess!
161
+ new_original = Tempfile.new("paperclip-reprocess")
162
+ old_original = to_file(:original)
163
+ new_original.write( old_original.read )
164
+ new_original.rewind
165
+
166
+ @queued_for_write = { :original => new_original }
167
+ post_process
168
+
169
+ old_original.close if old_original.respond_to?(:close)
170
+ end
171
+
170
172
  private
171
173
 
172
174
  def valid_assignment? file #:nodoc:
@@ -196,38 +198,35 @@ module Paperclip
196
198
  end
197
199
 
198
200
  def post_process #:nodoc:
199
- return nil if @file.nil?
201
+ return if @queued_for_write[:original].nil?
200
202
  @styles.each do |name, args|
201
203
  begin
202
204
  dimensions, format = args
203
- @processed_files[name] = Thumbnail.make(self.file,
204
- dimensions,
205
- format,
206
- @whiny_thumnails)
207
- rescue Errno::ENOENT => e
208
- @errors << "could not be processed because the file does not exist."
205
+ @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
206
+ dimensions,
207
+ format,
208
+ @whiny_thumnails)
209
209
  rescue PaperclipError => e
210
- @errors << e.message
210
+ @errors << e.message if @whiny_thumbnails
211
211
  end
212
212
  end
213
- @processed_files[:original] = @file
214
213
  end
215
214
 
216
- def interpolate pattern, style = nil #:nodoc:
217
- style ||= default_style
218
- pattern = pattern.dup
219
- self.class.interpolations.each do |tag, l|
220
- pattern.gsub!(/:\b#{tag}\b/) do |match|
221
- l.call( self, style )
215
+ def interpolate pattern, style = default_style #:nodoc:
216
+ interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
217
+ interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
218
+ tag, blk = interpolation
219
+ result.gsub(/:#{tag}/) do |match|
220
+ blk.call( self, style )
222
221
  end
223
222
  end
224
- pattern
225
223
  end
226
224
 
227
225
  def queue_existing_for_delete #:nodoc:
228
- @queued_for_delete += @processed_files.values
229
- @file = nil
230
- @processed_files = {}
226
+ return if original_filename.blank?
227
+ @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
228
+ path(style) if exists?(style)
229
+ end.compact
231
230
  @instance[:"#{@name}_file_name"] = nil
232
231
  @instance[:"#{@name}_content_type"] = nil
233
232
  @instance[:"#{@name}_file_size"] = nil
@@ -18,7 +18,7 @@ module Paperclip
18
18
  def self.from_file file
19
19
  file = file.path if file.respond_to? "path"
20
20
  parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
21
- raise(Errno::ENOENT, file)
21
+ raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
22
  end
23
23
 
24
24
  # Parses a "WxH" formatted string, where W is the width and H is the height.
@@ -25,7 +25,7 @@ module IOStream
25
25
  while self.read(in_blocks_of, buffer) do
26
26
  dstio.write(buffer)
27
27
  end
28
- dstio.rewind
28
+ dstio.rewind
29
29
  dstio
30
30
  end
31
31
  end
@@ -1,39 +1,47 @@
1
1
  module Paperclip
2
2
  module Storage
3
3
 
4
- # With the Filesystem module installed, @file and @processed_files are just File instances.
5
4
  module Filesystem
6
5
  def self.extended base
7
6
  end
8
-
9
- def locate_files
10
- [:original, *@styles.keys].uniq.inject({}) do |files, style|
11
- files[style] = File.new(path(style), "rb") if File.exist?(path(style))
12
- files
7
+
8
+ def exists?(style = default_style)
9
+ if original_filename
10
+ File.exist?(path(style))
11
+ else
12
+ false
13
13
  end
14
14
  end
15
15
 
16
+ # Returns representation of the data of the file assigned to the given
17
+ # style, in the format most representative of the current storage.
18
+ def to_file style = default_style
19
+ @queued_for_write[style] || (File.new(path(style)) if exists?(style))
20
+ end
21
+ alias_method :to_io, :to_file
22
+
16
23
  def flush_writes #:nodoc:
17
- @processed_files.each do |style, file|
18
- FileUtils.mkdir_p( File.dirname(path(style)) )
19
- @processed_files[style] = file.stream_to(path(style)) unless file.path == path(style)
24
+ @queued_for_write.each do |style, file|
25
+ FileUtils.mkdir_p(File.dirname(path(style)))
26
+ result = file.stream_to(path(style))
27
+ file.close
28
+ result.close
20
29
  end
30
+ @queued_for_write = {}
21
31
  end
22
32
 
23
33
  def flush_deletes #:nodoc:
24
- @queued_for_delete.compact.each do |file|
34
+ @queued_for_delete.each do |path|
25
35
  begin
26
- FileUtils.rm(file.path)
36
+ FileUtils.rm(path) if File.exist?(path)
27
37
  rescue Errno::ENOENT => e
28
- # ignore them
38
+ # ignore file-not-found, let everything else pass
29
39
  end
30
40
  end
31
41
  @queued_for_delete = []
32
42
  end
33
43
  end
34
44
 
35
- # With the S3 module included, @file and the @processed_files will be
36
- # RightAws::S3::Key instances.
37
45
  module S3
38
46
  def self.extended base
39
47
  require 'right_aws'
@@ -42,51 +50,62 @@ module Paperclip
42
50
  @s3_credentials = parse_credentials(@options[:s3_credentials])
43
51
  @s3_options = @options[:s3_options] || {}
44
52
  @s3_permissions = @options[:s3_permissions] || 'public-read'
45
-
46
- @s3 = RightAws::S3.new(@s3_credentials[:access_key_id],
47
- @s3_credentials[:secret_access_key],
48
- @s3_options)
49
- @s3_bucket = @s3.bucket(@bucket, true, @s3_permissions)
50
53
  @url = ":s3_url"
51
54
  end
52
55
  base.class.interpolations[:s3_url] = lambda do |attachment, style|
53
- attachment.to_io(style).public_link
56
+ "https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
54
57
  end
55
58
  end
56
59
 
60
+ def s3
61
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
62
+ @s3_credentials[:secret_access_key],
63
+ @s3_options)
64
+ end
65
+
66
+ def s3_bucket
67
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
68
+ end
69
+
70
+ def bucket_name
71
+ @bucket
72
+ end
73
+
57
74
  def parse_credentials creds
58
75
  creds = find_credentials(creds).stringify_keys
59
76
  (creds[ENV['RAILS_ENV']] || creds).symbolize_keys
60
77
  end
78
+
79
+ def exists?(style = default_style)
80
+ s3_bucket.key(path(style)) ? true : false
81
+ end
61
82
 
62
- def locate_files
63
- [:original, *@styles.keys].uniq.inject({}) do |files, style|
64
- files[style] = @s3_bucket.key(path(style))
65
- files
66
- end
83
+ # Returns representation of the data of the file assigned to the given
84
+ # style, in the format most representative of the current storage.
85
+ def to_file style = default_style
86
+ @queued_for_write[style] || s3_bucket.key(path(style))
67
87
  end
88
+ alias_method :to_io, :to_file
68
89
 
69
90
  def flush_writes #:nodoc:
70
- return if not dirty?
71
- @processed_files.each do |style, key|
91
+ @queued_for_write.each do |style, file|
72
92
  begin
73
- unless key.is_a? RightAws::S3::Key
74
- saved_data = key
75
- key = @processed_files[style] = @s3_bucket.key(path(style))
76
- key.data = saved_data
77
- end
93
+ key = s3_bucket.key(path(style))
94
+ key.data = file
78
95
  key.put(nil, @s3_permissions)
79
96
  rescue RightAws::AwsError => e
80
- @processed_files[style] = nil
81
97
  raise
82
98
  end
83
99
  end
100
+ @queued_for_write = {}
84
101
  end
85
102
 
86
103
  def flush_deletes #:nodoc:
87
- @queued_for_delete.compact.each do |file|
104
+ @queued_for_delete.each do |path|
88
105
  begin
89
- file.delete
106
+ if file = s3_bucket.key(path)
107
+ file.delete
108
+ end
90
109
  rescue RightAws::AwsError
91
110
  # Ignore this.
92
111
  end
@@ -9,7 +9,8 @@ module Paperclip
9
9
  type = self.path.match(/\.(\w+)$/)[1] rescue "octet-stream"
10
10
  case type
11
11
  when "jpg", "png", "gif" then "image/#{type}"
12
- when "txt", "csv", "xml", "html", "htm", "css", "js" then "text/#{type}"
12
+ when "txt" then "text/plain"
13
+ when "csv", "xml", "html", "htm", "css", "js" then "text/#{type}"
13
14
  else "x-application/#{type}"
14
15
  end
15
16
  end
@@ -1,11 +1,13 @@
1
1
  def obtain_class
2
2
  class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
3
4
  @klass = Object.const_get(class_name)
4
5
  end
5
6
 
6
7
  def obtain_attachments
7
8
  name = ENV['ATTACHMENT'] || ENV['attachment']
8
- if !name.blank? && @klass.attachment_names.include?(name)
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)
9
11
  [ name ]
10
12
  else
11
13
  @klass.attachment_definitions.keys
@@ -16,14 +18,14 @@ namespace :paperclip do
16
18
  desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)"
17
19
  task :refresh => :environment do
18
20
  klass = obtain_class
19
- instances = klass.find(:all)
20
21
  names = obtain_attachments
22
+ instances = klass.find(:all)
21
23
 
22
24
  puts "Regenerating thumbnails for #{instances.length} instances of #{klass.name}:"
23
25
  instances.each do |instance|
24
26
  names.each do |name|
25
27
  result = if instance.send("#{ name }?")
26
- instance.send(name).send("post_process")
28
+ instance.send(name).reprocess!
27
29
  instance.send(name).save
28
30
  else
29
31
  true