paperclip-cloudfiles 2.3.2 → 2.3.8

Sign up to get free protection for your applications and to get access to all the features.
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