Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
# config/initializers/activestorage.rb
Rails.application.config.to_prepare do
# Provides the class-level DSL for declaring that an Active Record model has attached blobs.
ActiveStorage::Attached::Macros.module_eval do
def has_one_attached(name, dependent: :purge_later, acl: :private)
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}, acl: "#{acl}")
end
def #{name}=(attachable)
#{name}.attach(attachable)
end
CODE
has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
if dependent == :purge_later
after_destroy_commit { public_send(name).purge_later }
else
before_destroy { public_send(name).detach }
end
end
def has_many_attached(name, dependent: :purge_later, acl: :private)
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}, acl: "#{acl}")
end
def #{name}=(attachables)
#{name}.attach(attachables)
end
CODE
has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
def purge
each(&:purge)
reset
end
def purge_later
each(&:purge_later)
reset
end
end
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
if dependent == :purge_later
after_destroy_commit { public_send(name).purge_later }
else
before_destroy { public_send(name).detach }
end
end
end
ActiveStorage::Blob.class_eval do
def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
filename = ActiveStorage::Filename.wrap(filename || self.filename)
expires_in = false if metadata[:acl] == 'public'
service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
end
def upload(io)
self.checksum = compute_checksum_in_chunks(io)
self.content_type = extract_content_type(io)
self.byte_size = io.size
self.identified = true
service.upload(key, io, checksum: checksum, acl: metadata[:acl])
end
end
ActiveStorage::Attached.class_eval do
attr_reader :name, :record, :dependent, :acl
def initialize(name, record, dependent:, acl: 'private')
@name, @record, @dependent, @acl = name, record, dependent, acl
end
private
def create_blob_from(attachable)
case attachable
when ActiveStorage::Blob
attachable
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
ActiveStorage::Blob.create_after_upload! \
io: attachable.open,
filename: attachable.original_filename,
content_type: attachable.content_type,
metadata: { acl: acl }
when Hash
ActiveStorage::Blob.create_after_upload!({ metadata: { acl: acl } }.deep_merge(attachable))
when String
ActiveStorage::Blob.find_signed(attachable)
else
nil
end
end
end
if defined?(ActiveStorage::Service)
ActiveStorage::Service.class_eval do
def upload(key, io, checksum: nil, acl: 'private')
raise NotImplementedError
end
end
end
if defined?(ActiveStorage::Service::DiskService)
ActiveStorage::Service::DiskService.class_eval do
def upload(key, io, checksum: nil, acl: 'private')
instrument :upload, key: key, checksum: checksum do
IO.copy_stream(io, make_path_for(key))
ensure_integrity_of(key, checksum) if checksum
end
end
end
end
if defined?(ActiveStorage::Service::S3Service)
# from activestorage/lib/active_storage/service/s3_service.rb
ActiveStorage::Service::S3Service.class_eval do
def upload(key, io, checksum: nil, acl: 'private')
instrument :upload, key: key, checksum: checksum, acl: acl do
begin
object_for(key).put(upload_options.merge(body: io, content_md5: checksum,
acl: acl == 'public' ? 'public-read' : 'private'))
rescue Aws::S3::Errors::BadDigest
raise ActiveStorage::IntegrityError
end
end
end
def url(key, expires_in:, filename:, disposition:, content_type:)
instrument :url, key: key, expires_in: expires_in do |payload|
generated_url = if expires_in == false
object_for(key).public_url
else
object_for(key).presigned_url :get,
expires_in: expires_in.to_i,
response_content_disposition: content_disposition_with(
type: disposition, filename: filename
),
response_content_type: content_type
end
payload[:url] = generated_url
generated_url
end
end
end
end
end
@lorn

This comment has been minimized.

Copy link

@lorn lorn commented Sep 5, 2018

Hi, thanks for the solution

what is the best way to use asset_host, config.action_controller.asset_host, to serve the image if its public?
My idea is similar to the CLOUDFRONT_URL env that was in the original issue, but since my cdn is already configured on asset_host - how can I serve the images using asset_host url?

I change some stuff on your file just to work, but it's not good enough for share :P

@TakuyaHarayama

This comment has been minimized.

Copy link

@TakuyaHarayama TakuyaHarayama commented Oct 12, 2018

