Permalink
Browse files

Pass aditional parameters to S3 expiring urls

This adds functionality to add additional params to the querystring for
S3 expiring URLs. The reason for this is if you want to override
response_content_type or response_content_disposition with expiring
URLs, you have to change Amazon's signature, which gets signed before
adding additional options to the querystring. Because it's added later,
and because Amazon's signature includes the full URL, the signature is
bad and the request fails.

To use this feature:

    has_attached_file :avatar,
                      :s3_url_options => { :response_content_disposition => "inline" }

Additionally, you can pass a lambda and it'll be evaluated when the URL
is generated. If, for example, the content type is incorrect in Amazon
(either empty or application/octet-stream), you can effectively cast the
response from Amazon as a particular content type.

    has_attached_file :avatar,
                      :s3_url_options => lambda {|model| { :response_content_type => model.avatar_content_type } }
  • Loading branch information...
1 parent 4e4fa9d commit f7284b9e497a326a5f8e5c02716a560958644e5b @joshuaclayton joshuaclayton committed with sikachu Mar 2, 2012
Showing with 60 additions and 13 deletions.
  1. +8 −1 lib/paperclip/storage/s3.rb
  2. +52 −12 test/storage/s3_test.rb
@@ -141,7 +141,8 @@ def self.extended base
def expiring_url(time = 3600, style_name = default_style)
if path
- s3_object(style_name).url_for(:read, :expires => time, :secure => use_secure_protocol?(style_name)).to_s
+ base_options = { :expires => time, :secure => use_secure_protocol?(style_name) }
+ s3_object(style_name).url_for(:read, base_options.merge(s3_url_options)).to_s
end
end
@@ -159,6 +160,12 @@ def s3_host_alias
@s3_host_alias
end
+ def s3_url_options
+ s3_url_options = @options[:s3_url_options] || {}
+ s3_url_options = s3_url_options.call(instance) if s3_url_options.is_a?(Proc)
+ s3_url_options
+ end
+
def bucket_name
@bucket = @options[:bucket] || s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
View
@@ -350,16 +350,24 @@ def counter
end
context "Generating a secure url with an expiration" do
- setup do
- rebuild_model :storage => :s3,
- :s3_credentials => {
- :production => { :bucket => "prod_bucket" },
- :development => { :bucket => "dev_bucket" }
- },
- :s3_host_alias => "something.something.com",
- :s3_permissions => "private",
- :path => ":attachment/:basename.:extension",
- :url => ":s3_alias_url"
+ def build_model_with_options(options = {})
+ base_options = {
+ :storage => :s3,
+ :s3_credentials => {
+ :production => { :bucket => "prod_bucket" },
+ :development => { :bucket => "dev_bucket" }
+ },
+ :s3_host_alias => "something.something.com",
+ :s3_permissions => "private",
+ :path => ":attachment/:basename.:extension",
+ :url => ":s3_alias_url"
+ }
+
+ rebuild_model base_options.merge(options)
+ end
+
+ should "use default options" do
+ build_model_with_options
rails_env("production")
@@ -373,8 +381,40 @@ def counter
@dummy.avatar.expiring_url
end
- should "should succeed" do
- assert true
+ should "allow overriding s3_url_options" do
+ build_model_with_options :s3_url_options => { :response_content_disposition => "inline" }
+
+ rails_env("production")
+
+ @dummy = Dummy.new
+ @dummy.avatar = StringIO.new(".")
+
+ object = stub
+ @dummy.avatar.stubs(:s3_object).returns(object)
+ object.expects(:url_for).with(:read, :expires => 3600, :secure => true, :response_content_disposition => "inline")
+
+ @dummy.avatar.expiring_url
+ end
+
+ should "allow overriding s3_object options with a proc" do
+ build_model_with_options :s3_url_options => lambda {|attachment| { :response_content_type => attachment.avatar_content_type } }
+
+ rails_env("production")
+
+ @dummy = Dummy.new
+
+ @file = StringIO.new(".")
+ @file.stubs(:original_filename).returns("5k.png\n\n")
+ @file.stubs(:content_type).returns("image/png\n\n")
+ @file.stubs(:to_tempfile).returns(@file)
+
+ @dummy.avatar = @file
+
+ object = stub
+ @dummy.avatar.stubs(:s3_object).returns(object)
+ object.expects(:url_for).with(:read, :expires => 3600, :secure => true, :response_content_type => "image/png")
+
+ @dummy.avatar.expiring_url
end
end

0 comments on commit f7284b9

Please sign in to comment.