Serving local files while using an asset host
published 2007-10-25
NOTE: The svn repository referenced here is no longer running. Hopefully you're running an new enough version of Rails that this post isn't necessary.
So, you've set up an asset host for your Rails application, but you have some files that that you don't want to or can't put on your asset host.
As an example, Plot-O-Matic creates images (of graphs) on the fly. If I had just blithely switched over to using asset hosting, then you wouldn't see any changes when you updated a graph on Plot-O-Matic, as the Rails application only updates the graph image in its local public directory, and the asset host file remains unchanged.
You have a few options to fix this.
- You could, of course, just use straight HTML tags to get at your local files, but that's ~~kind of~~ just plain ugly.
- You could just create the files in a directory that's not in public. But then you can't let people link to them easily, and you're still not able to use asset tag helpers.
- Finally, you could create a asset tag helper that created local links. That's what I chose to do on Plot-O-Matic, and here's how I did it.
Getting started
This will only work with the latest changes to the asset host code. There are two ways you can get this.
Upgrade to Rails 2.0
For instructions, scroll to the bottom of this post.
Install the multiple_asset_hosts plugin
script/plugin install svn://svn.spattendesign.com/svn/plugins/multiple_asset_hosts
All this plugin does is monkeypatch in the multiple asset host functionality from Rails Edge.
Creating the helper
Let's take a look at how the standard image tag helper works. The
following code is from vendor/rails/actionpack/lib/action_view/helpers/asset_tag_helper.rb.
def image_tag(source, options = {}) options.symbolize_keys! options[:src] = image_path(source) options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize if options[:size] options[:width], options[:height] = options[:size].split("x") if options[:size] =~ %r{^\d+x\d+$} options.delete(:size) end end
The important part here is the line
options[:src] = image_path(source)
This is where the path is calculated. Let's take a closer look at the
image_path method
def image_path(source) unless (source.split("/").last || source).include?(".") || source.blank? ActiveSupport::Deprecation.warn( "You've called image_path with a source that doesn't include an extension. " + "In Rails 2.0, that will not result in .png automatically being appended. " + "So you should call image_path('#{source}.png') instead", caller ) end compute_public_path(source, 'images', 'png') end
OK, so all that that does is call compute_public_path.
compute_public_path is the part that has been updated to work with
multiple asset hosts:
def compute_public_path(source, dir, ext, include_host = true) source += ".#{ext}" if File.extname(source).blank? if source =~ %r{^[-a-z]+://} source else source = "/#{dir}/#{source}" unless source[0] == ?/ source = "#{@controller.request.relative_url_root}#{source}" rewrite_asset_path!(source) if include_host host = compute_asset_host(source) unless host.blank? or host =~ %r{^[-a-z]+://} host = "#{@controller.request.protocol}#{host}" end "#{host}#{source}" else source end end end
Aha! So, all we need to do to get local calls is make a call to
compute_public_path with include_host = false. Let's create a
local_image_tag helper that does just that. The following code should
reside in RAILS_ROOT/app/helpers/application_helper.rb
def local_image_tag(source, options = {}) options.symbolize_keys! options[:src] = local_image_path(source) options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize if options[:size] options[:width], options[:height] = options[:size].split("x") if options[:size] =~ %r{^\d+x\d+$} options.delete(:size) end tag("img", options) end # Returns the path to an image on the server, ignoring any asset hosts. # Other than that, this is equivalent to asset_tag_helper#image_path def local_image_path(source) unless (source.split("/").last || source).include?(".") || source.blank? ActiveSupport::Deprecation.warn( "You've called image_path with a source that doesn't include an extension. " + "In Rails 2.0, that will not result in .png automatically being appended. " + "So you should call image_path('#{source}.png') instead", caller ) end compute_public_path(source, 'images', 'png', false) end
This is too ugly. What we really want is to be able to create an image tag like this:
def local_image_tag(source, options = {}) image_tag(source, options.merge(:include_host => false)) end
to do this, you would change the image_tag code to something like this
def image_tag(source, options = {}) options.symbolize_keys! include_host = options.has_key?(:include_host) ? options[:include_host] : true options.delete(:include_host) options[:src] = image_path(source, include_host) options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize if options[:size] options[:width], options[:height] = options[:size].split("x") if options[:size] =~ %r{^\d+x\d+$} options.delete(:size) end tag("img", options) end
and the image_path code to
def image_path(source, include_host = true) unless (source.split("/").last || source).include?(".") || source.blank? ActiveSupport::Deprecation.warn( "You've called image_path with a source that doesn't include an extension. " + "In Rails 2.0, that will not result in .png automatically being appended. " + "So you should call image_path('#{source}.png') instead", caller ) end compute_public_path(source, 'images', 'png', include_host) end