paperclip-cloudfiles 2.3.2 → 2.3.8

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 (48) hide show
  1. data/README.rdoc +10 -3
  2. data/Rakefile +8 -4
  3. data/generators/paperclip/USAGE +2 -2
  4. data/generators/paperclip/paperclip_generator.rb +8 -8
  5. data/lib/generators/paperclip/USAGE +8 -0
  6. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  7. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  8. data/lib/paperclip/attachment.rb +38 -18
  9. data/lib/paperclip/command_line.rb +80 -0
  10. data/lib/paperclip/geometry.rb +7 -7
  11. data/lib/paperclip/interpolations.rb +8 -2
  12. data/lib/paperclip/iostream.rb +11 -25
  13. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +3 -3
  14. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +0 -1
  15. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +0 -1
  16. data/lib/paperclip/processor.rb +15 -6
  17. data/lib/paperclip/railtie.rb +24 -0
  18. data/lib/paperclip/storage/cloud_files.rb +131 -0
  19. data/lib/paperclip/storage/filesystem.rb +73 -0
  20. data/lib/paperclip/storage/s3.rb +192 -0
  21. data/lib/paperclip/storage.rb +3 -371
  22. data/lib/paperclip/style.rb +11 -11
  23. data/lib/paperclip/thumbnail.rb +16 -15
  24. data/lib/paperclip/upfile.rb +5 -3
  25. data/lib/paperclip/version.rb +3 -0
  26. data/lib/paperclip.rb +78 -92
  27. data/lib/tasks/paperclip.rake +72 -0
  28. data/rails/init.rb +2 -0
  29. data/shoulda_macros/paperclip.rb +1 -2
  30. data/test/attachment_test.rb +74 -28
  31. data/test/command_line_test.rb +133 -0
  32. data/test/geometry_test.rb +2 -2
  33. data/test/helper.rb +22 -24
  34. data/test/integration_test.rb +10 -11
  35. data/test/interpolations_test.rb +7 -4
  36. data/test/iostream_test.rb +6 -13
  37. data/test/matchers/have_attached_file_matcher_test.rb +1 -1
  38. data/test/matchers/validate_attachment_content_type_matcher_test.rb +11 -1
  39. data/test/matchers/validate_attachment_presence_matcher_test.rb +1 -1
  40. data/test/matchers/validate_attachment_size_matcher_test.rb +1 -1
  41. data/test/paperclip_test.rb +54 -80
  42. data/test/processor_test.rb +1 -1
  43. data/test/storage_test.rb +32 -12
  44. data/test/style_test.rb +17 -17
  45. data/test/thumbnail_test.rb +18 -18
  46. data/test/upfile_test.rb +1 -1
  47. metadata +58 -31
  48. data/tasks/paperclip_tasks.rake +0 -79