Hi 😄
I just faced a problem that I could not cache in firebase storage GCS.
Your gist is very useful for me.
This is GCS version https://gist.github.com/TakuyaHarayama/3d05e5d6e11d23d410fd38b9f8a21d3a

@mengqing

This comment has been minimized.

Copy link

@mengqing mengqing commented Jun 11, 2019

Here is an updated version to support variants / previews

Rails.application.config.to_prepare do
  # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
  ActiveStorage::Attached::Macros.module_eval do
    def has_one_attached(name, dependent: :purge_later, acl: :private)
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}
          @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}, acl: "#{acl}")
        end
        def #{name}=(attachable)
          #{name}.attach(attachable)
        end
      CODE

      has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
      has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob

      scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }

      if dependent == :purge_later
        after_destroy_commit { public_send(name).purge_later }
      else
        before_destroy { public_send(name).detach }
      end
    end

    def has_many_attached(name, dependent: :purge_later, acl: :private)
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}
          @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}, acl: "#{acl}")
        end
        def #{name}=(attachables)
          #{name}.attach(attachables)
        end
      CODE

      has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
        def purge
          each(&:purge)
          reset
        end

        def purge_later
          each(&:purge_later)
          reset
        end
      end
      has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob

      scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }

      if dependent == :purge_later
        after_destroy_commit { public_send(name).purge_later }
      else
        before_destroy { public_send(name).detach }
      end
    end
  end

  ActiveStorage::Blob.class_eval do
    def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
      filename = ActiveStorage::Filename.wrap(filename || self.filename)
      expires_in = false if metadata[:acl] == 'public'
      service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
                       disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
    end

    def upload(io)
      self.checksum     = compute_checksum_in_chunks(io)
      self.content_type = extract_content_type(io)
      self.byte_size    = io.size
      self.identified   = true
      service.upload(key, io, checksum: checksum, acl: metadata[:acl])
    end
  end

  ActiveStorage::Attached.class_eval do
    attr_reader :name, :record, :dependent, :acl

    def initialize(name, record, dependent:, acl: 'private')
      @name, @record, @dependent, @acl = name, record, dependent, acl
    end

    private

      def create_blob_from(attachable)
        case attachable
        when ActiveStorage::Blob
          attachable
        when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
          ActiveStorage::Blob.create_after_upload! \
            io: attachable.open,
            filename: attachable.original_filename,
            content_type: attachable.content_type,
            metadata: { acl: acl }
        when Hash
          ActiveStorage::Blob.create_after_upload!({ metadata: { acl: acl } }.deep_merge(attachable))
        when String
          ActiveStorage::Blob.find_signed(attachable)
        else
          nil
        end
      end
  end

  if defined?(ActiveStorage::Service)
    ActiveStorage::Service.class_eval do
      def upload(key, io, checksum: nil, acl: 'private')
        raise NotImplementedError
      end
    end
  end

  ActiveStorage::Variant.class_eval do
    def service_url(expires_in: service.url_expires_in, disposition: :inline)
      metadata = blob.respond_to?(:record) ? blob.record.metadata : blob.metadata
      expires_in = false if metadata[:acl] == 'public'
      service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
    end

    private def upload(image)
      metadata = blob.respond_to?(:record) ? blob.record.metadata : blob.metadata
      File.open(image.path, "r") { |file| service.upload(key, file, acl: metadata[:acl]) }
    end
  end

  if defined?(ActiveStorage::Service::DiskService)
    ActiveStorage::Service::DiskService.class_eval do
      def upload(key, io, checksum: nil, acl: 'private')
        instrument :upload, key: key, checksum: checksum do
          IO.copy_stream(io, make_path_for(key))
          ensure_integrity_of(key, checksum) if checksum
        end
      end
    end
  end

  if defined?(ActiveStorage::Service::S3Service)
    # from activestorage/lib/active_storage/service/s3_service.rb
    ActiveStorage::Service::S3Service.class_eval do
      def upload(key, io, checksum: nil, acl: 'private')
        instrument :upload, key: key, checksum: checksum, acl: acl do
          begin
            object_for(key).put(upload_options.merge(body: io, content_md5: checksum,
                                                     acl: acl == 'public' ? 'public-read' : 'private'))
          rescue Aws::S3::Errors::BadDigest
            raise ActiveStorage::IntegrityError
          end
        end
      end

      def url(key, expires_in:, filename:, disposition:, content_type:)
        instrument :url, key: key, expires_in: expires_in do |payload|
          generated_url = if expires_in == false
                            object_for(key).public_url
                          else
                            object_for(key).presigned_url :get,
                                                          expires_in: expires_in.to_i,
                                                          response_content_disposition: content_disposition_with(
                                                            type: disposition, filename: filename
                                                          ),
                                                          response_content_type: content_type
                          end

          if (cloudfront_alias = Rails.application.credentials.dig(:s3, :cloudfront_alias)).present?
            uri = URI(generated_url)
            uri.host = cloudfront_alias
            generated_url = uri.to_s
          end

          payload[:url] = generated_url

          generated_url
        end
      end
    end
  end
