I wanted site admins for my Ruby on Rails application to be able to upload site assets such as logos and favicons in the GUI. In addition to being able to change assets from the front-end without redeployment, it is also nice to use ActiveStorage to process the image into the different sizes required instead of doing it manually. Since my application is Dockerized and uses AWS storage for its images, this did cause an annoying issue. There was a noticeable lag between loading one of the site’s pages and the logo image appearing. Plus, assets such as favicons and apple icons did not render at all.

I corrected this by creating subfolders under the public folder for each asset. I added a file called .keep under each directory so they would be tracked in git.

I added the paths to the images under the subfolders to .gitignore since these assets are intended to be dynamic and not under source control.

.gitignore

...
/public/logo/logo*
/public/favicon/favicon*
/public/apple_touch_icon/apple*
/public/default_avatar/default*
...

Instead of calling the ActiveStorage image variant for each asset in the layout, I call this helper method instead. It first looks for an image under the correct public subfolder that has a filename with the variant/size requested and the correct updated date for the site settings record. If it doesn’t find one, it will delete the old one with the incorrect site settings updated date, and then create the correct one and return that path.

Originally, I had the controller and model manage deleting and adding images to the public subfolders. But since this application is Dockerized, I needed to make sure all containers got updates to the assets as needed, and not just the container running the controller actions when an admin updates an asset.

app/helpers/application_helper.rb

def img_asset_path(image, size)
  object = SiteSetting.send("#{image}")
  file_ext = File.extname(object.blob.filename.to_s)
  asset_path = "#{image}/#{image}-" + size.to_s + "-" + SiteSetting.updated_at.to_i.to_s + file_ext
  path = Rails.root.join('public', asset_path).to_s
  if File.exist?(path)
    "/" + asset_path
  else
    begin
      dir_path = Rails.root.join("public/#{image}", "#{image}-#{size.to_s}-*" ).to_s
      FileUtils.rm_rf(Dir[dir_path])
      object.variant(size).processed
      File.open(path, 'wb') do |file|
        file.write(object.variant(size).download)
      end
      "/" + asset_path
    rescue
      object.variant(size)
    end
  end
end

def logo_navbar_path
  img_asset_path("logo", :navbar)
end

def favicon_path(size)
  img_asset_path("favicon", size)
end

def apple_icon_path(size)
  img_asset_path("apple_touch_icon", size)
end

def default_avatar_path(size)
  img_asset_path("default_avatar", size)
end

I do still have the controller actions clean out the subfolders when an asset is deleted, but that isn’t necessary. Each container will find or create the correct asset in the /public subfolders once a user visits a webpage and the layout is rendered. While this delete_default_avatar action would clear out the old images in the container that ran the action, it wouldn’t clear them out for other containers running the web service.

app/controllers/site_settings_controller.rb

...
def delete_default_avatar
  @siteSetting.default_avatar.purge if @siteSetting.default_avatar.attached?
  default_avatar_path = Rails.root.join('public/default_avatar', "default*" ).to_s
  FileUtils.rm_rf(Dir[default_avatar_path])
  respond_to do |format|
    format.html { redirect_to edit_site_setting_path(@siteSetting) }
    format.turbo_stream
  end
end
...

Before an asset is called through the helper method from the layout, the application first checks to see if there is an associated asset for that in the site settings. If not, it won’t render any old files that haven’t been deleted yet. If there is an associated asset, the helper method will check the updated date on the filename. If it doesn’t match the current site sittings, it will generate a correct one for that container.

app/layouts/application.html.erb

... <% if SiteSetting.favicon.attached? %> <link rel="icon" sizes="32x32" href="<%= favicon_path(:default) %>" /> <link rel="icon" sizes="192x192" href="<%= favicon_path(:large) %>" /> <% end %> <% if SiteSetting.apple_touch_icon.attached? %> <link rel="apple-touch-icon" sizes="180x180" href="<%= apple_icon_path(:default) %>" /> <link rel="apple-touch-icon-precomposed" sizes="144x144" href="<%= apple_icon_path(:one_hundred_forty_four) %>" /> <link rel="apple-touch-icon-precomposed" sizes="114x114" href="<%= apple_icon_path(:one_hundred_fourteen) %>" /> <link rel="apple-touch-icon-precomposed" sizes="72x72" href="<%= apple_icon_path(:seventy_two) %>" /> <link rel="apple-touch-icon-precomposed" sizes="57x57" href="<%= apple_icon_path(:fifty_seven) %>" /> <% end %> ...