@@ -1,371 +1,3 @@
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_name = default_style)
24
- if original_filename
25
- File.exist?(path(style_name))
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_name = default_style
34
- @queued_for_write[style_name] || (File.new(path(style_name), 'rb') if exists?(style_name))
35
- end
36
-
37
- def flush_writes #:nodoc:
38
- @queued_for_write.each do |style_name, file|
39
- file.close
40
- FileUtils.mkdir_p(File.dirname(path(style_name)))
41
- log("saving #{path(style_name)}")
42
- FileUtils.mv(file.path, path(style_name))
43
- FileUtils.chmod(0644, path(style_name))
44
- end
45
- @queued_for_write = {}
46
- end
47
-
48
- def flush_deletes #:nodoc:
49
- @queued_for_delete.each do |path|
50
- begin
51
- log("deleting #{path}")
52
- FileUtils.rm(path) if File.exist?(path)
53
- rescue Errno::ENOENT => e
54
- # ignore file-not-found, let everything else pass
55
- end
56
- begin
57
- while(true)
58
- path = File.dirname(path)
59
- FileUtils.rmdir(path)
60
- end
61
- rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
62
- # Stop trying to remove parent directories
63
- rescue SystemCallError => e
64
- log("There was an unexpected error while deleting directories: #{e.class}")
65
- # Ignore it
66
- end
67
- end
68
- @queued_for_delete = []
69
- end
70
- end
71
-
72
- # Amazon's S3 file hosting service is a scalable, easy place to store files for
73
- # distribution. You can find out more about it at http://aws.amazon.com/s3
74
- # There are a few S3-specific options for has_attached_file:
75
- # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
76
- # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
77
- # gives you. You can 'environment-space' this just like you do to your
78
- # database.yml file, so different environments can use different accounts:
79
- # development:
80
- # access_key_id: 123...
81
- # secret_access_key: 123...
82
- # test:
83
- # access_key_id: abc...
84
- # secret_access_key: abc...
85
- # production:
86
- # access_key_id: 456...
87
- # secret_access_key: 456...
88
- # This is not required, however, and the file may simply look like this:
89
- # access_key_id: 456...
90
- # secret_access_key: 456...
91
- # In which case, those access keys will be used in all environments. You can also
92
- # put your bucket name in this file, instead of adding it to the code directly.
93
- # This is useful when you want the same account but a different bucket for
94
- # development versus production.
95
- # * +s3_permissions+: This is a String that should be one of the "canned" access
96
- # policies that S3 provides (more information can be found here:
97
- # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
98
- # The default for Paperclip is :public_read.
99
- # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
100
- # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
101
- # default), and 'https' when your :s3_permissions are anything else.
102
- # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
103
- # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
104
- # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
105
- # Paperclip will attempt to create it. The bucket name will not be interpolated.
106
- # You can define the bucket as a Proc if you want to determine it's name at runtime.
107
- # Paperclip will call that Proc with attachment as the only argument.
108
- # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
109
- # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
110
- # link in the +url+ entry for more information about S3 domains and buckets.
111
- # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
112
- # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
113
- # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
114
- # :s3_alias_url. You can read more about CNAMEs and S3 at
115
- # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
116
- # Normally, this won't matter in the slightest and you can leave the default (which is
117
- # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
118
- # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
119
- # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
120
- # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
121
- # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
122
- # by S3.
123
- # * +path+: This is the key under the bucket in which the file will be stored. The
124
- # URL will be constructed from the bucket and the path. This is what you will want
125
- # to interpolate. Keys should be unique, like filenames, and despite the fact that
126
- # S3 (strictly speaking) does not support directories, you can still use a / to
127
- # separate parts of your file name.
128
- module S3
129
- def self.extended base
130
- begin
131
- require 'aws/s3'
132
- rescue LoadError => e
133
- e.message << " (You may need to install the aws-s3 gem)"
134
- raise e
135
- end
136
-
137
- base.instance_eval do
138
- @s3_credentials = parse_credentials(@options[:s3_credentials])
139
- @bucket = @options[:bucket] || @s3_credentials[:bucket]
140
- @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
141
- @s3_options = @options[:s3_options] || {}
142
- @s3_permissions = @options[:s3_permissions] || :public_read
143
- @s3_protocol = @options[:s3_protocol] || (@s3_permissions == :public_read ? 'http' : 'https')
144
- @s3_headers = @options[:s3_headers] || {}
145
- @s3_host_alias = @options[:s3_host_alias]
146
- @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
147
- AWS::S3::Base.establish_connection!( @s3_options.merge(
148
- :access_key_id => @s3_credentials[:access_key_id],
149
- :secret_access_key => @s3_credentials[:secret_access_key]
150
- ))
151
- end
152
- Paperclip.interpolates(:s3_alias_url) do |attachment, style|
153
- "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
154
- end
155
- Paperclip.interpolates(:s3_path_url) do |attachment, style|
156
- "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
157
- end
158
- Paperclip.interpolates(:s3_domain_url) do |attachment, style|
159
- "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
160
- end
161
- end
162
-
163
- def expiring_url(time = 3600)
164
- AWS::S3::S3Object.url_for(path, bucket_name, :expires_in => time )
165
- end
166
-
167
- def bucket_name
168
- @bucket
169
- end
170
-
171
- def s3_host_alias
172
- @s3_host_alias
173
- end
174
-
175
- def parse_credentials creds
176
- creds = find_credentials(creds).stringify_keys
177
- (creds[Rails.env] || creds).symbolize_keys
178
- end
179
-
180
- def exists?(style = default_style)
181
- if original_filename
182
- AWS::S3::S3Object.exists?(path(style), bucket_name)
183
- else
184
- false
185
- end
186
- end
187
-
188
- def s3_protocol
189
- @s3_protocol
190
- end
191
-
192
- # Returns representation of the data of the file assigned to the given
193
- # style, in the format most representative of the current storage.
194
- def to_file style = default_style
195
- return @queued_for_write[style] if @queued_for_write[style]
196
- file = Tempfile.new(path(style))
197
- file.write(AWS::S3::S3Object.value(path(style), bucket_name))
198
- file.rewind
199
- return file
200
- end
201
-
202
- def flush_writes #:nodoc:
203
- @queued_for_write.each do |style, file|
204
- begin
205
- log("saving #{path(style)}")
206
- AWS::S3::S3Object.store(path(style),
207
- file,
208
- bucket_name,
209
- {:content_type => instance_read(:content_type),
210
- :access => @s3_permissions,
211
- }.merge(@s3_headers))
212
- rescue AWS::S3::ResponseError => e
213
- raise
214
- end
215
- end
216
- @queued_for_write = {}
217
- end
218
-
219
- def flush_deletes #:nodoc:
220
- @queued_for_delete.each do |path|
221
- begin
222
- log("deleting #{path}")
223
- AWS::S3::S3Object.delete(path, bucket_name)
224
- rescue AWS::S3::ResponseError
225
- # Ignore this.
226
- end
227
- end
228
- @queued_for_delete = []
229
- end
230
-
231
- def find_credentials creds
232
- case creds
233
- when File
234
- YAML::load(ERB.new(File.read(creds.path)).result)
235
- when String, Pathname
236
- YAML::load(ERB.new(File.read(creds)).result)
237
- when Hash
238
- creds
239
- else
240
- raise ArgumentError, "Credentials are not a path, file, or hash."
241
- end
242
- end
243
- private :find_credentials
244
-
245
- end
246
-
247
- # Rackspace's Cloud Files service is a scalable, easy place to store files for
248
- # distribution, and is integrated into the Limelight CDN. You can find out more about
249
- # it at http://www.rackspacecloud.com/cloud_hosting_products/files
250
- #
251
- # To install the Cloud Files gem, add the Gemcutter gem source ("gem sources -a http://gemcutter.org"), then
252
- # do a "gem install cloudfiles". For more information, see the github repository at http://github.com/rackspace/ruby-cloudfiles/
253
- #
254
- # There are a few Cloud Files-specific options for has_attached_file:
255
- # * +cloudfiles_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
256
- # to a YAML file containing the +username+ and +api_key+ that Rackspace
257
- # gives you. Rackspace customers using the cloudfiles gem >= 1.4.1 can also set a servicenet
258
- # variable to true to send traffic over the unbilled internal Rackspace service network.
259
- # You can 'environment-space' this just like you do to your
260
- # database.yml file, so different environments can use different accounts:
261
- # development:
262
- # username: hayley
263
- # api_key: a7f...
264
- # test:
265
- # username: katherine
266
- # api_key: 7fa...
267
- # production:
268
- # username: minter
269
- # api_key: 87k...
270
- # servicenet: true
271
- # This is not required, however, and the file may simply look like this:
272
- # username: minter...
273
- # api_key: 11q...
274
- # In which case, those access keys will be used in all environments. You can also
275
- # put your container name in this file, instead of adding it to the code directly.
276
- # This is useful when you want the same account but a different container for
277
- # development versus production.
278
- # * +container+: This is the name of the Cloud Files container that will store your files.
279
- # This container should be marked "public" so that the files are available to the world at large.
280
- # If the container does not exist, it will be created and marked public.
281
- # * +path+: This is the path under the container in which the file will be stored. The
282
- # CDN URL will be constructed from the CDN identifier for the container and the path. This is what
283
- # you will want to interpolate. Keys should be unique, like filenames, and despite the fact that
284
- # Cloud Files (strictly speaking) does not support directories, you can still use a / to
285
- # separate parts of your file name, and they will show up in the URL structure.
286
- module CloudFile
287
- def self.extended base
288
- require 'cloudfiles'
289
- @@container ||= {}
290
- base.instance_eval do
291
- @cloudfiles_credentials = parse_credentials(@options[:cloudfiles_credentials])
292
- @container_name = @options[:container] || @cloudfiles_credentials[:container]
293
- @container_name = @container_name.call(self) if @container_name.is_a?(Proc)
294
- @cloudfiles_options = @options[:cloudfiles_options] || {}
295
- @@cdn_url = cloudfiles_container.cdn_url
296
- @path_filename = ":cf_path_filename" unless @url.to_s.match(/^:cf.*filename$/)
297
- @url = @@cdn_url + "/#{URI.encode(@path_filename).gsub(/&/,'%26')}"
298
- @path = (Paperclip::Attachment.default_options[:path] == @options[:path]) ? ":attachment/:id/:style/:basename.:extension" : @options[:path]
299
- end
300
- Paperclip.interpolates(:cf_path_filename) do |attachment, style|
301
- attachment.path(style)
302
- end
303
- end
304
-
305
- def cloudfiles
306
- @@cf ||= CloudFiles::Connection.new(@cloudfiles_credentials[:username], @cloudfiles_credentials[:api_key], true, @cloudfiles_credentials[:servicenet])
307
- end
308
-
309
- def create_container
310
- container = cloudfiles.create_container(@container_name)
311
- container.make_public
312
- container
313
- end
314
-
315
- def cloudfiles_container
316
- @@container[@container_name] ||= create_container
317
- end
318
-
319
- def container_name
320
- @container_name
321
- end
322
-
323
- def parse_credentials creds
324
- creds = find_credentials(creds).stringify_keys
325
- (creds[RAILS_ENV] || creds).symbolize_keys
326
- end
327
-
328
- def exists?(style = default_style)
329
- cloudfiles_container.object_exists?(path(style))
330
- end
331
-
332
- # Returns representation of the data of the file assigned to the given
333
- # style, in the format most representative of the current storage.
334
- def to_file style = default_style
335
- @queued_for_write[style] || cloudfiles_container.create_object(path(style))
336
- end
337
- alias_method :to_io, :to_file
338
-
339
- def flush_writes #:nodoc:
340
- @queued_for_write.each do |style, file|
341
- object = cloudfiles_container.create_object(path(style),false)
342
- object.write(file)
343
- end
344
- @queued_for_write = {}
345
- end
346
-
347
- def flush_deletes #:nodoc:
348
- @queued_for_delete.each do |path|
349
- cloudfiles_container.delete_object(path)
350
- end
351
- @queued_for_delete = []
352
- end
353
-
354
- def find_credentials creds
355
- case creds
356
- when File
357
- YAML.load_file(creds.path)
358
- when String
359
- YAML.load_file(creds)
360
- when Hash
361
- creds
362
- else
363
- raise ArgumentError, "Credentials are not a path, file, or hash."
364
- end
365
- end
366
- private :find_credentials
367
-
368
- end
369
-
370
- end
371
- end
1
+ require "paperclip/storage/filesystem"
2
+ require "paperclip/storage/s3"
3
+ require "paperclip/storage/cloud_files"
@@ -3,7 +3,7 @@ module Paperclip
3
3
  # The Style class holds the definition of a thumbnail style, applying