end
@davidbasalla

This comment has been minimized.

Copy link

@davidbasalla davidbasalla commented Sep 4, 2019

I found this very useful for my Rails 5 app, so thank you! 🙏

You wouldn't happen to have an update of this for Rails 6? 🙂It looks like the underlying module changed from ActiveStorage::Attached::Macros to ActiveStorage::Attached::Model. I've been poking it a bit, but no luck so far...

@mengqing

This comment has been minimized.

Copy link

@mengqing mengqing commented Sep 12, 2019

Rails 6 version

Rails.application.config.to_prepare do
  # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
  ActiveStorage::Attached::Model.module_eval do
    class_methods do
      def has_one_attached(name, dependent: :purge_later, acl: :private)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
            @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, acl: "#{acl}")
          end
          def #{name}=(attachable)
            attachment_changes["#{name}"] =
              if attachable.nil?
                ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
              else
                ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable, acl: "#{acl}")
              end
          end
        CODE

        has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
        has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob

        scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }

        after_save { attachment_changes[name.to_s]&.save }

        after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }

        ActiveRecord::Reflection.add_attachment_reflection(
          self,
          name,
          ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
        )
      end

      def has_many_attached(name, dependent: :purge_later, acl: :private)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
            @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, acl: "#{acl}")
          end
          def #{name}=(attachables)
            if ActiveStorage.replace_on_assign_to_many
              attachment_changes["#{name}"] =
                if Array(attachables).none?
                  ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
                else
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables, acl: "#{acl}")
                end
            else
              if Array(attachables).any?
                attachment_changes["#{name}"] =
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables, acl: "#{acl}")
              end
            end
          end
        CODE

        has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do
          def purge
            each(&:purge)
            reset
          end

          def purge_later
            each(&:purge_later)
            reset
          end
        end
        has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob

        scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }

        after_save { attachment_changes[name.to_s]&.save }

        after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }

        ActiveRecord::Reflection.add_attachment_reflection(
          self,
          name,
          ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
        )
      end
    end
  end

  ActiveStorage::Blob.class_eval do
    def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
      filename = ActiveStorage::Filename.wrap(filename || self.filename)
      expires_in = false if metadata[:acl] == 'public'

      service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
        disposition: forced_disposition_for_service_url || disposition, **options
    end

    def upload_without_unfurling(io)
      service.upload key, io, checksum: checksum, **service_metadata.merge(acl: metadata[:acl])
    end
  end

  ActiveStorage::Attached::Changes::CreateOne.class_eval do
    attr_reader :name, :record, :attachable, :acl

    def initialize(name, record, attachable, acl: 'private')
      @name, @record, @attachable, @acl = name, record, attachable, acl
    end

    private def find_or_build_blob
        case attachable
        when ActiveStorage::Blob
          attachable
        when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
          ActiveStorage::Blob.build_after_unfurling \
            io: attachable.open,
            filename: attachable.original_filename,
            content_type: attachable.content_type,
            metadata: { acl: acl }
        when Hash
          ActiveStorage::Blob.build_after_unfurling({ metadata: { acl: acl } }.deep_merge(attachable))
        when String
          ActiveStorage::Blob.find_signed(attachable)
        else
          raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
        end
      end
  end

  ActiveStorage::Attached::Changes::CreateMany.class_eval do
    attr_reader :name, :record, :attachables, :acl

    def initialize(name, record, attachables, acl: 'private')
      @name, @record, @attachables, @acl = name, record, Array(attachables), acl
    end

    private def build_subchange_from(attachable)
      ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable, acl: acl)
    end
  end

  ActiveStorage::Attached.class_eval do
    attr_reader :name, :record, :acl

    def initialize(name, record, acl: 'private')
      @name, @record, @acl = name, record, acl
    end
  end

  if defined?(ActiveStorage::Service)
    ActiveStorage::Service.class_eval do
      def upload(key, io, checksum: nil, acl: 'private')
        raise NotImplementedError
      end
    end
  end

  ActiveStorage::Variant.class_eval do
    def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
      metadata = blob.respond_to?(:record) ? blob.record.metadata : blob.metadata
      expires_in = false if metadata[:acl] == 'public'
      service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
    end

    private def upload(image)
      metadata = blob.respond_to?(:record) ? blob.record.metadata : blob.metadata
      File.open(image.path, "r") { |file| service.upload(key, file, acl: metadata[:acl]) }
    end
  end

  if defined?(ActiveStorage::Service::DiskService)
    ActiveStorage::Service::DiskService.class_eval do
      def upload(key, io, checksum: nil, acl: 'private', **)
        instrument :upload, key: key, checksum: checksum do
          IO.copy_stream(io, make_path_for(key))
          ensure_integrity_of(key, checksum) if checksum
        end
      end
    end
  end

  if defined?(ActiveStorage::Service::S3Service)
    # from activestorage/lib/active_storage/service/s3_service.rb
    ActiveStorage::Service::S3Service.class_eval do
      def upload(key, io, checksum: nil, content_type: nil, acl: 'private', **)
        instrument :upload, key: key, checksum: checksum, acl: acl do
          begin
            object_for(key).put(upload_options.merge(body: io, content_md5: checksum,
                                                     acl: acl == 'public' ? 'public-read' : 'private'))
          rescue Aws::S3::Errors::BadDigest
            raise ActiveStorage::IntegrityError
          end
        end
      end

      def url(key, expires_in:, filename:, disposition:, content_type:)
        instrument :url, key: key, expires_in: expires_in do |payload|
          generated_url = if expires_in == false
                            object_for(key).public_url
                          else
                            object_for(key).presigned_url :get,
                                                          expires_in: expires_in.to_i,
                                                          response_content_disposition: content_disposition_with(
                                                            type: disposition, filename: filename
                                                          ),
                                                          response_content_type: content_type
                          end

          if (cloudfront_alias = Rails.application.credentials.dig(:aws, :cloudfront_alias)).present?
            uri = URI(generated_url)
            uri.host = cloudfront_alias
            generated_url = uri.to_s
          end

          payload[:url] = generated_url

          generated_url
        end
      end
    end
  end
