Auto Update Assets In Refinery
Refinery uses the Dragonfly gem to upload images and files. It's a really nice gem, but there is a problem when using it in RefineryCms. The problem is, that every time you update an image or file with Dragonfly, you get a new url to the asset. Dragonfly uses a job id in the asset url to encode the path to the image. So the image url may look like:
/system/resources/W1siZiIsIjIwMTQvMTEvMjYvMThfMTZfMjdfNzA4X3Rlc3QucGRmIl1d/test.pdf
and the path to the image is would look like
2014/11/26/18/16/27/708/test.pdf
, which you'll see is a directory path
of a timestamp.
So what's the problem?
If you link to this pdf in 10 pages of content, and later someone comes up with an edit to the pdf, you'll need to reupload the pdf, which will generate a new link to the pdf and break all of the links to the old version. This is really annoying because people like to update pdf's and images quite frequently and are used to updating the file or image without breaking the links.
How to fix?
Originally I tried to figure out a way to configure Dragonfly so that it would not regenerate the path or that maybe it would not use such a specific timestamp method of creating the directory. I could not figure this out. Plus, if do you that, then you still have an issue with caching, especially if you're using a CDN like CloudFront (note the remark about fingerprinting if you visit the link).
Then I had a thought. What if we just automated the updating of links each time an asset was updated? Well, I started exploring and it turns out it's pretty easy. Here's how it works.
First you need an after_update
hook for the Resource (files) and Image
models. Since these models are in the refinery gem, the best way is to
use a decorator:
# app/decorators/models/refinery/image_decorator.rb
Refinery::Image.class_eval do
include UpdateAssetReference
end
# app/decorators/models/refinery/resource_decorator.rb
Refinery::Resource.class_eval do
include UpdateAssetReference
end
I created a concern here so we're not duplicating the content in both models:
# app/models/concerns/update_asset_reference.rb
module UpdateAssetReference
extend ActiveSupport::Concern
included do
after_update :update_asset_references
end
def update_asset_references
AssetUrlReplacer.new(self).update_asset_references
end
end
In the concern, all we do is create the after_update
hook and then
pass in the instance of the file or image model to the AssetUrlReplacer,
which is where all the real work is done.
# app/models/asset_url_replacer.rb
require 'nokogiri'
class AssetUrlReplacer
attr_reader :asset
def initialize(asset)
@asset = asset
end
def asset_type
if asset.is_a?(Refinery::Image)
"image"
else
"file"
end
end
def asset_name
asset_type + "_name"
end
def references
Refinery::PagePart::Translation
.where("body like ?", "%#{asset.send(asset_name)}%")
end
def update_asset_references
references.each do |translation|
html = Nokogiri::HTML.fragment(translation.body)
replace_attributes(html, "a", :href)
replace_attributes(html, "img", :src)
translation.body = html.to_html
translation.save
end
end
def replace_attributes(html, tag, attribute)
if (tags = html.css(tag)).any?
tags.each do |element|
if element[attribute].include?(asset.send(asset_name))
&& element[attribute].include?("/system/")
element[attribute] = asset.send(asset_type).url
end
end
end
end
end
Since the resource and image models use different method names to get the image url, we have to do a little meta programming. Other than that, the logic is rather straight forward. First we search for any references to the asset's file name. Then we loop through those records and search for either links or images that are referencing the file name. If there's a match, we update the html and save the record. We use nokogiri to parse the html so we don't have
That's it. Now whenever a client updates their files or images, they will be replaced in the content if they are referneced there.
What do you think? See any issues??