Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added fingerprinting support

Leverage browser caching and proxy caching by setting far future Expires
headers and changing filenames when file contents change. This can make
your web app faster for users and also reduce your bandwidth costs.

By adding the column :avatar_fingerprint to our db table and including
:fingerprint in the attachment filename, we ensure the filename will
change whenever the file contents do.

  has_attached_file :avatar,
    :styles => { :medium => "300x300>", :thumb => "100x100>" },
    :path => "users/:id/:attachment/:fingerprint-:style.:extension",
    :storage => :s3,
    :s3_headers => {'Expires' => 1.year.from_now.httpdate},
    :s3_credentials => "#{RAILS_ROOT}/config/s3.yml",
    :include_updated_timestamp => false

This enables us to set far future expire headers so that browsers
don't need to check for a newer version. If a change does occur,
say because a user uploads a new avatar, the new filename will
be rendered in your html and the cached version will be ignored.

The example above will set Expires headers in S3. If you're using
local storage you can configure your webserver to do something similar.

We disable the timestamped query string because some proxies refuse
to cache items with query strings.

For more info on optimizing for caching:

http://code.google.com/speed/page-speed/docs/caching.html
  • Loading branch information...
commit 2cdeb39a514242df2fdc8a75e193dcecde5a74d5 1 parent 16a926e
@mbailey mbailey authored jyurek committed
View
1  lib/paperclip.rb
@@ -26,6 +26,7 @@
# See the +has_attached_file+ documentation for more details.
require 'erb'
+require 'digest'
require 'tempfile'
require 'paperclip/version'
require 'paperclip/upfile'
View
12 lib/paperclip/attachment.rb
@@ -15,6 +15,7 @@ def self.default_options
:default_url => "/:attachment/:style/missing.png",
:default_style => :original,
:storage => :filesystem,
+ :include_updated_timestamp => true,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
}
end
@@ -39,6 +40,7 @@ def initialize name, instance, options = {}
@default_url = options[:default_url]
@default_style = options[:default_style]
@storage = options[:storage]
+ @include_updated_timestamp = options[:include_updated_timestamp]
@whiny = options[:whiny_thumbnails] || options[:whiny]
@convert_options = options[:convert_options]
@processors = options[:processors]
@@ -90,6 +92,7 @@ def assign uploaded_file
instance_write(:file_name, uploaded_file.original_filename.strip)
instance_write(:content_type, uploaded_file.content_type.to_s.strip)
instance_write(:file_size, uploaded_file.size.to_i)
+ instance_write(:fingerprint, uploaded_file.fingerprint)
instance_write(:updated_at, Time.now)
@dirty = true
@@ -98,6 +101,7 @@ def assign uploaded_file
# Reset the file size if the original file was reprocessed.
instance_write(:file_size, @queued_for_write[:original].size.to_i)
+ instance_write(:fingerprint, @queued_for_write[:original].fingerprint)
ensure
uploaded_file.close if close_uploaded_file
end
@@ -109,7 +113,7 @@ def assign uploaded_file
# security, however, for performance reasons. set
# include_updated_timestamp to false if you want to stop the attachment
# update time appended to the url
- def url style_name = default_style, include_updated_timestamp = true
+ def url(style_name = default_style, include_updated_timestamp = @include_updated_timestamp)
url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name)
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
end
@@ -174,6 +178,12 @@ def size
instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
end
+ # Returns the hash of the file as originally assigned, and lives in the
+ # <attachment>_fingerprint attribute of the model.
+ def fingerprint
+ instance_read(:fingerprint) || (@queued_for_write[:original] && @queued_for_write[:original].fingerprint)
+ end
+
# Returns the content_type of the file as originally assigned, and lives
# in the <attachment>_content_type attribute of the model.
def content_type
View
5 lib/paperclip/interpolations.rb
@@ -88,6 +88,11 @@ def id attachment, style_name
attachment.instance.id
end
+ # Returns the fingerprint of the instance.
+ def fingerprint attachment, style_name
+ attachment.fingerprint
+ end
+
# Returns the id of the instance in a split path form. e.g. returns
# 000/001/234 for an id of 1234.
def id_partition attachment, style_name
View
10 lib/paperclip/upfile.rb
@@ -32,18 +32,26 @@ def original_filename
def size
File.size(self)
end
+
+ # Returns the hash of the file.
+ def fingerprint
+ Digest::MD5.hexdigest(self.read)
+ end
end
end
if defined? StringIO
class StringIO
- attr_accessor :original_filename, :content_type
+ attr_accessor :original_filename, :content_type, :fingerprint
def original_filename
@original_filename ||= "stringio.txt"
end
def content_type
@content_type ||= "text/plain"
end
+ def fingerprint
+ @fingerprint ||= Digest::MD5.hexdigest(self.string)
+ end
end
end
View
26 test/attachment_test.rb
@@ -446,6 +446,8 @@ def do_after_all; end
@not_file = mock
@tempfile = mock
@not_file.stubs(:nil?).returns(false)
+ @not_file.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
+ @tempfile.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
@not_file.expects(:size).returns(10)
@tempfile.expects(:size).returns(10)
@not_file.expects(:to_tempfile).returns(@tempfile)
@@ -754,5 +756,29 @@ def do_after_all; end
assert_equal @file.size, @dummy.avatar.size
end
end
+
+ context "and avatar_fingerprint column" do
+ setup do
+ ActiveRecord::Base.connection.add_column :dummies, :avatar_fingerprint, :string
+ rebuild_class
+ @dummy = Dummy.new
+ end
+
+ should "not error when assigned an attachment" do
+ assert_nothing_raised { @dummy.avatar = @file }
+ end
+
+ should "return the right value when sent #avatar_fingerprint" do
+ @dummy.avatar = @file
+ assert_equal 'aec488126c3b33c08a10c3fa303acf27', @dummy.avatar_fingerprint
+ end
+
+ should "return the right value when saved, reloaded, and sent #avatar_fingerprint" do
+ @dummy.avatar = @file
+ @dummy.save
+ @dummy = Dummy.find(@dummy.id)
+ assert_equal 'aec488126c3b33c08a10c3fa303acf27', @dummy.avatar_fingerprint
+ end
+ end
end
end
View
2  test/helper.rb
@@ -84,6 +84,7 @@ def rebuild_model options = {}
table.column :avatar_content_type, :string
table.column :avatar_file_size, :integer
table.column :avatar_updated_at, :datetime
+ table.column :avatar_fingerprint, :string
end
rebuild_class options
end
@@ -103,6 +104,7 @@ class FakeModel
:avatar_file_size,
:avatar_last_updated,
:avatar_content_type,
+ :avatar_fingerprint,
:id
def errors
Please sign in to comment.
Something went wrong with that request. Please try again.