Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active September 13, 2019 08:47
Show Gist options
  • Save palkan/e3a272f612d37b6e24aa1c66fe7cc3cb to your computer and use it in GitHub Desktop.
Save palkan/e3a272f612d37b6e24aa1c66fe7cc3cb to your computer and use it in GitHub Desktop.
active-storage-proxy

ActiveStorageProxy

Adds support for proxying Active Storage blobs (to serve them via CDN).

Based on rails/rails#34477.

Usage

To serve a blob via proxy use generate the URL using the following url helper

rails_proxy_blob_url(signed_id: attachment.signed_id, filename: attachment.filename)

# and for variants
variant = attachment.variant(:medium)
rails_proxy_blob_url(signed_id: attachment.signed_id, variation_key: variant.variation.key, filename: variant.filename)

Configuration

You can configure the cache headers:

# config/environments/production.rb

# Default values are
config.active_storage_proxy.proxy_urls_expire_in = 1.year
config.active_storage_proxy.proxy_urls_public = true
# frozen_string_literal: true
require "rails/engine"
require "active_storage/engine"
require_relative "./variant_download"
module ActiveStorageProxy
class Engine < ::Rails::Engine
isolate_namespace ActiveStorageProxy
engine_name "active_storage_proxy"
config.proxy_urls_expire_in = 1.year
config.proxy_urls_public = true
initializer "active_storage_proxy.routes" do |_app|
config.after_initialize do |app|
app.routes.prepend do
scope ActiveStorage.routes_prefix do
get "/proxy/:signed_id(/:variation_key)/*filename" => "active_storage/proxy#show", :as => :rails_proxy_blob
end
end
end
end
end
end
# frozen_string_literal: true
# Takes a blob using a signed reference and proxies the contents of the blob
# Based on https://github.com/rails/rails/pull/34477/files
class ActiveStorage::ProxyController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
expires_in(
ActiveStorageProxy::Engine.config.proxy_urls_expire_in,
public: ActiveStorageProxy::Engine.config.proxy_urls_public
)
@blob = @blob.representation(params[:variation_key]).processed if params[:variation_key]
response.headers["Content-Type"] = @blob.content_type
response.headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(
disposition: params[:disposition] || "inline",
filename: @blob.filename.sanitized
)
@blob.download do |chunk|
response.stream.write(chunk)
end
ensure
response.stream.close
end
end
# frozen_string_literal: true
module ActiveStorageProxy
# Adds `download` for variants
module VariantDownload
def download(&block)
service.download key, &block
end
end
end
ActiveSupport.on_load :active_storage_blob do
ActiveStorage::Variant.prepend(ActiveStorageProxy::VariantDownload)
end
# frozen_string_literal: true
module ActiveStorage
# Provides methods to generate `asset_host`-aware blob urls.
#
# When `asset_host` is present then generates a proxy URL.
# Otherwise uses a default URL (redirect).
#
# Also takes care of GIFs: skip variants generation and return originals.
#
# Example:
# # you can use it directly
# ActiveStorage::BlobUrlHelper.blob_url(user.avatar)
#
# # or include into controller and add a helper
# include ActiveStorage::BlobUrlHelper
# helper_method :blob_path
#
# # pass a variant as an arg to handle GIFs (skip transformation)
# ActiveStorage::BlobUrlHelper.blob_url(user.avatar, variant: :medium)
module BlobUrlHelper
module_function
def blob_url(attachment, variant: nil, **options)
attachment = attachment.variant(variant) if attachment.variable? && variant && attachment.content_type !~ /gif/
if Rails.application.config.action_controller.asset_host
Rails.application.routes.url_helpers.rails_proxy_blob_url(
signed_id: attachment.is_a?(::ActiveStorage::Variant) ? attachment.blob.signed_id : attachment.signed_id,
variation_key: attachment.is_a?(::ActiveStorage::Variant) ? attachment.variation.key : nil,
filename: attachment.filename,
**blob_url_options.merge(options)
)
else
Rails.application.routes.url_helpers.polymorphic_url(
attachment,
blob_url_options.merge(options)
)
end
end
# Returns a path to blob if no asset host provided.
# Otherwise returns an URL.
def blob_path(attachment, **options)
if Rails.application.config.action_controller.asset_host
blob_url(attachment, **options)
else
blob_url(attachment, **options, only_path: true)
end
end
def blob_url_options
{}.tap do |opts|
opts[:protocol] = Rails.application.config.force_ssl ? :https : :http
opts[:host] = Rails.application.config.action_controller.asset_host
end.compact
end
end
end
@vitobotta
Copy link

Hi! This seems what I'm looking for until Rails has something built in to allow for usa with CDNs. Question: which syntax should I use to generate a variant with e.g. resize_to_limit and similar? Thanks

@vitobotta
Copy link

vitobotta commented Sep 7, 2019

Another question: how do I specify the host? For example the app is at app.domain.com but I want to serve the assets from cdn.domain.com. Thanks

@palkan
Copy link
Author

palkan commented Sep 11, 2019

@vitobotta Added BlobUrlHelper which used to generate URLs for blobs and variants with a CDN host if any.
We used the same CDN for assets and uploads, so we relied on the presence of the Rails.application.config.action_controller.asset_host parameter.

@vitobotta
Copy link

@vitobotta Added BlobUrlHelper which used to generate URLs for blobs and variants with a CDN host if any.
We used the same CDN for assets and uploads, so we relied on the presence of the Rails.application.config.action_controller.asset_host parameter.

Hi, thanks for adding this. What about the variants and specifying the size etc? Thanks!

@palkan
Copy link
Author

palkan commented Sep 13, 2019

With this helper you can call: blob_url(attachment, variant: {resize_to_limit: [w, h]})

@vitobotta
Copy link

Perfect, thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment