Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
Added fingerprinting support
Browse files Browse the repository at this point in the history
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
mbailey authored and Jon Yurek committed Aug 16, 2010
1 parent 16a926e commit 2cdeb39
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/paperclip.rb
Expand Up @@ -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'
Expand Down
12 changes: 11 additions & 1 deletion lib/paperclip/attachment.rb
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/paperclip/interpolations.rb
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion lib/paperclip/upfile.rb
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions test/attachment_test.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions test/helper.rb
Expand Up @@ -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
Expand All @@ -103,6 +104,7 @@ class FakeModel
:avatar_file_size,
:avatar_last_updated,
:avatar_content_type,
:avatar_fingerprint,
:id

def errors
Expand Down

3 comments on commit 2cdeb39

@mlangenberg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow this commit is completely breaking image uploading for me. Maybe because I am using the S3 gem (not aws-s3, because of European bucket support) in combination with a custom Paperclip::Storage module. Although this commit doesn't make any changes in paperclip/storage/*.

@mlangenberg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was able to find the problem. The thing is, Upfile#fingerprint looks like a safe (idempotent) method, however, it is not. The method calls IO#read, without calling IO#rewind. Thus, any Paperclip::Storage backend must make sure to call call IO#rewind before reading the contents of the file. See http://github.com/qoobaa/s3/blob/master/extra/s3_paperclip.rb#L114 for an example of where things might go wrong.

@dolzenko
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 have the same problem with the custom code which does attachment.to_file.read (expecting it to return file contents of course). It would be nice if it cleans up with something like self.rewind

Please sign in to comment.