Skip to content

Instantly share code, notes, and snippets.

@dinatih
Last active November 6, 2022 12:52
Show Gist options
  • 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
@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