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 @@ ...@@ -26,6 +26,7 @@
# See the +has_attached_file+ documentation for more details. # See the +has_attached_file+ documentation for more details.
require 'erb' require 'erb'
require 'digest'
require 'tempfile' require 'tempfile'
require 'paperclip/version' require 'paperclip/version'
require 'paperclip/upfile' require 'paperclip/upfile'
......
...@@ -15,6 +15,7 @@ module Paperclip ...@@ -15,6 +15,7 @@ module Paperclip
:default_url => "/:attachment/:style/missing.png", :default_url => "/:attachment/:style/missing.png",
:default_style => :original, :default_style => :original,
:storage => :filesystem, :storage => :filesystem,
:include_updated_timestamp => true,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails] :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
} }
end end
...@@ -39,6 +40,7 @@ module Paperclip ...@@ -39,6 +40,7 @@ module Paperclip
@default_url = options[:default_url] @default_url = options[:default_url]
@default_style = options[:default_style] @default_style = options[:default_style]
@storage = options[:storage] @storage = options[:storage]
@include_updated_timestamp = options[:include_updated_timestamp]
@whiny = options[:whiny_thumbnails] || options[:whiny] @whiny = options[:whiny_thumbnails] || options[:whiny]
@convert_options = options[:convert_options] @convert_options = options[:convert_options]
@processors = options[:processors] @processors = options[:processors]
...@@ -90,6 +92,7 @@ module Paperclip ...@@ -90,6 +92,7 @@ module Paperclip
instance_write(:file_name, uploaded_file.original_filename.strip) instance_write(:file_name, uploaded_file.original_filename.strip)
instance_write(:content_type, uploaded_file.content_type.to_s.strip) instance_write(:content_type, uploaded_file.content_type.to_s.strip)
instance_write(:file_size, uploaded_file.size.to_i) instance_write(:file_size, uploaded_file.size.to_i)
instance_write(:fingerprint, uploaded_file.fingerprint)
instance_write(:updated_at, Time.now) instance_write(:updated_at, Time.now)
@dirty = true @dirty = true
...@@ -98,6 +101,7 @@ module Paperclip ...@@ -98,6 +101,7 @@ module Paperclip
# Reset the file size if the original file was reprocessed. # Reset the file size if the original file was reprocessed.
instance_write(:file_size, @queued_for_write[:original].size.to_i) instance_write(:file_size, @queued_for_write[:original].size.to_i)
instance_write(:fingerprint, @queued_for_write[:original].fingerprint)
ensure ensure
uploaded_file.close if close_uploaded_file uploaded_file.close if close_uploaded_file
end end
...@@ -109,7 +113,7 @@ module Paperclip ...@@ -109,7 +113,7 @@ module Paperclip
# security, however, for performance reasons. set # security, however, for performance reasons. set
# include_updated_timestamp to false if you want to stop the attachment # include_updated_timestamp to false if you want to stop the attachment
# update time appended to the url # 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) 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 include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
end end
...@@ -174,6 +178,12 @@ module Paperclip ...@@ -174,6 +178,12 @@ module Paperclip
instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size) instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
end 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 # Returns the content_type of the file as originally assigned, and lives
# in the <attachment>_content_type attribute of the model. # in the <attachment>_content_type attribute of the model.
def content_type def content_type
......
...@@ -88,6 +88,11 @@ module Paperclip ...@@ -88,6 +88,11 @@ module Paperclip
attachment.instance.id attachment.instance.id
end 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 # Returns the id of the instance in a split path form. e.g. returns
# 000/001/234 for an id of 1234. # 000/001/234 for an id of 1234.
def id_partition attachment, style_name def id_partition attachment, style_name
......
...@@ -32,18 +32,26 @@ module Paperclip ...@@ -32,18 +32,26 @@ module Paperclip
def size def size
File.size(self) File.size(self)
end end
# Returns the hash of the file.
def fingerprint
Digest::MD5.hexdigest(self.read)
end
end end
end end
if defined? StringIO if defined? StringIO
class StringIO class StringIO
attr_accessor :original_filename, :content_type attr_accessor :original_filename, :content_type, :fingerprint
def original_filename def original_filename
@original_filename ||= "stringio.txt" @original_filename ||= "stringio.txt"
end end
def content_type def content_type
@content_type ||= "text/plain" @content_type ||= "text/plain"
end end
def fingerprint
@fingerprint ||= Digest::MD5.hexdigest(self.string)
end
end end
end end
......
...@@ -446,6 +446,8 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -446,6 +446,8 @@ class AttachmentTest < Test::Unit::TestCase
@not_file = mock @not_file = mock
@tempfile = mock @tempfile = mock
@not_file.stubs(:nil?).returns(false) @not_file.stubs(:nil?).returns(false)
@not_file.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
@tempfile.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
@not_file.expects(:size).returns(10) @not_file.expects(:size).returns(10)
@tempfile.expects(:size).returns(10) @tempfile.expects(:size).returns(10)
@not_file.expects(:to_tempfile).returns(@tempfile) @not_file.expects(:to_tempfile).returns(@tempfile)
...@@ -754,5 +756,29 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -754,5 +756,29 @@ class AttachmentTest < Test::Unit::TestCase
assert_equal @file.size, @dummy.avatar.size assert_equal @file.size, @dummy.avatar.size
end end
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
end end
...@@ -84,6 +84,7 @@ def rebuild_model options = {} ...@@ -84,6 +84,7 @@ def rebuild_model options = {}
table.column :avatar_content_type, :string table.column :avatar_content_type, :string
table.column :avatar_file_size, :integer table.column :avatar_file_size, :integer
table.column :avatar_updated_at, :datetime table.column :avatar_updated_at, :datetime
table.column :avatar_fingerprint, :string
end end
rebuild_class options rebuild_class options
end end
...@@ -103,6 +104,7 @@ class FakeModel ...@@ -103,6 +104,7 @@ class FakeModel
:avatar_file_size, :avatar_file_size,
:avatar_last_updated, :avatar_last_updated,
:avatar_content_type, :avatar_content_type,
:avatar_fingerprint,
:id :id
def errors 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