Commit 2cdeb39a by Mike Bailey Committed by Jon Yurek

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
parent 16a926ef
......@@ -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 @@ module Paperclip
: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 @@ module Paperclip
@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 @@ module Paperclip
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 @@ module Paperclip
# 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 @@ module Paperclip
# 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 @@ module Paperclip
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 @@ module Paperclip
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
......
......@@ -32,18 +32,26 @@ module Paperclip
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
......
......@@ -446,6 +446,8 @@ class AttachmentTest < Test::Unit::TestCase
@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 @@ class AttachmentTest < Test::Unit::TestCase
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
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment