Serving local files while using an asset host
published Oct 25, 2007
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 ofjust 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
endThe 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')
endOK, 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
endAha! 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
#Creates an image tag for a file on the server, ignoring any asset hosts.
#Other than that, this is equivalent and mostly stolen from asset_tag_helper#image_tag
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)
endThis 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))
endto 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)
endand 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