end
@sarmadsaleem

This comment has been minimized.

Copy link

@sarmadsaleem sarmadsaleem commented Sep 16, 2019

Thanks for sharing this, it has been super useful.

@mengqing I was trying out the Rails 6 version just now which works as expected in development environment whereas fails with following error in test environment (same Active Storage configuration):

Failure/Error: has_many_attached :images, acl: :public

ArgumentError:
  unknown keyword: acl

Am I missing something?

@mengqing

This comment has been minimized.

Copy link

@mengqing mengqing commented Sep 16, 2019

@sarmadsaleem that error looks like the test environment isn't loading the override file. Where have you placed the file? Also, have you cleared cache / restarted spring?

@sarmadsaleem

This comment has been minimized.

Copy link

@sarmadsaleem sarmadsaleem commented Sep 16, 2019

@mengqing I have placed the file in config/initializers/active_storage.rb. Looks like if I explicitly load this file in config/environments/test.rb like so require File.expand_path('../initializers/active_storage.rb', __dir__), then it works otherwise I get unknown keyword errors.

Could it be that Rails.application.config.to_prepare only works in certain environments?

@EnziinSystem

This comment has been minimized.

Copy link

@EnziinSystem EnziinSystem commented Feb 5, 2020

Curiously, Rails haven't put this solution into the master branch so far, which is essential for public assets.

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