4
4
  # whatever processing is required to normalize the definition and delaying
5
5
  # the evaluation of block parameters until useful context is available.
6
-
6
+
7
7
  class Style
8
8
 
9
9
  attr_reader :name, :attachment, :format
@@ -25,7 +25,7 @@ module Paperclip
25
25
  end
26
26
  @format = nil if @format.blank?
27
27
  end
28
-
28
+
29
29
  # retrieves from the attachment the processors defined in the has_attached_file call
30
30
  # (which method (in the attachment) will call any supplied procs)
31
31
  # There is an important change of interface here: a style rule can set its own processors
@@ -33,17 +33,17 @@ module Paperclip
33
33
  def processors
34
34
  @processors || attachment.processors
35
35
  end
36
-
36
+
37
37
  # retrieves from the attachment the whiny setting
38
38
  def whiny
39
39
  attachment.whiny
40
40
  end
41
-
41
+
42
42
  # returns true if we're inclined to grumble
43
43
  def whiny?
44
44
  !!whiny
45
45
  end
46
-
46
+
47
47
  def convert_options
48
48
  attachment.send(:extra_options_for, name)
49
49
  end
@@ -55,7 +55,7 @@ module Paperclip
55
55
  end
56
56
 
57
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
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
59
  # initialization and any procs are called here, just before post-processing.
