cloudfuji_paperclip 2.4.6

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 (105) hide show
  1. data/.gitignore +22 -0
  2. data/.travis.yml +13 -0
  3. data/Appraisals +14 -0
  4. data/CONTRIBUTING.md +38 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.lock +137 -0
  7. data/LICENSE +26 -0
  8. data/README.md +444 -0
  9. data/Rakefile +41 -0
  10. data/cucumber/paperclip_steps.rb +6 -0
  11. data/features/basic_integration.feature +46 -0
  12. data/features/rake_tasks.feature +63 -0
  13. data/features/step_definitions/attachment_steps.rb +65 -0
  14. data/features/step_definitions/html_steps.rb +15 -0
  15. data/features/step_definitions/rails_steps.rb +182 -0
  16. data/features/step_definitions/s3_steps.rb +14 -0
  17. data/features/step_definitions/web_steps.rb +209 -0
  18. data/features/support/env.rb +8 -0
  19. data/features/support/fakeweb.rb +3 -0
  20. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  21. data/features/support/fixtures/boot_config.txt +15 -0
  22. data/features/support/fixtures/gemfile.txt +5 -0
  23. data/features/support/fixtures/preinitializer.txt +20 -0
  24. data/features/support/paths.rb +28 -0
  25. data/features/support/rails.rb +46 -0
  26. data/features/support/selectors.rb +19 -0
  27. data/gemfiles/rails2.gemfile +9 -0
  28. data/gemfiles/rails3.gemfile +9 -0
  29. data/gemfiles/rails3_1.gemfile +9 -0
  30. data/generators/paperclip/USAGE +5 -0
  31. data/generators/paperclip/paperclip_generator.rb +27 -0
  32. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  33. data/init.rb +4 -0
  34. data/lib/generators/paperclip/USAGE +8 -0
  35. data/lib/generators/paperclip/paperclip_generator.rb +33 -0
  36. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  37. data/lib/paperclip/attachment.rb +468 -0
  38. data/lib/paperclip/callback_compatibility.rb +61 -0
  39. data/lib/paperclip/geometry.rb +120 -0
  40. data/lib/paperclip/interpolations.rb +174 -0
  41. data/lib/paperclip/iostream.rb +45 -0
  42. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  43. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +81 -0
  44. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  45. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  46. data/lib/paperclip/matchers.rb +64 -0
  47. data/lib/paperclip/missing_attachment_styles.rb +87 -0
  48. data/lib/paperclip/processor.rb +58 -0
  49. data/lib/paperclip/railtie.rb +31 -0
  50. data/lib/paperclip/schema.rb +39 -0
  51. data/lib/paperclip/storage/filesystem.rb +81 -0
  52. data/lib/paperclip/storage/fog.rb +174 -0
  53. data/lib/paperclip/storage/s3.rb +333 -0
  54. data/lib/paperclip/storage.rb +3 -0
  55. data/lib/paperclip/style.rb +103 -0
  56. data/lib/paperclip/thumbnail.rb +105 -0
  57. data/lib/paperclip/upfile.rb +62 -0
  58. data/lib/paperclip/url_generator.rb +64 -0
  59. data/lib/paperclip/version.rb +3 -0
  60. data/lib/paperclip.rb +487 -0
  61. data/lib/tasks/paperclip.rake +101 -0
  62. data/paperclip.gemspec +41 -0
  63. data/rails/init.rb +2 -0
  64. data/shoulda_macros/paperclip.rb +124 -0
  65. data/test/.gitignore +1 -0
  66. data/test/attachment_test.rb +1116 -0
  67. data/test/database.yml +4 -0
  68. data/test/fixtures/12k.png +0 -0
  69. data/test/fixtures/50x50.png +0 -0
  70. data/test/fixtures/5k.png +0 -0
  71. data/test/fixtures/animated.gif +0 -0
  72. data/test/fixtures/bad.png +1 -0
  73. data/test/fixtures/fog.yml +8 -0
  74. data/test/fixtures/question?mark.png +0 -0
  75. data/test/fixtures/s3.yml +8 -0
  76. data/test/fixtures/spaced file.png +0 -0
  77. data/test/fixtures/text.txt +1 -0
  78. data/test/fixtures/twopage.pdf +0 -0
  79. data/test/fixtures/uppercase.PNG +0 -0
  80. data/test/geometry_test.rb +206 -0
  81. data/test/helper.rb +177 -0
  82. data/test/integration_test.rb +654 -0
  83. data/test/interpolations_test.rb +216 -0
  84. data/test/iostream_test.rb +71 -0
  85. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  86. data/test/matchers/validate_attachment_content_type_matcher_test.rb +87 -0
  87. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  88. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  89. data/test/paperclip_missing_attachment_styles_test.rb +80 -0
  90. data/test/paperclip_test.rb +390 -0
  91. data/test/processor_test.rb +10 -0
  92. data/test/schema_test.rb +98 -0
  93. data/test/storage/filesystem_test.rb +56 -0
  94. data/test/storage/fog_test.rb +219 -0
  95. data/test/storage/s3_live_test.rb +138 -0
  96. data/test/storage/s3_test.rb +943 -0
  97. data/test/style_test.rb +209 -0
  98. data/test/support/mock_attachment.rb +22 -0
  99. data/test/support/mock_interpolator.rb +24 -0
  100. data/test/support/mock_model.rb +2 -0
  101. data/test/support/mock_url_generator_builder.rb +27 -0
  102. data/test/thumbnail_test.rb +383 -0
  103. data/test/upfile_test.rb +53 -0
  104. data/test/url_generator_test.rb +187 -0
  105. metadata +404 -0
