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.
- data/lib/paperclip.rb +42 -7
- data/lib/paperclip/attachment.rb +41 -42
- data/lib/paperclip/geometry.rb +1 -1
- data/lib/paperclip/iostream.rb +1 -1
- data/lib/paperclip/storage.rb +54 -35
- data/lib/paperclip/upfile.rb +2 -1
- data/tasks/paperclip_tasks.rake +5 -3
- data/test/debug.log +1671 -602
- data/test/fixtures/text.txt +0 -0
- data/test/test_attachment.rb +96 -73
- data/test/test_geometry.rb +1 -1
- data/test/test_integration.rb +99 -6
- data/test/test_paperclip.rb +23 -3
- data/test/test_storage.rb +10 -14
- metadata +4 -3
data/lib/paperclip.rb
CHANGED
@@ -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.
|
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).
|
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?
|
156
|
-
|
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
|
-
|
165
|
-
|
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
|
data/lib/paperclip/attachment.rb
CHANGED
@@ -15,7 +15,7 @@ module Paperclip
|
|
15
15
|
}
|
16
16
|
end
|
17
17
|
|
18
|
-
attr_reader :name, :instance, :
|
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
|
-
@
|
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
|
-
@
|
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 =
|
83
|
-
|
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.
|
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
|
201
|
+
return if @queued_for_write[:original].nil?
|
200
202
|
@styles.each do |name, args|
|
201
203
|
begin
|
202
204
|
dimensions, format = args
|
203
|
-
@
|
204
|
-
|
205
|
-
|
206
|
-
|
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 =
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
229
|
-
@
|
230
|
-
|
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
|
data/lib/paperclip/geometry.rb
CHANGED
@@ -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(
|
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.
|
data/lib/paperclip/iostream.rb
CHANGED
data/lib/paperclip/storage.rb
CHANGED
@@ -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
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
@
|
18
|
-
FileUtils.mkdir_p(
|
19
|
-
|
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.
|
34
|
+
@queued_for_delete.each do |path|
|
25
35
|
begin
|
26
|
-
FileUtils.rm(
|
36
|
+
FileUtils.rm(path) if File.exist?(path)
|
27
37
|
rescue Errno::ENOENT => e
|
28
|
-
# ignore
|
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.
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
71
|
-
@processed_files.each do |style, key|
|
91
|
+
@queued_for_write.each do |style, file|
|
72
92
|
begin
|
73
|
-
|
74
|
-
|
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.
|
104
|
+
@queued_for_delete.each do |path|
|
88
105
|
begin
|
89
|
-
file.
|
106
|
+
if file = s3_bucket.key(path)
|
107
|
+
file.delete
|
108
|
+
end
|
90
109
|
rescue RightAws::AwsError
|
91
110
|
# Ignore this.
|
92
111
|
end
|
data/lib/paperclip/upfile.rb
CHANGED
@@ -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"
|
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
|
data/tasks/paperclip_tasks.rake
CHANGED
@@ -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
|
-
|
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).
|
28
|
+
instance.send(name).reprocess!
|
27
29
|
instance.send(name).save
|
28
30
|
else
|
29
31
|
true
|