Skip to content

Instantly share code, notes, and snippets.

@beniutek
Last active November 1, 2023 17:23
Show Gist options
  • Save beniutek/6681af441cf0eccd53bd6b220e3b95f6 to your computer and use it in GitHub Desktop.
Save beniutek/6681af441cf0eccd53bd6b220e3b95f6 to your computer and use it in GitHub Desktop.
Active Storage Direct Upload with multiple services
# This is taken from: https://github.com/rails/rails/blob/main/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
# Have to override this controller because it's actually the easiest way (IMO)
# to let us choose which service to use for a blob that doesn't use
# the default rails active storage service
# place it in the app/controllers/active_storage folder
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
# by default, active storage doesn't do any authorization, we choose to do it on our own via devise `authenticate_user!`
# we also get `current_user` thanks to devise
before_action :authenticate_user!
def create
# apart from the usual blob_args, we expect to receive a :service_name and :prefix in the request
result = DirectUploadsService.new(blob_args, current_user)
.create_before_direct_upload(params[:service_name], params[:prefix])
render json: result.as_json, status: result.created? ? :ok : :forbidden
end
private
def blob_args
params.require(:blob)
.permit(:filename, :byte_size, :checksum, :content_type, metadata: {})
.to_h
.symbolize_keys
end
end
# that is just a class that contains all the upload and generate url logic so that it is not inside the controller
class DirectUploadsService < ApplicationService
attr_reader :params, :user
def initialize(params, user)
@params = params
@user = user
end
def create_before_direct_upload(service_name = nil, prefix = nil)
case prefix
when user.prefix, nil
# check https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob.rb#L115
# to learn more about this method
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args(service_name, prefix))
Result.new(blob, created: true)
else
Result.new({}, created: false)
end
end
private
def blob_args(service_name, prefix)
params[:service_name] = service_name if service_name
params[:key] = blob_key(prefix)
params
end
# in addition to custom s3 service used for user avatars, we want to have them in certain folders,
# that's what prefxi is used for
def blob_key(prefix)
key = ActiveStorage::Blob.new.key
prefix ? "#{prefix}/#{key}" : key
end
class Result
attr_reader :blob, :created
def initialize(blob, created:)
@blob = blob
@created = created
end
def created?
@created
end
def as_json
if created?
blob.as_json(root: false, methods: :signed_id)
.merge(direct_upload: {
url: blob.service_url_for_direct_upload,
headers: blob.service_headers_for_direct_upload
})
else
{}
end
end
end
end
# example usage of the image upload, in this case User has one Avatar and a file is attached to the Avatar model.
<%= form_with model: [@user, @avatar] do |form| %>
<%=
form.file_field :file,
accept: 'image/jpeg',
direct_upload: true,
direct_upload_url:
rails_direct_uploads_url(
service_name: Rails.configuration.settings[:custom_s3_service_name],
prefix: @user.prefix
),
multipart: true
%>
<%= form.submit "submit" %>
<% end %>
# for convenience we want to add the ability to still use the `file_filed` in the `erb` files
# but we want to pass custom url for direct uploads that will contain the service_name and prefix as query params
# if we don't use form helpers then that is not needed
# you can see rails implementation of it here: https://github.com/rails/rails/blob/24c8eb5dd1038df41ce84cd9f1acb965852ea449/actionview/lib/action_view/helpers/form_tag_helper.rb#L914
ActionView::Helpers::FormTagHelper.class_eval do
def convert_direct_upload_option_to_url(options)
if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
options['data-direct-upload-url'] = options.delete(:direct_upload_url) || rails_direct_uploads_url
end
options
end
end
@beniutek
Copy link
Author

That's my idea of how to use multiple active storage services with direct upload in Ruby on Rails.
Currently the issue is that Active Storage doesn't fully support that, see this SO question

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