@@ -0,0 +1,333 @@
1
+ module Paperclip
2
+ module Storage
3
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
4
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
5
+ #
6
+ # To use Paperclip with S3, include the +aws-sdk+ gem in your Gemfile:
7
+ # gem 'aws-sdk'
8
+ # There are a few S3-specific options for has_attached_file:
9
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
10
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
11
+ # gives you. You can 'environment-space' this just like you do to your
12
+ # database.yml file, so different environments can use different accounts:
13
+ # development:
14
+ # access_key_id: 123...
15
+ # secret_access_key: 123...
16
+ # test:
17
+ # access_key_id: abc...
18
+ # secret_access_key: abc...
19
+ # production:
20
+ # access_key_id: 456...
21
+ # secret_access_key: 456...
22
+ # This is not required, however, and the file may simply look like this:
23
+ # access_key_id: 456...
24
+ # secret_access_key: 456...
25
+ # In which case, those access keys will be used in all environments. You can also
26
+ # put your bucket name in this file, instead of adding it to the code directly.
27
+ # This is useful when you want the same account but a different bucket for
28
+ # development versus production.
29
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
30
+ # policies that S3 provides (more information can be found here:
31
+ # http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAccessPolicy.html)
32
+ # The default for Paperclip is :public_read.
33
+ #
34
+ # You can set permission on a per style bases by doing the following:
35
+ # :s3_permissions => {
36
+ # :original => :private
37
+ # }
38
+ # Or globaly:
39
+ # :s3_permissions => :private
40
+ #
41
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
42
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
43
+ # default), and 'https' when your :s3_permissions are anything else.
44
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
45
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
46
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
47
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
48
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
49
+ # Paperclip will call that Proc with attachment as the only argument.
50
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
51
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
52
+ # link in the +url+ entry for more information about S3 domains and buckets.
53
+ # * +url+: There are four options for the S3 url. You can choose to have the bucket's name
54
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
55
+ # You can also specify a CNAME (which requires the CNAME to be specified as
56
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
57
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
58
+ # Normally, this won't matter in the slightest and you can leave the default (which is
59
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
60
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
61
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
62
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
63
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
64
+ # by S3. The fourth option for the S3 url is :asset_host, which uses Rails' built-in
65
+ # asset_host settings. NOTE: To get the full url from a paperclip'd object, use the
66
+ # image_path helper; this is what image_tag uses to generate the url for an img tag.
67
+ # * +path+: This is the key under the bucket in which the file will be stored. The
68
+ # URL will be constructed from the bucket and the path. This is what you will want
69
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
70
+ # S3 (strictly speaking) does not support directories, you can still use a / to
71
+ # separate parts of your file name.
72
+ # * +s3_host_name+: If you are using your bucket in Tokyo region etc, write host_name.
73
+ # * +s3_metadata+: These key/value pairs will be stored with the
74
+ # object. This option works by prefixing each key with
75
+ # "x-amz-meta-" before sending it as a header on the object
76
+ # upload request.
77
+ # * +s3_storage_class+: If this option is set to
78
+ # <tt>:reduced_redundancy</tt>, the object will be stored using Reduced
79
+ # Redundancy Storage. RRS enables customers to reduce their
80
+ # costs by storing non-critical, reproducible data at lower
81
+ # levels of redundancy than Amazon S3's standard storage.
82
+ module S3
83
+ def self.extended base
84
+ begin
85
+ require 'aws-sdk'
86
+ rescue LoadError => e
87
+ e.message << " (You may need to install the aws-sdk gem)"
88
+ raise e
89
+ end unless defined?(AWS::Core)
90
+
91
+ base.instance_eval do
92
+ @s3_options = @options[:s3_options] || {}
93
+ @s3_permissions = set_permissions(@options[:s3_permissions])
94
+ @s3_protocol = @options[:s3_protocol] ||
95
+ Proc.new do |style, attachment|
96
+ permission = (@s3_permissions[style.to_sym] || @s3_permissions[:default])
97
+ permission = permission.call(attachment, style) if permission.is_a?(Proc)
98
+ (permission == :public_read) ? 'http' : 'https'
99
+ end
100
+ @s3_metadata = @options[:s3_metadata] || {}
101
+ @s3_headers = (@options[:s3_headers] || {}).inject({}) do |headers,(name,value)|
102
+ case name.to_s
103
+ when /^x-amz-meta-(.*)/i
104
+ @s3_metadata[$1.downcase] = value
105
+ else
106
+ name = name.to_s.downcase.sub(/^x-amz-/,'').tr("-","_").to_sym
107
+ headers[name] = value
108
+ end
109
+ headers
110
+ end
111
+
112
+ @s3_headers[:storage_class] = @options[:s3_storage_class] if @options[:s3_storage_class]
113
+
114
+ unless @options[:url].to_s.match(/^:s3.*url$/) || @options[:url] == ":asset_host"
115
+ @options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/^:rails_root\/public\/system/, '')
116
+ @options[:url] = ":s3_path_url"
117
+ end
118
+ @options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
119
+
120
+ @http_proxy = @options[:http_proxy] || nil
121
+ end
122
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
123
+ "#{attachment.s3_protocol(style)}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
124
+ end unless Paperclip::Interpolations.respond_to? :s3_alias_url
125
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
126
+ "#{attachment.s3_protocol(style)}://#{attachment.s3_host_name}/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
127
+ end unless Paperclip::Interpolations.respond_to? :s3_path_url
128
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
129
+ "#{attachment.s3_protocol(style)}://#{attachment.bucket_name}.#{attachment.s3_host_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
130
+ end unless Paperclip::Interpolations.respond_to? :s3_domain_url
131
+ Paperclip.interpolates(:asset_host) do |attachment, style|
132
+ "#{attachment.path(style).gsub(%r{^/}, "")}"
133
+ end unless Paperclip::Interpolations.respond_to? :asset_host
134
+ end
135
+
136
+ def expiring_url(time = 3600, style_name = default_style)
137
+ if path
138
+ s3_object(style_name).url_for(:read, :expires => time, :secure => use_secure_protocol?(style_name)).to_s
139
+ end
140
+ end
141
+
142
+ def s3_credentials
143
+ @s3_credentials ||= parse_credentials(@options[:s3_credentials])
144
+ end
145
+
146
+ def s3_host_name
147
+ @options[:s3_host_name] || s3_credentials[:s3_host_name] || "s3.amazonaws.com"
148
+ end
149
+
150
+ def s3_host_alias
151
+ @s3_host_alias = @options[:s3_host_alias]
152
+ @s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)
153
+ @s3_host_alias
154
+ end
155
+
156
+ def bucket_name
157
+ @bucket = @options[:bucket] || s3_credentials[:bucket]
158
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
159
+ @bucket or raise ArgumentError, "missing required :bucket option"
160
+ end
161
+
162
+ def s3_interface
163
+ @s3_interface ||= begin
164
+ config = { :s3_endpoint => s3_host_name }
165
+
166
+ if using_http_proxy?
167
+
168
+ proxy_opts = { :host => http_proxy_host }
169
+ proxy_opts[:port] = http_proxy_port if http_proxy_port
170
+ if http_proxy_user
171
+ userinfo = http_proxy_user.to_s
172
+ userinfo += ":#{http_proxy_password}" if http_proxy_password
173
+ proxy_opts[:userinfo] = userinfo
174
+ end
175
+ config[:proxy_uri] = URI::HTTP.build(proxy_opts)
176
+ end
177
+
178
+ [:access_key_id, :secret_access_key, :session_token].each do |opt|
179
+ config[opt] = s3_credentials[opt] if s3_credentials[opt]
180
+ end
181
+
182
+ AWS::S3.new(config.merge(@s3_options))
183
+ end
184
+ end
185
+
186
+ def s3_bucket
187
+ @s3_bucket ||= s3_interface.buckets[bucket_name]
188
+ end
189
+
190
+ def s3_object style_name = default_style
191
+ s3_bucket.objects[path(style_name).sub(%r{^/},'')]
192
+ end
193
+
194
+ def using_http_proxy?
195
+ !!@http_proxy
196
+ end
197
+
198
+ def http_proxy_host
199
+ using_http_proxy? ? @http_proxy[:host] : nil
200
+ end
201
+
202
+ def http_proxy_port
203
+ using_http_proxy? ? @http_proxy[:port] : nil
204
+ end
205
+
206
+ def http_proxy_user
207
+ using_http_proxy? ? @http_proxy[:user] : nil
208
+ end
209
+
210
+ def http_proxy_password
211
+ using_http_proxy? ? @http_proxy[:password] : nil
212
+ end
213
+
214
+ def set_permissions permissions
215
+ if permissions.is_a?(Hash)
216
+ permissions[:default] = permissions[:default] || :public_read
217
+ else
218
+ permissions = { :default => permissions || :public_read }
219
+ end
220
+ permissions
221
+ end
222
+
223
+ def parse_credentials creds
224
+ creds = find_credentials(creds).stringify_keys
225
+ env = Object.const_defined?(:Rails) ? Rails.env : nil
226
+ (creds[env] || creds).symbolize_keys
227
+ end
228
+
229
+ def exists?(style = default_style)
230
+ if original_filename
231
+ s3_object(style).exists?
232
+ else
233
+ false
234
+ end
235
+ end
236
+
237
+ def s3_permissions(style = default_style)
238
+ s3_permissions = @s3_permissions[style] || @s3_permissions[:default]
239
+ s3_permissions = s3_permissions.call(self, style) if s3_permissions.is_a?(Proc)
240
+ s3_permissions
241
+ end
242
+
243
+ def s3_protocol(style = default_style)
244
+ if @s3_protocol.is_a?(Proc)
245
+ @s3_protocol.call(style, self)
246
+ else
247
+ @s3_protocol
248
+ end
249
+ end
250
+
251
+ # Returns representation of the data of the file assigned to the given
252
+ # style, in the format most representative of the current storage.
253
+ def to_file style = default_style
254
+ return @queued_for_write[style] if @queued_for_write[style]
255
+ filename = path(style)
256
+ extname = File.extname(filename)
257
+ basename = File.basename(filename, extname)
258
+ file = Tempfile.new([basename, extname])
259
+ file.binmode
260
+ file.write(s3_object(style).read)
261
+ file.rewind
262
+ return file
263
+ end
264
+
265
+ def create_bucket
266
+ s3_interface.buckets.create(bucket_name)
267
+ end
268
+
269
+ def flush_writes #:nodoc:
270
+ @queued_for_write.each do |style, file|
271
+ begin
272
+ log("saving #{path(style)}")
273
+ acl = @s3_permissions[style] || @s3_permissions[:default]
274
+ acl = acl.call(self, style) if acl.respond_to?(:call)
275
+ write_options = {
276
+ :content_type => file.content_type.to_s.strip,
277
+ :acl => acl
278
+ }
279
+ write_options[:metadata] = @s3_metadata unless @s3_metadata.empty?
280
+ write_options.merge!(@s3_headers)
281
+ s3_object(style).write(file, write_options)
282
+ rescue AWS::S3::Errors::NoSuchBucket => e
283
+ create_bucket
284
+ retry
285
+ end
286
+ end
287
+
288
+ after_flush_writes # allows attachment to clean up temp files
289
+
290
+ @queued_for_write = {}
291
+ end
292
+
293
+ def flush_deletes #:nodoc:
294
+ @queued_for_delete.each do |path|
295
+ begin
296
+ log("deleting #{path}")
297
+ s3_bucket.objects[path.sub(%r{^/},'')].delete
298
+ rescue AWS::Errors::Base => e
299
+ # Ignore this.
300
+ end
301
+ end
302
+ @queued_for_delete = []
303
+ end
304
+
305
+ def find_credentials creds
306
+ case creds
307
+ when File
308
+ YAML::load(ERB.new(File.read(creds.path)).result)
309
+ when String, Pathname
310
+ YAML::load(ERB.new(File.read(creds)).result)
311
+ when Hash
312
+ creds
313
+ else
314
+ raise ArgumentError, "Credentials are not a path, file, or hash."
315
+ end
316
+ end
317
+ private :find_credentials
318
+
319
+ def establish_connection!
320
+ @connection ||= AWS::S3::Base.establish_connection!( @s3_options.merge(
321
+ :access_key_id => s3_credentials[:access_key_id],
322
+ :secret_access_key => s3_credentials[:secret_access_key]
323
+ ))
324
+ end
325
+ private :establish_connection!
326
+
327
+ def use_secure_protocol?(style_name)
328
+ s3_protocol(style_name) == "https"
329
+ end
330
+ private :use_secure_protocol?
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,3 @@
1
+ require "paperclip/storage/filesystem"
2
+ require "paperclip/storage/fog"
3
+ require "paperclip/storage/s3"
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @convert_options = definition.delete(:convert_options)
22
+ @source_file_options = definition.delete(:source_file_options)
23
+ @other_args = definition
24
+ elsif definition.is_a? String
25
+ @geometry = definition
26
+ @format = nil
27
+ @other_args = {}
28
+ else
29
+ @geometry, @format = [definition, nil].flatten[0..1]
30
+ @other_args = {}
31
+ end
32
+ @format = nil if @format.blank?
33
+ end
34
+
35
+ # retrieves from the attachment the processors defined in the has_attached_file call
36
+ # (which method (in the attachment) will call any supplied procs)
37
+ # There is an important change of interface here: a style rule can set its own processors
38
+ # by default we behave as before, though.
39
+ # if a proc has been supplied, we call it here
40
+ def processors
41
+ @processors.respond_to?(:call) ? @processors.call(attachment.instance) : (@processors || attachment.processors)
42
+ end
43
+
44
+ # retrieves from the attachment the whiny setting
45
+ def whiny
46
+ attachment.whiny
47
+ end
48
+
49
+ # returns true if we're inclined to grumble
50
+ def whiny?
51
+ !!whiny
52
+ end
53
+
54
+ def convert_options
55
+ @convert_options.respond_to?(:call) ? @convert_options.call(attachment.instance) :
56
+ (@convert_options || attachment.send(:extra_options_for, name))
57
+ end
58
+
59
+ def source_file_options
60
+ @source_file_options.respond_to?(:call) ? @source_file_options.call(attachment.instance) :
61
+ (@source_file_options || attachment.send(:extra_source_file_options_for, name))
62
+ end
63
+
64
+ # returns the geometry string for this style
65
+ # if a proc has been supplied, we call it here
66
+ def geometry
67
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
68
+ end
69
+
70
+ # Supplies the hash of options that processors expect to receive as their second argument
71
+ # Arguments other than the standard geometry, format etc are just passed through from
72
+ # initialization and any procs are called here, just before post-processing.
73
+ def processor_options
74
+ args = {}
75
+ @other_args.each do |k,v|
76
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
77
+ end
78
+ [:processors, :geometry, :format, :whiny, :convert_options, :source_file_options].each do |k|
79
+ (arg = send(k)) && args[k] = arg
80
+ end
81
+ args
82
+ end
83
+
84
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
85
+ # eg. @attachment.styles[:large][:geometry]@ will still work
86
+ def [](key)
87
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
88
+ send(key)
89
+ elsif defined? @other_args[key]
90
+ @other_args[key]
91
+ end
92
+ end
93
+
94
+ def []=(key, value)
95
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
96
+ send("#{key}=".intern, value)
97
+ else
98
+ @other_args[key] = value
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,105 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options,
6
+ :source_file_options, :animated
7
+
8
+ # List of formats that we need to preserve animation
9
+ ANIMATED_FORMATS = %w(gif)
10
+
11
+ # Creates a Thumbnail object set to work on the +file+ given. It
12
+ # will attempt to transform the image into one defined by +target_geometry+
13
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
14
+ # unless specified. Thumbnail creation will raise no errors unless
15
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
16
+ # set, the options will be appended to the convert command upon image conversion
17
+ #
18
+ # Options include:
19
+ #
20
+ # +geometry+ - the desired width and height of the thumbnail (required)
21
+ # +file_geometry_parser+ - an object with a method named +from_file+ that takes an image file and produces its geometry and a +transformation_to+. Defaults to Paperclip::Geometry
22
+ # +string_geometry_parser+ - an object with a method named +parse+ that takes a string and produces an object with +width+, +height+, and +to_s+ accessors. Defaults to Paperclip::Geometry
23
+ # +source_file_options+ - flags passed to the +convert+ command that influence how the source file is read
24
+ # +convert_options+ - flags passed to the +convert+ command that influence how the image is processed
25
+ # +whiny+ - whether to raise an error when processing fails. Defaults to true
26
+ # +format+ - the desired filename extension
27
+ # +animated+ - whether to merge all the layers in the image. Defaults to true
28
+ def initialize(file, options = {}, attachment = nil)
29
+ super
30
+
31
+ geometry = options[:geometry] # this is not an option
32
+ @file = file
33
+ @crop = geometry[-1,1] == '#'
34
+ @target_geometry = (options[:string_geometry_parser] || Geometry).parse(geometry)
35
+ @current_geometry = (options[:file_geometry_parser] || Geometry).from_file(@file)
36
+ @source_file_options = options[:source_file_options]
37
+ @convert_options = options[:convert_options]
38
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
39
+ @format = options[:format]
40
+ @animated = options[:animated].nil? ? true : options[:animated]
41
+
42
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
43
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
44
+
45
+ @current_format = File.extname(@file.path)
46
+ @basename = File.basename(@file.path, @current_format)
47
+
48
+ end
49
+
50
+ # Returns true if the +target_geometry+ is meant to crop.
51
+ def crop?
52
+ @crop
53
+ end
54
+
55
+ # Returns true if the image is meant to make use of additional convert options.
56
+ def convert_options?
57
+ !@convert_options.nil? && !@convert_options.empty?
58
+ end
59
+
60
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
61
+ # that contains the new image.
62
+ def make
63
+ src = @file
64
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
65
+ dst.binmode
66
+
67
+ begin
68
+ parameters = []
69
+ parameters << source_file_options
70
+ parameters << ":source"
71
+ parameters << transformation_command
72
+ parameters << convert_options
73
+ parameters << ":dest"
74
+
75
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
76
+
77
+ success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}#{'[0]' unless animated?}", :dest => File.expand_path(dst.path))
78
+ rescue Cocaine::ExitStatusError => e
79
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
80
+ rescue Cocaine::CommandNotFoundError => e
81
+ raise Paperclip::CommandNotFoundError.new("Could not run the `convert` command. Please install ImageMagick.")
82
+ end
83
+
84
+ dst
85
+ end
86
+
87
+ # Returns the command ImageMagick's +convert+ needs to transform the image
88
+ # into the thumbnail.
89
+ def transformation_command
90
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
91
+ trans = []
92
+ trans << "-coalesce" if animated?
93
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
94
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
95
+ trans
96
+ end
97
+
98
+ protected
99
+
100
+ # Return true if the format is animated
101
+ def animated?
102
+ @animated && ANIMATED_FORMATS.include?(@current_format[1..-1]) && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,62 @@
1
+ require 'mime/types'
2
+
3
+ module Paperclip
4
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
5
+ # to the +File+ class. Useful for testing.
6
+ # user.avatar = File.new("test/test_avatar.jpg")
7
+ module Upfile
8
+
9
+ # Infer the MIME-type of the file from the extension.
10
+ def content_type
11
+ types = MIME::Types.type_for(self.original_filename)
12
+ if types.length == 0
13
+ type_from_file_command
14
+ elsif types.length == 1
15
+ types.first.content_type
16
+ else
17
+ iterate_over_array_to_find_best_option(types)
18
+ end
19
+ end
20
+
21
+ def iterate_over_array_to_find_best_option(types)
22
+ types.reject {|type| type.content_type.match(/\/x-/) }.first
23
+ end
24
+
25
+ def type_from_file_command
26
+ # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
27
+ type = (self.original_filename.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
28
+ mime_type = (Paperclip.run("file", "-b --mime :file", :file => self.path).split(/[:;]\s+/)[0] rescue "application/x-#{type}")
29
+ mime_type = "application/x-#{type}" if mime_type.match(/\(.*?\)/)
30
+ mime_type
31
+ end
32
+
33
+ # Returns the file's normal name.
34
+ def original_filename
35
+ File.basename(self.path)
36
+ end
37
+
38
+ # Returns the size of the file.
39
+ def size
40
+ File.size(self)
41
+ end
42
+ end
43
+ end
44
+
45
+ if defined? StringIO
46
+ class StringIO
47
+ attr_accessor :original_filename, :content_type, :fingerprint
48
+ def original_filename
49
+ @original_filename ||= "stringio.txt"
50
+ end
51
+ def content_type
52
+ @content_type ||= "text/plain"
53
+ end
54
+ def fingerprint
55
+ @fingerprint ||= Digest::MD5.hexdigest(self.string)
56
+ end
57
+ end
58
+ end
59
+
60
+ class File #:nodoc:
61
+ include Paperclip::Upfile
62
+ end
@@ -0,0 +1,64 @@
1
+ require 'uri'
2
+
3
+ module Paperclip
4
+ class UrlGenerator
5
+ def initialize(attachment, attachment_options)
6
+ @attachment = attachment
7
+ @attachment_options = attachment_options
8
+ end
9
+
10
+ def for(style_name, options)
11
+ escape_url_as_needed(
12
+ timestamp_as_needed(
13
+ @attachment_options[:interpolator].interpolate(most_appropriate_url, @attachment, style_name),
14
+ options
15
+ ), options)
16
+ end
17
+
18
+ private
19
+
20
+ # This method is all over the place.
21
+ def default_url
22
+ if @attachment_options[:default_url].respond_to?(:call)
23
+ @attachment_options[:default_url].call(@attachment)
24
+ elsif @attachment_options[:default_url].is_a?(Symbol)
25
+ @attachment.instance.send(@attachment_options[:default_url])
26
+ else
27
+ @attachment_options[:default_url]
28
+ end
29
+ end
30
+
31
+ def most_appropriate_url
32
+ if @attachment.original_filename.nil?
33
+ default_url
34
+ else
35
+ @attachment_options[:url]
36
+ end
37
+ end
38
+
39
+ def timestamp_as_needed(url, options)
40
+ if options[:timestamp] && timestamp_possible?
41
+ delimiter_char = url.match(/\?.+=/) ? '&' : '?'
42
+ "#{url}#{delimiter_char}#{@attachment.updated_at.to_s}"
43
+ else
44
+ url
45
+ end
46
+ end
47
+
48
+ def timestamp_possible?
49
+ @attachment.respond_to?(:updated_at) && @attachment.updated_at.present?
50
+ end
51
+
52
+ def escape_url_as_needed(url, options)
53
+ if options[:escape]
54
+ escape_url(url)
55
+ else
56
+ url
57
+ end
58
+ end
59
+
60
+ def escape_url(url)
61
+ (url.respond_to?(:escape) ? url.escape : URI.escape(url)).gsub(/(\/.+)\?(.+\.)/, '\1%3F\2')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Paperclip
2
+ VERSION = "2.4.6" unless defined? Paperclip::VERSION
3
+ end