60
60
  def processor_options
61
61
  args = {}
@@ -63,7 +63,7 @@ module Paperclip
63
63
  args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
64
  end
65
65
  [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
- (arg = send(k)) && args[k] = arg
66
+ (arg = send(k)) && args[k] = arg
67
67
  end
68
68
  args
69
69
  end
@@ -71,13 +71,13 @@ module Paperclip
71
71
  # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
72
  # eg. @attachment.styles[:large][:geometry]@ will still work
73
73
  def [](key)
74
- if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
75
  send(key)
76
76
  elsif defined? @other_args[key]
77
77
  @other_args[key]
78
78
  end
79
79
  end
80
-
80
+
81
81
  def []=(key, value)
82
82
  if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
83
  send("#{key}=".intern, value)
@@ -85,6 +85,6 @@ module Paperclip
85
85
  @other_args[key] = value
86
86
  end
87
87
  end
88
-
88
+
89
89
  end
90
- end
90
+ end
@@ -9,10 +9,10 @@ module Paperclip
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
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
12
+ # set, the options will be appended to the convert command upon image conversion
13
13
  def initialize file, options = {}, attachment = nil
14
14
  super
15
-
15
+
16
16
  geometry = options[:geometry]
17
17
  @file = file
18
18
  @crop = geometry[-1,1] == '#'
@@ -28,14 +28,14 @@ module Paperclip
28
28
 
29
29
  @current_format = File.extname(@file.path)
30
30
  @basename = File.basename(@file.path, @current_format)
31
-
31
+
32
32
  end
33
33
 
34
34
  # Returns true if the +target_geometry+ is meant to crop.
35
35
  def crop?
36
36
  @crop
37
37
  end
38
-
38
+
39
39
  # Returns true if the image is meant to make use of additional convert options.
40
40
  def convert_options?
41
41
  !@convert_options.nil? && !@convert_options.empty?
@@ -45,19 +45,20 @@ module Paperclip
45
45
  # that contains the new image.
46
46
  def make
47
47
  src = @file
48
- dst = Tempfile.new([@basename, @format].compact.join("."))
48
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
49
49
  dst.binmode
50
50
 
51
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
52
+ parameters = []
53
+ parameters << source_file_options
54
+ parameters << ":source"
55
+ parameters << transformation_command
56
+ parameters << convert_options
57
+ parameters << ":dest"
58
+
59
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
59
60
 
60
- success = Paperclip.run("convert", *options)
61
+ success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}[0]", :dest => File.expand_path(dst.path))
61
62
  rescue PaperclipCommandLineError => e
