Skip to content

Instantly share code, notes, and snippets.

@dinatih
Last active November 6, 2022 12:52
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dinatih/dbfdfd4e84faac4037448a06c9fdc016 to your computer and use it in GitHub Desktop.
Save dinatih/dbfdfd4e84faac4037448a06c9fdc016 to your computer and use it in GitHub Desktop.
# 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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

@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
Copy link

@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
Copy link

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

@vbrazo
Copy link

vbrazo commented Mar 10, 2021

Rails 6 proposed solution is throwing an error in Rails 6.1.3

NameError: undefined local variable or method `content_type_for_service_url' for #<ActiveStorage::Blob:0x...> Did you mean? content_type_for_serving

@8parth
Copy link

8parth commented Sep 6, 2021

@vbrazo I faced issue in rails 6.1 for above script and found one solution!

Seems like methods forced_disposition_for_service_url and content_type_for_service_url got changed with forced_disposition_for_serving and forced_disposition_for_serving respectively. So changing replacing both methods used inside ActiveStorage::Blob.class_eval do ... end block as below works:

  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_serving,
        disposition: forced_disposition_for_serving || disposition, **options
    end

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

@Grey-worm
Copy link

Grey-worm commented Sep 8, 2021

Hi @vbrazo

I have one question. Since this is for public assets, does it mean we need to maintain public access on our S3 buckets

@Lloyd66
Copy link

Lloyd66 commented Nov 25, 2021

When attaching a file generated via a tempfile, I had an issue (Rails 6.0) with build_after_unfurling method
"ArgumentError: wrong number of arguments (given 1, expected 0; required keywords: io, filename)"

Here is the corrected code :

when Hash
          ActiveStorage::Blob.build_after_unfurling \
            io: attachable[:io],
            filename: attachable[:filename],
            content_type: attachable[:content_type],
            metadata: { acl: acl }

Full code :

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 \
            io: attachable[:io],
            filename: attachable[:filename],
            content_type: attachable[:content_type],
            metadata: { acl: acl }
        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`

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