Last active
February 25, 2023 08:07
-
-
Save jordancrawfordnz/3b6aa87ff9e16931c951b0fc597feec8 to your computer and use it in GitHub Desktop.
ActiveStorage S3 - Cacheable direct presigned URLs by storing the issue time in a cookie
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# config/application.rb | |
... | |
config.active_storage.track_variants = true | |
config.active_storage.service_urls_expire_in = 7.days | |
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# config/storage.yml | |
# | |
# I was using Cloudflare R2 but this will equally work with S3. | |
cloudflare: | |
service: S3 | |
endpoint: <%= ENV["R2_ENDPOINT"] %> | |
access_key_id: <%= ENV["R2_ACCESS_KEY_ID"] %> | |
secret_access_key: <%= ENV["R2_ACCESS_KEY_SECRET"] %> | |
region: auto | |
bucket: <%= ENV["R2_BUCKET"] %> | |
upload: | |
cache_control: 'max-age=<%= 7.days %>, private' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/helpers/media_helper.rb | |
# | |
# Example of a helper which uses the presigned URLs | |
module MediaHelper | |
def direct_media_photo_url(photo:, variant:, options: {}) | |
variant = photo.variant(variant) | |
if variant.service.is_a?(ActiveStorage::Service::S3Service) | |
variant.url(**options.merge(time: fetch_and_update_s3_presigned_url_issue_time)) | |
else | |
# If we're using another service, fall back to the standard URL. | |
rails_representation_url(variant, **options) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/helpers/s3_presigned_url_issue_time_helper.rb | |
# | |
# S3 presigned URLs include the time they're issued. | |
# This means if we generate a presigned URL as part of a request it'll be a different | |
# URL each time, making it impossible for the browser to cache. | |
# | |
# By default Rails mitigates this issue with the rails_representation_url - a permanent path | |
# for the file which redirects to the presigned URL (and can be cached just like the S3 resource) | |
# | |
# However, this approach results in an additional web request for each file shown on the page. | |
# If there are lots of files, this can cause noticable server load and latency. | |
# | |
# This solution shares a consistent presigned issued at time for all requests (and persists it | |
# in a cookie). This means the file will use the same presigned URL for the entire period. | |
# | |
# The primary downside of this approach is that our entire cache will be busted at once, but the | |
# impact can be reduced by using a long service URL expiry time (e.g.: 7 days). | |
module S3PresignedUrlIssueTimeHelper | |
COOKIE_NAME = :s3_presignerd_url_issue_time | |
def fetch_and_update_s3_presigned_url_issue_time | |
current_issue_time = current_cookie_value | |
expired = current_issue_time.present? && (current_issue_time + service_url_expiry).past? | |
future = current_issue_time.present? && current_issue_time.future? | |
current_issue_time = Time.current if current_issue_time.nil? || expired || future | |
set_cookie_value!(current_issue_time) if current_cookie_value != current_issue_time | |
current_issue_time | |
end | |
private | |
def current_cookie_value | |
Time.parse(cookies[COOKIE_NAME]) if cookies[COOKIE_NAME].present? | |
end | |
def set_cookie_value!(value) | |
cookies[COOKIE_NAME] = { | |
value: value.to_s, | |
expires: value + service_url_expiry | |
} | |
end | |
def service_url_expiry | |
Rails.configuration.active_storage.service_urls_expire_in | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/views/example.html.erb | |
<%= image_tag direct_media_photo_url(photo: model.photo, variant: :small) %> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment