- 
      
 - 
        
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 | 
@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?
Curiously, Rails haven't put this solution into the master branch so far, which is essential for public assets.
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
    @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
  endHi @vbrazo
I have one question. Since this is for public assets, does it mean we need to maintain public access on our S3 buckets
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`
@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?