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 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

#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)
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
blog comments powered by Disqus