Permalink
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...
1 parent 16a926e commit 2cdeb39a514242df2fdc8a75e193dcecde5a74d5 @mbailey mbailey committed with jyurek Jun 10, 2010
Showing with 54 additions and 2 deletions.
  1. +1 −0 lib/paperclip.rb
  2. +11 −1 lib/paperclip/attachment.rb
  3. +5 −0 lib/paperclip/interpolations.rb
  4. +9 −1 lib/paperclip/upfile.rb
  5. +26 −0 test/attachment_test.rb
  6. +2 −0 test/helper.rb
View
@@ -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'
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

0 comments on commit 2cdeb39

Please sign in to comment.