62
63
  raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
63
64
  end
@@ -70,8 +71,8 @@ module Paperclip
70
71
  def transformation_command
71
72
  scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
72
73
  trans = []
73
- trans << "-resize" << scale unless scale.nil? || scale.empty?
74
- trans << "-crop" << crop << "+repage" if crop
74
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
75
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
75
76
  trans
76
77
  end
77
78
  end
@@ -17,7 +17,7 @@ module Paperclip
17
17
  when "csv", "xml", "css" then "text/#{type}"
18
18
  else
19
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}")
20
+ content_type = (Paperclip.run("file", "-b --mime-type :file", :file => self.path).split(':').last.strip rescue "application/x-#{type}")
21
21
  content_type = "application/x-#{type}" if content_type.match(/\(.*?\)/)
22
22
  content_type
23
23
  end
@@ -37,17 +37,19 @@ end
37
37
 
38
38
  if defined? StringIO
39
39
  class StringIO
40
- attr_accessor :original_filename, :content_type
40
+ attr_accessor :original_filename, :content_type, :fingerprint
41
41
  def original_filename
42
42
  @original_filename ||= "stringio.txt"
43
43
  end
44
44
  def content_type
45
45
  @content_type ||= "text/plain"
46
46
  end
47
+ def fingerprint
48
+ @fingerprint ||= Digest::MD5.hexdigest(self.string)
49
+ end
47
50
  end
48
51
  end
49
52
 
50
53
  class File #:nodoc:
51
54
  include Paperclip::Upfile
52
55
  end
53
-
@@ -0,0 +1,3 @@
1
+ module Paperclip
2
+ VERSION = "2.3.8" unless defined? Paperclip::VERSION
3
+ end