diff options
| -rw-r--r-- | activestorage/lib/active_storage/attached.rb | 46 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/attached/macros.rb | 144 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/attached/many.rb | 85 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/attached/one.rb | 96 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/log_subscriber.rb | 70 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service.rb | 195 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/azure_storage_service.rb | 168 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/configurator.rb | 44 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/disk_service.rb | 187 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/gcs_service.rb | 112 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/mirror_service.rb | 74 | ||||
| -rw-r--r-- | activestorage/lib/active_storage/service/s3_service.rb | 134 | 
12 files changed, 690 insertions, 665 deletions
| diff --git a/activestorage/lib/active_storage/attached.rb b/activestorage/lib/active_storage/attached.rb index 2dbf841864..5ac8ba5377 100644 --- a/activestorage/lib/active_storage/attached.rb +++ b/activestorage/lib/active_storage/attached.rb @@ -2,33 +2,35 @@ require "action_dispatch"  require "action_dispatch/http/upload"  require "active_support/core_ext/module/delegation" +module ActiveStorage  # Abstract baseclass for the concrete `ActiveStorage::Attached::One` and `ActiveStorage::Attached::Many`  # classes that both provide proxy access to the blob association for a record. -class ActiveStorage::Attached -  attr_reader :name, :record +  class Attached +    attr_reader :name, :record -  def initialize(name, record) -    @name, @record = name, record -  end +    def initialize(name, record) +      @name, @record = name, record +    end -  private -    def create_blob_from(attachable) -      case attachable -      when ActiveStorage::Blob -        attachable -      when ActionDispatch::Http::UploadedFile -        ActiveStorage::Blob.create_after_upload! \ -          io: attachable.open, -          filename: attachable.original_filename, -          content_type: attachable.content_type -      when Hash -        ActiveStorage::Blob.create_after_upload!(attachable) -      when String -        ActiveStorage::Blob.find_signed(attachable) -      else -        nil +    private +      def create_blob_from(attachable) +        case attachable +        when ActiveStorage::Blob +          attachable +        when ActionDispatch::Http::UploadedFile +          ActiveStorage::Blob.create_after_upload! \ +            io: attachable.open, +            filename: attachable.original_filename, +            content_type: attachable.content_type +        when Hash +          ActiveStorage::Blob.create_after_upload!(attachable) +        when String +          ActiveStorage::Blob.find_signed(attachable) +        else +          nil +        end        end -    end +  end  end  require "active_storage/attached/one" diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb index f09f3e1f6d..5779348148 100644 --- a/activestorage/lib/active_storage/attached/macros.rb +++ b/activestorage/lib/active_storage/attached/macros.rb @@ -1,82 +1,84 @@ -# Provides the class-level DSL for declaring that an Active Record model has attached blobs. -module ActiveStorage::Attached::Macros -  # Specifies the relation between a single attachment and the model. -  # -  #   class User < ActiveRecord::Base -  #     has_one_attached :avatar -  #   end -  # -  # There is no column defined on the model side, Active Storage takes -  # care of the mapping between your records and the attachment. -  # -  # Under the covers, this relationship is implemented as a `has_one` association to a -  # `ActiveStorage::Attachment` record and a `has_one-through` association to a -  # `ActiveStorage::Blob` record. These associations are available as `avatar_attachment` -  # and `avatar_blob`. But you shouldn't need to work with these associations directly in -  # most circumstances. -  # -  # The system has been designed to having you go through the `ActiveStorage::Attached::One` -  # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. -  # -  # If the +:dependent+ option isn't set, the attachment will be purged -  # (i.e. destroyed) whenever the record is destroyed. -  def has_one_attached(name, dependent: :purge_later) -    define_method(name) do -      if instance_variable_defined?("@active_storage_attached_#{name}") -        instance_variable_get("@active_storage_attached_#{name}") -      else -        instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self)) +module ActiveStorage +  # Provides the class-level DSL for declaring that an Active Record model has attached blobs. +  module Attached::Macros +    # Specifies the relation between a single attachment and the model. +    # +    #   class User < ActiveRecord::Base +    #     has_one_attached :avatar +    #   end +    # +    # There is no column defined on the model side, Active Storage takes +    # care of the mapping between your records and the attachment. +    # +    # Under the covers, this relationship is implemented as a `has_one` association to a +    # `ActiveStorage::Attachment` record and a `has_one-through` association to a +    # `ActiveStorage::Blob` record. These associations are available as `avatar_attachment` +    # and `avatar_blob`. But you shouldn't need to work with these associations directly in +    # most circumstances. +    # +    # The system has been designed to having you go through the `ActiveStorage::Attached::One` +    # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. +    # +    # If the +:dependent+ option isn't set, the attachment will be purged +    # (i.e. destroyed) whenever the record is destroyed. +    def has_one_attached(name, dependent: :purge_later) +      define_method(name) do +        if instance_variable_defined?("@active_storage_attached_#{name}") +          instance_variable_get("@active_storage_attached_#{name}") +        else +          instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self)) +        end        end -    end -    has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record -    has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob +      has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record +      has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob -    if dependent == :purge_later -      before_destroy { public_send(name).purge_later } +      if dependent == :purge_later +        before_destroy { public_send(name).purge_later } +      end      end -  end -  # Specifies the relation between multiple attachments and the model. -  # -  #   class Gallery < ActiveRecord::Base -  #     has_many_attached :photos -  #   end -  # -  # There are no columns defined on the model side, Active Storage takes -  # care of the mapping between your records and the attachments. -  # -  # To avoid N+1 queries, you can include the attached blobs in your query like so: -  # -  #   Gallery.where(user: Current.user).with_attached_photos -  # -  # Under the covers, this relationship is implemented as a `has_many` association to a -  # `ActiveStorage::Attachment` record and a `has_many-through` association to a -  # `ActiveStorage::Blob` record. These associations are available as `photos_attachments` -  # and `photos_blobs`. But you shouldn't need to work with these associations directly in -  # most circumstances. -  # -  # The system has been designed to having you go through the `ActiveStorage::Attached::Many` -  # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. -  # -  # If the +:dependent+ option isn't set, all the attachments will be purged -  # (i.e. destroyed) whenever the record is destroyed. -  def has_many_attached(name, dependent: :purge_later) -    define_method(name) do -      if instance_variable_defined?("@active_storage_attached_#{name}") -        instance_variable_get("@active_storage_attached_#{name}") -      else -        instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self)) +    # Specifies the relation between multiple attachments and the model. +    # +    #   class Gallery < ActiveRecord::Base +    #     has_many_attached :photos +    #   end +    # +    # There are no columns defined on the model side, Active Storage takes +    # care of the mapping between your records and the attachments. +    # +    # To avoid N+1 queries, you can include the attached blobs in your query like so: +    # +    #   Gallery.where(user: Current.user).with_attached_photos +    # +    # Under the covers, this relationship is implemented as a `has_many` association to a +    # `ActiveStorage::Attachment` record and a `has_many-through` association to a +    # `ActiveStorage::Blob` record. These associations are available as `photos_attachments` +    # and `photos_blobs`. But you shouldn't need to work with these associations directly in +    # most circumstances. +    # +    # The system has been designed to having you go through the `ActiveStorage::Attached::Many` +    # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. +    # +    # If the +:dependent+ option isn't set, all the attachments will be purged +    # (i.e. destroyed) whenever the record is destroyed. +    def has_many_attached(name, dependent: :purge_later) +      define_method(name) do +        if instance_variable_defined?("@active_storage_attached_#{name}") +          instance_variable_get("@active_storage_attached_#{name}") +        else +          instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self)) +        end        end -    end -    has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" -    has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob +      has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" +      has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob -    scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } +      scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } -    if dependent == :purge_later -      before_destroy { public_send(name).purge_later } +      if dependent == :purge_later +        before_destroy { public_send(name).purge_later } +      end      end    end -end +end
\ No newline at end of file diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 035cd9c091..82989e4605 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -1,51 +1,54 @@ -# Decorated proxy object representing of multiple attachments to a model. -class ActiveStorage::Attached::Many < ActiveStorage::Attached -  delegate_missing_to :attachments +module ActiveStorage +  # Decorated proxy object representing of multiple attachments to a model. +  class Attached::Many < Attached +    delegate_missing_to :attachments -  # Returns all the associated attachment records. -  # -  # All methods called on this proxy object that aren't listed here will automatically be delegated to `attachments`. -  def attachments -    record.public_send("#{name}_attachments") -  end +    # Returns all the associated attachment records. +    # +    # All methods called on this proxy object that aren't listed here will automatically be delegated to `attachments`. +    def attachments +      record.public_send("#{name}_attachments") +    end -  # Associates one or several attachments with the current record, saving them to the database. -  # Examples: -  # -  #   document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects -  #   document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload -  #   document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") -  #   document.images.attach([ first_blob, second_blob ]) -  def attach(*attachables) -    attachables.flatten.collect do |attachable| -      attachments.create!(name: name, blob: create_blob_from(attachable)) +    # Associates one or several attachments with the current record, saving them to the database. +    # Examples: +    # +    #   document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects +    #   document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload +    #   document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") +    #   document.images.attach([ first_blob, second_blob ]) +    def attach(*attachables) +      attachables.flatten.collect do |attachable| +        attachments.create!(name: name, blob: create_blob_from(attachable)) +      end      end -  end -  # Returns true if any attachments has been made. -  # -  #   class Gallery < ActiveRecord::Base -  #     has_many_attached :photos -  #   end -  # -  #   Gallery.new.photos.attached? # => false -  def attached? -    attachments.any? -  end +    # Returns true if any attachments has been made. +    # +    #   class Gallery < ActiveRecord::Base +    #     has_many_attached :photos +    #   end +    # +    #   Gallery.new.photos.attached? # => false +    def attached? +      attachments.any? +    end -  # Directly purges each associated attachment (i.e. destroys the blobs and -  # attachments and deletes the files on the service). -  def purge -    if attached? -      attachments.each(&:purge) -      attachments.reload +    # Directly purges each associated attachment (i.e. destroys the blobs and +    # attachments and deletes the files on the service). +    def purge +      if attached? +        attachments.each(&:purge) +        attachments.reload +      end      end -  end -  # Purges each associated attachment through the queuing system. -  def purge_later -    if attached? -      attachments.each(&:purge_later) +    # Purges each associated attachment through the queuing system. +    def purge_later +      if attached? +        attachments.each(&:purge_later) +      end      end    end  end + diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index 0c522e856e..6b34b30f1c 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -1,56 +1,58 @@ -# Representation of a single attachment to a model. -class ActiveStorage::Attached::One < ActiveStorage::Attached -  delegate_missing_to :attachment +module ActiveStorage +  # Representation of a single attachment to a model. +  class Attached::One < Attached +    delegate_missing_to :attachment -  # Returns the associated attachment record. -  # -  # You don't have to call this method to access the attachment's methods as -  # they are all available at the model level. -  def attachment -    record.public_send("#{name}_attachment") -  end - -  # Associates a given attachment with the current record, saving it to the database. -  # Examples: -  # -  #   person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object -  #   person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload -  #   person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg") -  #   person.avatar.attach(avatar_blob) # ActiveStorage::Blob object -  def attach(attachable) -    write_attachment \ -      ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable)) -  end +    # Returns the associated attachment record. +    # +    # You don't have to call this method to access the attachment's methods as +    # they are all available at the model level. +    def attachment +      record.public_send("#{name}_attachment") +    end -  # Returns true if an attachment has been made. -  # -  #   class User < ActiveRecord::Base -  #     has_one_attached :avatar -  #   end -  # -  #   User.new.avatar.attached? # => false -  def attached? -    attachment.present? -  end +    # Associates a given attachment with the current record, saving it to the database. +    # Examples: +    # +    #   person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object +    #   person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload +    #   person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg") +    #   person.avatar.attach(avatar_blob) # ActiveStorage::Blob object +    def attach(attachable) +      write_attachment \ +        ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable)) +    end -  # Directly purges the attachment (i.e. destroys the blob and -  # attachment and deletes the file on the service). -  def purge -    if attached? -      attachment.purge -      write_attachment nil +    # Returns true if an attachment has been made. +    # +    #   class User < ActiveRecord::Base +    #     has_one_attached :avatar +    #   end +    # +    #   User.new.avatar.attached? # => false +    def attached? +      attachment.present?      end -  end -  # Purges the attachment through the queuing system. -  def purge_later -    if attached? -      attachment.purge_later +    # Directly purges the attachment (i.e. destroys the blob and +    # attachment and deletes the file on the service). +    def purge +      if attached? +        attachment.purge +        write_attachment nil +      end      end -  end -  private -    def write_attachment(attachment) -      record.public_send("#{name}_attachment=", attachment) +    # Purges the attachment through the queuing system. +    def purge_later +      if attached? +        attachment.purge_later +      end      end + +    private +      def write_attachment(attachment) +        record.public_send("#{name}_attachment=", attachment) +      end +  end  end diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb index 4ac34a3b25..5c1b8d23ef 100644 --- a/activestorage/lib/active_storage/log_subscriber.rb +++ b/activestorage/lib/active_storage/log_subscriber.rb @@ -1,48 +1,50 @@  require "active_support/log_subscriber" -class ActiveStorage::LogSubscriber < ActiveSupport::LogSubscriber -  def service_upload(event) -    message = "Uploaded file to key: #{key_in(event)}" -    message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum] -    info event, color(message, GREEN) -  end - -  def service_download(event) -    info event, color("Downloaded file from key: #{key_in(event)}", BLUE) -  end - -  def service_delete(event) -    info event, color("Deleted file from key: #{key_in(event)}", RED) -  end - -  def service_exist(event) -    debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE) -  end - -  def service_url(event) -    debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE) -  end +module ActiveStorage +  class LogSubscriber < ActiveSupport::LogSubscriber +    def service_upload(event) +      message = "Uploaded file to key: #{key_in(event)}" +      message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum] +      info event, color(message, GREEN) +    end -  def logger -    ActiveStorage::Service.logger -  end +    def service_download(event) +      info event, color("Downloaded file from key: #{key_in(event)}", BLUE) +    end -  private -    def info(event, colored_message) -      super log_prefix_for_service(event) + colored_message +    def service_delete(event) +      info event, color("Deleted file from key: #{key_in(event)}", RED)      end -    def debug(event, colored_message) -      super log_prefix_for_service(event) + colored_message +    def service_exist(event) +      debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)      end -    def log_prefix_for_service(event) -      color "  #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN +    def service_url(event) +      debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)      end -    def key_in(event) -      event.payload[:key] +    def logger +      ActiveStorage::Service.logger      end + +    private +      def info(event, colored_message) +        super log_prefix_for_service(event) + colored_message +      end + +      def debug(event, colored_message) +        super log_prefix_for_service(event) + colored_message +      end + +      def log_prefix_for_service(event) +        color "  #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN +      end + +      def key_in(event) +        event.payload[:key] +      end +  end  end  ActiveStorage::LogSubscriber.attach_to :active_storage diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index 4223295ed8..eb25e9f001 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -1,114 +1,115 @@  require "active_storage/log_subscriber" -# Abstract class serving as an interface for concrete services. -# -# The available services are: -# -# * +Disk+, to manage attachments saved directly on the hard drive. -# * +GCS+, to manage attachments through Google Cloud Storage. -# * +S3+, to manage attachments through Amazon S3. -# * +AzureStorage+, to manage attachments through Microsoft Azure Storage. -# * +Mirror+, to be able to use several services to manage attachments. -# -# Inside a Rails application, you can set-up your services through the -# generated <tt>config/storage.yml</tt> file and reference one -# of the aforementioned constant under the +service+ key. For example: -# -#   local: -#     service: Disk -#     root: <%= Rails.root.join("storage") %> -# -# You can checkout the service's constructor to know which keys are required. -# -# Then, in your application's configuration, you can specify the service to -# use like this: -# -#   config.active_storage.service = :local -# -# If you are using Active Storage outside of a Ruby on Rails application, you -# can configure the service to use like this: -# -#   ActiveStorage::Blob.service = ActiveStorage::Service.configure( -#     :Disk, -#     root: Pathname("/foo/bar/storage") -#   ) -class ActiveStorage::Service -  class ActiveStorage::IntegrityError < StandardError; end +module ActiveStorage +  class IntegrityError < StandardError; end +  # Abstract class serving as an interface for concrete services. +  # +  # The available services are: +  # +  # * +Disk+, to manage attachments saved directly on the hard drive. +  # * +GCS+, to manage attachments through Google Cloud Storage. +  # * +S3+, to manage attachments through Amazon S3. +  # * +AzureStorage+, to manage attachments through Microsoft Azure Storage. +  # * +Mirror+, to be able to use several services to manage attachments. +  # +  # Inside a Rails application, you can set-up your services through the +  # generated <tt>config/storage.yml</tt> file and reference one +  # of the aforementioned constant under the +service+ key. For example: +  # +  #   local: +  #     service: Disk +  #     root: <%= Rails.root.join("storage") %> +  # +  # You can checkout the service's constructor to know which keys are required. +  # +  # Then, in your application's configuration, you can specify the service to +  # use like this: +  # +  #   config.active_storage.service = :local +  # +  # If you are using Active Storage outside of a Ruby on Rails application, you +  # can configure the service to use like this: +  # +  #   ActiveStorage::Blob.service = ActiveStorage::Service.configure( +  #     :Disk, +  #     root: Pathname("/foo/bar/storage") +  #   ) +  class Service +    extend ActiveSupport::Autoload +    autoload :Configurator -  extend ActiveSupport::Autoload -  autoload :Configurator +    class_attribute :logger -  class_attribute :logger +    class << self +      # Configure an Active Storage service by name from a set of configurations, +      # typically loaded from a YAML file. The Active Storage engine uses this +      # to set the global Active Storage service when the app boots. +      def configure(service_name, configurations) +        Configurator.build(service_name, configurations) +      end -  class << self -    # Configure an Active Storage service by name from a set of configurations, -    # typically loaded from a YAML file. The Active Storage engine uses this -    # to set the global Active Storage service when the app boots. -    def configure(service_name, configurations) -      Configurator.build(service_name, configurations) +      # Override in subclasses that stitch together multiple services and hence +      # need to build additional services using the configurator. +      # +      # Passes the configurator and all of the service's config as keyword args. +      # +      # See MirrorService for an example. +      def build(configurator:, service: nil, **service_config) #:nodoc: +        new(**service_config) +      end      end -    # Override in subclasses that stitch together multiple services and hence -    # need to build additional services using the configurator. -    # -    # Passes the configurator and all of the service's config as keyword args. -    # -    # See MirrorService for an example. -    def build(configurator:, service: nil, **service_config) #:nodoc: -      new(**service_config) +    # Upload the `io` to the `key` specified. If a `checksum` is provided, the service will +    # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. +    def upload(key, io, checksum: nil) +      raise NotImplementedError      end -  end - -  # Upload the `io` to the `key` specified. If a `checksum` is provided, the service will -  # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. -  def upload(key, io, checksum: nil) -    raise NotImplementedError -  end - -  # Return the content of the file at the `key`. -  def download(key) -    raise NotImplementedError -  end -  # Delete the file at the `key`. -  def delete(key) -    raise NotImplementedError -  end - -  # Return true if a file exists at the `key`. -  def exist?(key) -    raise NotImplementedError -  end +    # Return the content of the file at the `key`. +    def download(key) +      raise NotImplementedError +    end -  # Returns a signed, temporary URL for the file at the `key`. The URL will be valid for the amount -  # of seconds specified in `expires_in`. You most also provide the `disposition` (`:inline` or `:attachment`), -  # `filename`, and `content_type` that you wish the file to be served with on request. -  def url(key, expires_in:, disposition:, filename:, content_type:) -    raise NotImplementedError -  end +    # Delete the file at the `key`. +    def delete(key) +      raise NotImplementedError +    end -  # Returns a signed, temporary URL that a direct upload file can be PUT to on the `key`. -  # The URL will be valid for the amount of seconds specified in `expires_in`. -  # You most also provide the `content_type`, `content_length`, and `checksum` of the file -  # that will be uploaded. All these attributes will be validated by the service upon upload. -  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) -    raise NotImplementedError -  end +    # Return true if a file exists at the `key`. +    def exist?(key) +      raise NotImplementedError +    end -  # Returns a Hash of headers for `url_for_direct_upload` requests. -  def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:) -    {} -  end +    # Returns a signed, temporary URL for the file at the `key`. The URL will be valid for the amount +    # of seconds specified in `expires_in`. You most also provide the `disposition` (`:inline` or `:attachment`), +    # `filename`, and `content_type` that you wish the file to be served with on request. +    def url(key, expires_in:, disposition:, filename:, content_type:) +      raise NotImplementedError +    end -  private -    def instrument(operation, key, payload = {}, &block) -      ActiveSupport::Notifications.instrument( -        "service_#{operation}.active_storage", -        payload.merge(key: key, service: service_name), &block) +    # Returns a signed, temporary URL that a direct upload file can be PUT to on the `key`. +    # The URL will be valid for the amount of seconds specified in `expires_in`. +    # You most also provide the `content_type`, `content_length`, and `checksum` of the file +    # that will be uploaded. All these attributes will be validated by the service upon upload. +    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) +      raise NotImplementedError      end -    def service_name -      # ActiveStorage::Service::DiskService => Disk -      self.class.name.split("::").third.remove("Service") +    # Returns a Hash of headers for `url_for_direct_upload` requests. +    def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:) +      {}      end + +    private +      def instrument(operation, key, payload = {}, &block) +        ActiveSupport::Notifications.instrument( +          "service_#{operation}.active_storage", +          payload.merge(key: key, service: service_name), &block) +      end + +      def service_name +        # ActiveStorage::Service::DiskService => Disk +        self.class.name.split("::").third.remove("Service") +      end +  end  end diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index e13b32eb98..c2e1b25a43 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -2,114 +2,116 @@ require "active_support/core_ext/numeric/bytes"  require "azure/storage"  require "azure/storage/core/auth/shared_access_signature" -# Wraps the Microsoft Azure Storage Blob Service as a Active Storage service. -# See `ActiveStorage::Service` for the generic API documentation that applies to all services. -class ActiveStorage::Service::AzureStorageService < ActiveStorage::Service -  attr_reader :client, :path, :blobs, :container, :signer - -  def initialize(path:, storage_account_name:, storage_access_key:, container:) -    @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key) -    @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key) -    @blobs = client.blob_client -    @container = container -    @path = path -  end +module ActiveStorage +  # Wraps the Microsoft Azure Storage Blob Service as a Active Storage service. +  # See `ActiveStorage::Service` for the generic API documentation that applies to all services. +  class Service::AzureStorageService < Service +    attr_reader :client, :path, :blobs, :container, :signer + +    def initialize(path:, storage_account_name:, storage_access_key:, container:) +      @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key) +      @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key) +      @blobs = client.blob_client +      @container = container +      @path = path +    end -  def upload(key, io, checksum: nil) -    instrument :upload, key, checksum: checksum do -      begin -        blobs.create_block_blob(container, key, io, content_md5: checksum) -      rescue Azure::Core::Http::HTTPError -        raise ActiveStorage::IntegrityError +    def upload(key, io, checksum: nil) +      instrument :upload, key, checksum: checksum do +        begin +          blobs.create_block_blob(container, key, io, content_md5: checksum) +        rescue Azure::Core::Http::HTTPError +          raise ActiveStorage::IntegrityError +        end        end      end -  end -  def download(key) -    if block_given? -      instrument :streaming_download, key do -        stream(key, &block) -      end -    else -      instrument :download, key do -        _, io = blobs.get_blob(container, key) -        io.force_encoding(Encoding::BINARY) +    def download(key) +      if block_given? +        instrument :streaming_download, key do +          stream(key, &block) +        end +      else +        instrument :download, key do +          _, io = blobs.get_blob(container, key) +          io.force_encoding(Encoding::BINARY) +        end        end      end -  end -  def delete(key) -    instrument :delete, key do -      begin -        blobs.delete_blob(container, key) -      rescue Azure::Core::Http::HTTPError -        false +    def delete(key) +      instrument :delete, key do +        begin +          blobs.delete_blob(container, key) +        rescue Azure::Core::Http::HTTPError +          false +        end        end      end -  end -  def exist?(key) -    instrument :exist, key do |payload| -      answer = blob_for(key).present? -      payload[:exist] = answer -      answer +    def exist?(key) +      instrument :exist, key do |payload| +        answer = blob_for(key).present? +        payload[:exist] = answer +        answer +      end      end -  end -  def url(key, expires_in:, disposition:, filename:) -    instrument :url, key do |payload| -      base_url = url_for(key) -      generated_url = signer.signed_uri(URI(base_url), false, permissions: "r", -        expiry: format_expiry(expires_in), content_disposition: "#{disposition}; filename=\"#{filename}\"").to_s +    def url(key, expires_in:, disposition:, filename:) +      instrument :url, key do |payload| +        base_url = url_for(key) +        generated_url = signer.signed_uri(URI(base_url), false, permissions: "r", +          expiry: format_expiry(expires_in), content_disposition: "#{disposition}; filename=\"#{filename}\"").to_s -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) -    instrument :url, key do |payload| -      base_url = url_for(key) -      generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw", -        expiry: format_expiry(expires_in)).to_s +    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) +      instrument :url, key do |payload| +        base_url = url_for(key) +        generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw", +          expiry: format_expiry(expires_in)).to_s -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end - -  def headers_for_direct_upload(key, content_type:, checksum:, **) -    { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" } -  end -  private -    def url_for(key) -      "#{path}/#{container}/#{key}" +    def headers_for_direct_upload(key, content_type:, checksum:, **) +      { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }      end -    def blob_for(key) -      blobs.get_blob_properties(container, key) -    rescue Azure::Core::Http::HTTPError -      false -    end +    private +      def url_for(key) +        "#{path}/#{container}/#{key}" +      end -    def format_expiry(expires_in) -      expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil -    end +      def blob_for(key) +        blobs.get_blob_properties(container, key) +      rescue Azure::Core::Http::HTTPError +        false +      end + +      def format_expiry(expires_in) +        expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil +      end -    # Reads the object for the given key in chunks, yielding each to the block. -    def stream(key, options = {}, &block) -      blob = blob_for(key) +      # Reads the object for the given key in chunks, yielding each to the block. +      def stream(key, options = {}, &block) +        blob = blob_for(key) -      chunk_size = 5.megabytes -      offset = 0 +        chunk_size = 5.megabytes +        offset = 0 -      while offset < blob.properties[:content_length] -        _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1) -        yield io -        offset += chunk_size +        while offset < blob.properties[:content_length] +          _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1) +          yield io +          offset += chunk_size +        end        end -    end +  end  end diff --git a/activestorage/lib/active_storage/service/configurator.rb b/activestorage/lib/active_storage/service/configurator.rb index a0afdaa912..5d6475a8ae 100644 --- a/activestorage/lib/active_storage/service/configurator.rb +++ b/activestorage/lib/active_storage/service/configurator.rb @@ -1,28 +1,30 @@ -class ActiveStorage::Service::Configurator #:nodoc: -  attr_reader :configurations +module ActiveStorage +  class Service::Configurator #:nodoc: +    attr_reader :configurations -  def self.build(service_name, configurations) -    new(configurations).build(service_name) -  end +    def self.build(service_name, configurations) +      new(configurations).build(service_name) +    end -  def initialize(configurations) -    @configurations = configurations.deep_symbolize_keys -  end +    def initialize(configurations) +      @configurations = configurations.deep_symbolize_keys +    end -  def build(service_name) -    config = config_for(service_name.to_sym) -    resolve(config.fetch(:service)).build(**config, configurator: self) -  end +    def build(service_name) +      config = config_for(service_name.to_sym) +      resolve(config.fetch(:service)).build(**config, configurator: self) +    end -  private -    def config_for(name) -      configurations.fetch name do -        raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}" +    private +      def config_for(name) +        configurations.fetch name do +          raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}" +        end        end -    end -    def resolve(class_name) -      require "active_storage/service/#{class_name.to_s.underscore}_service" -      ActiveStorage::Service.const_get(:"#{class_name}Service") -    end +      def resolve(class_name) +        require "active_storage/service/#{class_name.to_s.underscore}_service" +        ActiveStorage::Service.const_get(:"#{class_name}Service") +      end +  end  end diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 35b0909297..3d92102cf0 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -3,122 +3,125 @@ require "pathname"  require "digest/md5"  require "active_support/core_ext/numeric/bytes" -# Wraps a local disk path as a Active Storage service. See `ActiveStorage::Service` for the generic API -# documentation that applies to all services. -class ActiveStorage::Service::DiskService < ActiveStorage::Service -  attr_reader :root - -  def initialize(root:) -    @root = root -  end +module ActiveStorage +  # Wraps a local disk path as a Active Storage service. See `ActiveStorage::Service` for the generic API +  # documentation that applies to all services. +  class Service::DiskService < Service +    attr_reader :root + +    def initialize(root:) +      @root = root +    end -  def upload(key, io, checksum: nil) -    instrument :upload, key, checksum: checksum do -      IO.copy_stream(io, make_path_for(key)) -      ensure_integrity_of(key, checksum) if checksum +    def upload(key, io, checksum: nil) +      instrument :upload, key, checksum: checksum do +        IO.copy_stream(io, make_path_for(key)) +        ensure_integrity_of(key, checksum) if checksum +      end      end -  end -  def download(key) -    if block_given? -      instrument :streaming_download, key do -        File.open(path_for(key), "rb") do |file| -          while data = file.read(64.kilobytes) -            yield data +    def download(key) +      if block_given? +        instrument :streaming_download, key do +          File.open(path_for(key), "rb") do |file| +            while data = file.read(64.kilobytes) +              yield data +            end            end          end -      end -    else -      instrument :download, key do -        File.binread path_for(key) +      else +        instrument :download, key do +          File.binread path_for(key) +        end        end      end -  end -  def delete(key) -    instrument :delete, key do -      begin -        File.delete path_for(key) -      rescue Errno::ENOENT -        # Ignore files already deleted +    def delete(key) +      instrument :delete, key do +        begin +          File.delete path_for(key) +        rescue Errno::ENOENT +          # Ignore files already deleted +        end        end      end -  end -  def exist?(key) -    instrument :exist, key do |payload| -      answer = File.exist? path_for(key) -      payload[:exist] = answer -      answer +    def exist?(key) +      instrument :exist, key do |payload| +        answer = File.exist? path_for(key) +        payload[:exist] = answer +        answer +      end      end -  end -  def url(key, expires_in:, disposition:, filename:, content_type:) -    instrument :url, key do |payload| -      verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) - -      generated_url = -        if defined?(Rails.application) -          Rails.application.routes.url_helpers.rails_disk_service_path \ -            verified_key_with_expiration, -            disposition: disposition, filename: filename, content_type: content_type -        else -          "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}" -        end +    def url(key, expires_in:, disposition:, filename:, content_type:) +      instrument :url, key do |payload| +        verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) + +        generated_url = +          if defined?(Rails.application) +            Rails.application.routes.url_helpers.rails_disk_service_path \ +              verified_key_with_expiration, +              disposition: disposition, filename: filename, content_type: content_type +          else +            "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}" +          end -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) -    instrument :url, key do |payload| -      verified_token_with_expiration = ActiveStorage.verifier.generate( -        { -          key: key, -          content_type: content_type, -          content_length: content_length, -          checksum: checksum -        }, -        expires_in: expires_in, -        purpose: :blob_token -      ) - -      generated_url = -        if defined?(Rails.application) -          Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration -        else -          "/rails/active_storage/disk/#{verified_token_with_expiration}" -        end +    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) +      instrument :url, key do |payload| +        verified_token_with_expiration = ActiveStorage.verifier.generate( +          { +            key: key, +            content_type: content_type, +            content_length: content_length, +            checksum: checksum +          }, +          expires_in: expires_in, +          purpose: :blob_token +        ) + +        generated_url = +          if defined?(Rails.application) +            Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration +          else +            "/rails/active_storage/disk/#{verified_token_with_expiration}" +          end -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def headers_for_direct_upload(key, content_type:, **) -    { "Content-Type" => content_type } -  end - -  private -    def path_for(key) -      File.join root, folder_for(key), key +    def headers_for_direct_upload(key, content_type:, **) +      { "Content-Type" => content_type }      end -    def folder_for(key) -      [ key[0..1], key[2..3] ].join("/") -    end +    private +      def path_for(key) +        File.join root, folder_for(key), key +      end -    def make_path_for(key) -      path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } -    end +      def folder_for(key) +        [ key[0..1], key[2..3] ].join("/") +      end -    def ensure_integrity_of(key, checksum) -      unless Digest::MD5.file(path_for(key)).base64digest == checksum -        delete key -        raise ActiveStorage::IntegrityError +      def make_path_for(key) +        path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }        end -    end + +      def ensure_integrity_of(key, checksum) +        unless Digest::MD5.file(path_for(key)).base64digest == checksum +          delete key +          raise ActiveStorage::IntegrityError +        end +      end +  end  end + diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index 73629f7486..ea4ec5a790 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -1,79 +1,81 @@  require "google/cloud/storage"  require "active_support/core_ext/object/to_query" -# Wraps the Google Cloud Storage as a Active Storage service. See `ActiveStorage::Service` for the generic API -# documentation that applies to all services. -class ActiveStorage::Service::GCSService < ActiveStorage::Service -  attr_reader :client, :bucket +module ActiveStorage +  # Wraps the Google Cloud Storage as a Active Storage service. See `ActiveStorage::Service` for the generic API +  # documentation that applies to all services. +  class Service::GCSService < Service +    attr_reader :client, :bucket -  def initialize(project:, keyfile:, bucket:) -    @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile) -    @bucket = @client.bucket(bucket) -  end +    def initialize(project:, keyfile:, bucket:) +      @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile) +      @bucket = @client.bucket(bucket) +    end -  def upload(key, io, checksum: nil) -    instrument :upload, key, checksum: checksum do -      begin -        bucket.create_file(io, key, md5: checksum) -      rescue Google::Cloud::InvalidArgumentError -        raise ActiveStorage::IntegrityError +    def upload(key, io, checksum: nil) +      instrument :upload, key, checksum: checksum do +        begin +          bucket.create_file(io, key, md5: checksum) +        rescue Google::Cloud::InvalidArgumentError +          raise ActiveStorage::IntegrityError +        end        end      end -  end -  # FIXME: Add streaming when given a block -  def download(key) -    instrument :download, key do -      io = file_for(key).download -      io.rewind -      io.read +    # FIXME: Add streaming when given a block +    def download(key) +      instrument :download, key do +        io = file_for(key).download +        io.rewind +        io.read +      end      end -  end -  def delete(key) -    instrument :delete, key do -      file_for(key).try(:delete) +    def delete(key) +      instrument :delete, key do +        file_for(key).try(:delete) +      end      end -  end -  def exist?(key) -    instrument :exist, key do |payload| -      answer = file_for(key).present? -      payload[:exist] = answer -      answer +    def exist?(key) +      instrument :exist, key do |payload| +        answer = file_for(key).present? +        payload[:exist] = answer +        answer +      end      end -  end -  def url(key, expires_in:, disposition:, filename:, content_type:) -    instrument :url, key do |payload| -      generated_url = file_for(key).signed_url expires: expires_in, query: { -        "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"", -        "response-content-type" => content_type -      } +    def url(key, expires_in:, disposition:, filename:, content_type:) +      instrument :url, key do |payload| +        generated_url = file_for(key).signed_url expires: expires_in, query: { +          "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"", +          "response-content-type" => content_type +        } -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) -    instrument :url, key do |payload| -      generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, -        content_type: content_type, content_md5: checksum +    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) +      instrument :url, key do |payload| +        generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, +          content_type: content_type, content_md5: checksum -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def headers_for_direct_upload(key, content_type:, checksum:, **) -    { "Content-Type" => content_type, "Content-MD5" => checksum } -  end - -  private -    def file_for(key) -      bucket.file(key) +    def headers_for_direct_upload(key, content_type:, checksum:, **) +      { "Content-Type" => content_type, "Content-MD5" => checksum }      end + +    private +      def file_for(key) +        bucket.file(key) +      end +  end  end diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb index 7c407f2730..2403eeb1e9 100644 --- a/activestorage/lib/active_storage/service/mirror_service.rb +++ b/activestorage/lib/active_storage/service/mirror_service.rb @@ -1,46 +1,48 @@  require "active_support/core_ext/module/delegation" -# Wraps a set of mirror services and provides a single `ActiveStorage::Service` object that will all -# have the files uploaded to them. A `primary` service is designated to answer calls to `download`, `exists?`, -# and `url`. -class ActiveStorage::Service::MirrorService < ActiveStorage::Service -  attr_reader :primary, :mirrors - -  delegate :download, :exist?, :url, to: :primary - -  # Stitch together from named services. -  def self.build(primary:, mirrors:, configurator:, **options) #:nodoc: -    new \ -      primary: configurator.build(primary), -      mirrors: mirrors.collect { |name| configurator.build name } -  end - -  def initialize(primary:, mirrors:) -    @primary, @mirrors = primary, mirrors -  end - -  # Upload the `io` to the `key` specified to all services. If a `checksum` is provided, all services will -  # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. -  def upload(key, io, checksum: nil) -    each_service.collect do |service| -      service.upload key, io.tap(&:rewind), checksum: checksum +module ActiveStorage +  # Wraps a set of mirror services and provides a single `ActiveStorage::Service` object that will all +  # have the files uploaded to them. A `primary` service is designated to answer calls to `download`, `exists?`, +  # and `url`. +  class Service::MirrorService < Service +    attr_reader :primary, :mirrors + +    delegate :download, :exist?, :url, to: :primary + +    # Stitch together from named services. +    def self.build(primary:, mirrors:, configurator:, **options) #:nodoc: +      new \ +        primary: configurator.build(primary), +        mirrors: mirrors.collect { |name| configurator.build name }      end -  end - -  # Delete the file at the `key` on all services. -  def delete(key) -    perform_across_services :delete, key -  end -  private -    def each_service(&block) -      [ primary, *mirrors ].each(&block) +    def initialize(primary:, mirrors:) +      @primary, @mirrors = primary, mirrors      end -    def perform_across_services(method, *args) -      # FIXME: Convert to be threaded +    # Upload the `io` to the `key` specified to all services. If a `checksum` is provided, all services will +    # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. +    def upload(key, io, checksum: nil)        each_service.collect do |service| -        service.public_send method, *args +        service.upload key, io.tap(&:rewind), checksum: checksum        end      end + +    # Delete the file at the `key` on all services. +    def delete(key) +      perform_across_services :delete, key +    end + +    private +      def each_service(&block) +        [ primary, *mirrors ].each(&block) +      end + +      def perform_across_services(method, *args) +        # FIXME: Convert to be threaded +        each_service.collect do |service| +          service.public_send method, *args +        end +      end +  end  end diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index ca461c2994..5153f5db0d 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -1,96 +1,98 @@  require "aws-sdk"  require "active_support/core_ext/numeric/bytes" -# Wraps the Amazon Simple Storage Service (S3) as a Active Storage service. -# See `ActiveStorage::Service` for the generic API documentation that applies to all services. -class ActiveStorage::Service::S3Service < ActiveStorage::Service -  attr_reader :client, :bucket, :upload_options +module ActiveStorage +  # Wraps the Amazon Simple Storage Service (S3) as a Active Storage service. +  # See `ActiveStorage::Service` for the generic API documentation that applies to all services. +  class Service::S3Service < Service +    attr_reader :client, :bucket, :upload_options -  def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options) -    @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options) -    @bucket = @client.bucket(bucket) +    def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options) +      @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options) +      @bucket = @client.bucket(bucket) -    @upload_options = upload -  end +      @upload_options = upload +    end -  def upload(key, io, checksum: nil) -    instrument :upload, key, checksum: checksum do -      begin -        object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) -      rescue Aws::S3::Errors::BadDigest -        raise ActiveStorage::IntegrityError +    def upload(key, io, checksum: nil) +      instrument :upload, key, checksum: checksum do +        begin +          object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) +        rescue Aws::S3::Errors::BadDigest +          raise ActiveStorage::IntegrityError +        end        end      end -  end -  def download(key) -    if block_given? -      instrument :streaming_download, key do -        stream(key, &block) -      end -    else -      instrument :download, key do -        object_for(key).get.body.read.force_encoding(Encoding::BINARY) +    def download(key) +      if block_given? +        instrument :streaming_download, key do +          stream(key, &block) +        end +      else +        instrument :download, key do +          object_for(key).get.body.read.force_encoding(Encoding::BINARY) +        end        end      end -  end -  def delete(key) -    instrument :delete, key do -      object_for(key).delete +    def delete(key) +      instrument :delete, key do +        object_for(key).delete +      end      end -  end -  def exist?(key) -    instrument :exist, key do |payload| -      answer = object_for(key).exists? -      payload[:exist] = answer -      answer +    def exist?(key) +      instrument :exist, key do |payload| +        answer = object_for(key).exists? +        payload[:exist] = answer +        answer +      end      end -  end -  def url(key, expires_in:, disposition:, filename:, content_type:) -    instrument :url, key do |payload| -      generated_url = object_for(key).presigned_url :get, expires_in: expires_in, -        response_content_disposition: "#{disposition}; filename=\"#{filename}\"", -        response_content_type: content_type +    def url(key, expires_in:, disposition:, filename:, content_type:) +      instrument :url, key do |payload| +        generated_url = object_for(key).presigned_url :get, expires_in: expires_in, +          response_content_disposition: "#{disposition}; filename=\"#{filename}\"", +          response_content_type: content_type -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end -  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) -    instrument :url, key do |payload| -      generated_url = object_for(key).presigned_url :put, expires_in: expires_in, -        content_type: content_type, content_length: content_length, content_md5: checksum +    def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) +      instrument :url, key do |payload| +        generated_url = object_for(key).presigned_url :put, expires_in: expires_in, +          content_type: content_type, content_length: content_length, content_md5: checksum -      payload[:url] = generated_url +        payload[:url] = generated_url -      generated_url +        generated_url +      end      end -  end - -  def headers_for_direct_upload(key, content_type:, checksum:, **) -    { "Content-Type" => content_type, "Content-MD5" => checksum } -  end -  private -    def object_for(key) -      bucket.object(key) +    def headers_for_direct_upload(key, content_type:, checksum:, **) +      { "Content-Type" => content_type, "Content-MD5" => checksum }      end -    # Reads the object for the given key in chunks, yielding each to the block. -    def stream(key, options = {}, &block) -      object = object_for(key) +    private +      def object_for(key) +        bucket.object(key) +      end + +      # Reads the object for the given key in chunks, yielding each to the block. +      def stream(key, options = {}, &block) +        object = object_for(key) -      chunk_size = 5.megabytes -      offset = 0 +        chunk_size = 5.megabytes +        offset = 0 -      while offset < object.content_length -        yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}")) -        offset += chunk_size +        while offset < object.content_length +          yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}")) +          offset += chunk_size +        end        end -    end +  end  end | 
